Repository navigation
fix: on the Ângulo, GET /api/keypad sends the faces and points to /api/ui, not the original's geometry (F4) - #311
Merged
Merged
Conversation
…(F4)
F4 of v2.13.0's Known list: on the Ângulo, GET /api/keypad publishes the
original panel's grid and popup geometry, so a client that taps by it
misses the Ângulo's keys. The maintainer chose option (c) on 2026-10-31:
on the Ângulo the route sends faces, up, kb and policy plus
"targets":"/api/ui", and no grid and no pop; on the original it stays as
it is, byte for byte. The two bench tools that tap the original panel by
the grid are to refuse an Ângulo answer through one shared parser.
This commit is the test, written first. tools/test_keypad_answer.py reads
tools/keypad_answer.py, which does not exist yet, so on this commit the
test is red:
$ python3 tools/test_keypad_answer.py
test_keypad_answer: FAILED, tools/keypad_answer.py cannot be imported
(No module named 'keypad_answer') exit 1
It has four cases (an original answer, an Ângulo answer, an answer older
than v25, the groups keyboard with its pop), two checks that the tools
read the route only through the parser, and four controls: the parser run
again with one line taken out must fail exactly the cases that line
serves (the refusal out: the Ângulo case alone). It is added to the gates
job of the workflow next to test_ui_caps.py.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uy7K2mzEmmjBnpxpdYNDFu
…ls; it refuses the Ângulo's answer (F4)
panel_users_hw_test.py and panel_fulltable_test.py tap the original panel
by the grid that GET /api/keypad publishes. The Ângulo's answer will not
carry one (option (c): faces, up, kb, policy and "targets":"/api/ui"), so
both tools now read the route through tools/keypad_answer.py, which
refuses an answer with "targets" and says where to tap instead (the
targets of /api/ui, as tools/tft_ui/roteiro_ui.py does).
- keypad_answer.parse( ) holds what Rig.keypad_faces( ) did inline (faces,
kb, grid, pop, policy, with the same shape checks) and the v24 layout
(PIN_GRID_FALLBACK, moved here and imported back, because
panel_fulltable_test.py takes it from panel_users_hw_test.py). An answer
with no grid and no targets is a device older than v25, and keeps the v24
layout: the refusal comes from "targets", not from the missing grid.
- The refusal is a SystemExit and not a KeypadGone: the callers reopen the
keypad and read again on a KeypadGone, and the device would answer the
same.
- Rig also checks once at its start (refuse_angulo( )), so that the run
stops before it makes accounts and re-points the telemetry. A route that
cannot be read is said and passed over, not taken for the Ângulo.
- panel_fulltable_test.py reads the policy it prints through the parser; it
prints the same list as before.
Evidence (no device was contacted):
- T3, red on the commit before this one: the module did not exist
("cannot be imported", exit 1). Now:
$ python3 tools/test_keypad_answer.py -v
case original ok / case angulo ok / case pre-v25 ok / case groups ok
control "the refusal taken out": fails ['angulo'], the line serves ['angulo']
control "the v24 fallback taken out": fails ['pre-v25'] (serves ['pre-v25'])
control "pop taken out": fails ['groups'] (serves ['groups'])
control "grid ignored": fails ['groups', 'original'] (serves the same)
4 cases pass; 4 of 4 controls bite; 2 of 2 tools read the route through the parser.
- Negative control on the file: with the refusal's raise replaced by pass
in a copy of tools/, exactly one case fails (angulo: "an answer with
`targets` was accepted"), the other three pass; the real file passes.
- A/B of Rig.keypad_faces( ), the version of 6981fd9 against this one, on
15 answers that are not the Ângulo's (4, 6 and 12 cards, the numeric pad
with its two empty keys, the groups keyboard with pop, an answer older
than v25, keypad not up, grids, pops and policies cut short, kb missing
or empty, faces null or of the wrong count): the same return or
exception, and the same kb, pop, grid and policy on all 15. On the
Ângulo's two answers the old code fell back to the v24 grid and went on;
the new one stops with the message.
- tools/README.md: two rows (test_keypad_answer.py, keypad_answer.py) and
the counts, measured: the "Called by CI" heading already said 73 for 74
rows at 6981fd9, so the total was 212 and not 211; it is 214 now (75, 17,
64, 58), here and in CLAUDE.md.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uy7K2mzEmmjBnpxpdYNDFu
…ts keys, not the original's geometry (F4)
F4 of v2.13.0's Known list: on an image built with the Ângulo, the route
published the original panel's grid and popup geometry, so a client that
taps by it misses the Ângulo's keys (for four cards the original's are
152x44 at 6,72, pitch 156/50, and the Ângulo's 146x44 at 12,96, pitch
150/48; its popup keys are 48 px wide, 52 tall on one row and 36 on two,
where the original's are 56x52).
The maintainer's option (c), 2026-10-31. On the Ângulo the answer is
{"faces":[...],"up":...,"kb":"...","policy":[...],"targets":"/api/ui"}:
the core's state, which means the same on both panels (the Ângulo's key k
is PinKb's key k), and the route that lists where the keys are; no grid,
no pop. On the original the statements are the ones that were, untouched.
- The switch is a UI hook, not an #if: uiKeypadTargets( ) is nullptr on
the original (UI_HOOK, always_inline, src/ui/UiDriverMembers.h, beside
uiLangLoaded( )) and "/api/ui" on the Ângulo (defined in the class,
src/ui/angulo/Members.h, like uaSujo( ), so the handler sees a constant).
The comment of src/ui/UiDriver.h that said the Ângulo's hooks are
ordinary functions gains the clause for this one.
- handleApiKeypad( ) builds the Ângulo's answer when the hook answers,
and returns; otherwise the grid and pop statements run, textually
unchanged. No #if was added: tools/feature_sprawl.json is unchanged and
check_feature_sprawl.py passes at the same count.
- tools/ui_texts_allow.json: "/api/ui" in Members.h is a route path and
not text the panel draws (group 6, the bench endpoint's route). Without
the entry check_ui_texts.py reports exactly "src/ui/angulo/Members.h:17:
"/api/ui"" (run on a copy), with it 0 findings.
- Comments that described the route are corrected: the handler's own, and
those of PinKeypad.h, ui/original/PinKeypadLayout.h (two places) and
DisplayManager.h (pinGrid( )). The comment at DisplayManager.h:1185
(the CLI and /api/keypad read the faces through pinKeyFace( )) is still
true on both panels and stays.
Proof, all eleven images built from zero on this tree and on 6981fd9
through the pio shim, one at a time under the build lock, -Werror holding,
each held to tools/check_flash_budget.py before the budget edit that follows:
- T1, the bytes of each firmware.bin (stage script, out/t1_bin_grep.py).
Red on the base: six failures, all in the Ângulo section (no
',"targets":"', and the "pop":[%d and "grid":[ format strings still
there, on both Ângulo images). Green on this tree: 49 of 49 checks. Its
control (an Ângulo image read under an original's name) fails exactly the
six checks of the two swapped images.
The briefing said the nine others must contain the pop string. Three of
them cannot: pico_w_alpha, pico_w_air and pico_wire have no TFT, so
handleApiKeypad( ) is not compiled in them (0 occurrences of the route
on the base). T1 asks of those three that neither answer is there, and of
the six with the route (pico_w_release, _test, _test_https, _asserts,
pico_wiretft, pico2_w_original) that the pop and grid format strings are
there once and "targets" is not.
- The nine images that are not the Ângulo's: sha256, size and name equal to
the base's, `diff` of the two lists empty (a39bf356, 2a01445f, dbc57bfe,
a490b838, bf5381ff, 0fe6a215, 480268b8, d6cdd1f0, 3f3ef7aa). No __LINE__
is used in src/, and the line count of WebManager_History.cpp changed.
- The Ângulo's two images (the sha256 changes, the .bin shrinks):
pico2_w_release 1,038,352 -> 1,038,144 B (-208; used 1,038,328 -> 1,038,120)
pico_wiretft_angulo 829,200 -> 828,984 B (-216; used 817,164 -> 816,948)
handleApiKeypad( ) goes from 508 to 348 B (24 + a 324 B .part.0) on the
Pico 2 W and from 488 to 326 B on the Wire; the strings, -52 B (the grid
and policy format 82, the pop format 27 and the closing "}" 2 are out,
the new format 59 is in; "/api/ui" is the literal the route registration
already holds, merged by the linker, 0 B). The rest is alignment.
- check_authz.py exit 0 and its --list identical to the base's;
check_readme_numbers.py exit 0; check_ui_boundary.py 0 findings;
test_ui_boundary.py 316 of 316; check_ui_texts.py 0 findings;
check_ui_link.py 0 findings and 12 of 12 allowances on both new ELFs;
test_keypad_answer.py green.
Not run on a board and no device was contacted: the answer was proved by
the images' bytes and by the host tests. The budget, the documents and the
generated manual follow in their own commits.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uy7K2mzEmmjBnpxpdYNDFu
pico2_w_release and pico_wiretft_angulo shrink with the F4 change (the original panel's grid and popup format strings and the code that wrote them are out of the Ângulo's images). The budget is a high-water mark that comes down in the change that lowers it: pico2_w_release used 1,038,328 -> 1,038,120 (-208) .bin 1,038,352 -> 1,038,144 pico_wiretft_angulo used 817,164 -> 816,948 (-216) .bin 829,200 -> 828,984 The nine other records stay: those images have the sha256 of 6981fd9's. The OTA headroom table of PLANO_STABLE.md section 3.1 follows the .bin records (signed, + 241 B): pico2_w_release 1.038.385 B with 530.383 B of slot left (530.175 before), pico_wiretft_angulo 829.225 B with 211.159 B under the OTA ceiling (210.943 before). All eleven built from zero on the tip of the F4 change (the baseline 6981fd9 first, equal to every record), each held to the gate on its own build log with its .bin present: $ python3 tools/check_flash_budget.py <env> out/tip/<env>.log (eleven times) rc=0 for all eleven (OK for eight, WARN for pico_w_release, pico_w_test and pico_w_asserts as before: each under one flash sector of the OTA ceiling, 3,731, 2,947 and 1,427 B; SKIP for the exempt pico_w_test_https) $ python3 tools/check_flash_budget.py --table OK the OTA headroom table matches the record (11 images, 1 exempt). Not run on a board. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Uy7K2mzEmmjBnpxpdYNDFu
Each hit of `grep -n "api/keypad"` was read against the code (the handler in WebManager_History.cpp, uiKeypadTargets( ) in the two driver headers). Changed, because they described the grid and the popup as the route's answer on every image: - AGENTS.md (the bench manual, Portuguese): the grid and policy are read from the route "never from constants", and that holds for the original panel; on the Ângulo images the answer has no grid and no pop and names /api/ui, and the two original-panel tools refuse it (keypad_answer.py). The pop line says it is the original's. - docs/AUTHORIZATION.md: same gate (PERM_SYS_CONFIG) on both panels; what the answer carries differs. - docs/MANUAL.md and docs/MANUAL.pt-BR.md, the route table row: grid and pop on the original, "targets":"/api/ui" in their place on the Ângulo. - docs/_manual/cap26-api-rest.md, the source of the generated manual: the route table row, and the example. The example was not an answer the firmware writes (kb "num" with four faces and a grid that is no layout of PinKb); it is now the factory four-card answer, followed by the Ângulo's, with a paragraph on each field and on which to read. The generated HTML follows in its own commit. - docs/analysis/UI_DRIVERS.md (Living): the three places that called F4 open (the S9 list, S10a's F4 and S10b-1's F4) say it is closed, with the maintainer's option (c), the hook and the result; the two places that said the route publishes the geometry (the PinKeypad.h row of section 4.2 and the S9 audit bullet) say "on the original panel's images only". Read and left as they are: cap26 line 537 (the route's one-line summary, "the faces of the PIN keypad", true on both panels), A3's record in UI_DRIVERS.md (what was tried on 2026-10-30, with the route as it was), MODELO_DE_RECURSOS.md (the route is a bench switch that only the TFT images carry, 368 B, still true), tools/route_sweep.py and tools/build_webui_gz.py (they say the route is left out of the sweep and registered by the TFT images only), the historical entries of both CHANGELOGs and the strings of tools/flash_budget.json. The v2.13.0 Known list keeps F4: it is the record of what that release shipped; the Unreleased section closes it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Uy7K2mzEmmjBnpxpdYNDFu
…13.0's Known list, with its flash table A section on GET /api/keypad on the Ângulo after the Flash section of F3 and the alignment, and a sentence in the Unreleased summary. It says what was wrong (the original's grid and popup on the Ângulo's images), what the route sends now (the maintainer's option (c)), how it is switched (the uiKeypadTargets( ) hook, a constant on each side, no #if), the bench tools, the tests, and that nothing was run on a board. The flash table follows the table above it: the signed firmware.bin (the image plus its 241-byte signature), pico_w_test_https unsigned, against main at 6981fd9, all eleven from zero. The same numbers as the build's .bin, without the signature, are in the text: pico2_w_release 1,038,593 -> 1,038,385 (-208) .bin 1,038,352 -> 1,038,144 pico_wiretft_angulo 829,441 -> 829,225 (-216) .bin 829,200 -> 828,984 the other nine unchanged, sha256 equal The slack moves only on the Ângulo's images: 530,383 B left in the Pico 2 W's slot (530,175 before) and 211,159 B under the OTA ceiling on the Wire's Ângulo (210,943). Every figure was read from the build outputs of the tip (out/tip) and from check_flash_budget.py's own lines, not computed twice. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Uy7K2mzEmmjBnpxpdYNDFu
tools/build_manual.py renders docs/_manual/ into the page. This machine's pandoc (3.7.0.2) no longer writes the table row classes the committed page has (header, odd, even), so the page was made in three steps: 1. main (6981fd9) regenerated in a clean export of the tree and compared with its committed page: they differ by 3,240 lines and by nothing else. Every differing line is a <tr class="header|odd|even"> against a bare <tr> (the committed page, with those classes taken out, is the regenerated page, byte for byte). 2. A post-step puts the classes back by pandoc's old rule (header on the rows of the <thead>, odd and even alternating from odd on each <tbody>, on the <tr> that stand alone on a line; build_manual.py's own two tables are one line of compact HTML and never had them). Proof on main: regenerated + post-step is the committed page, cmp equal, sha256 ce4ef77f861af0b5e24164d13bcd3706b4c5d1ded5c0396731612931339be051. 3. This tree (the commit before this one) regenerated the same way. The page differs from main's in 16 diff lines: the route's row in the chapter 26 table, and the example of GET /api/keypad with its paragraph and the Ângulo's example and paragraph. Nothing else moved. python3 tools/build_manual.py --check passes (it is what CI runs). The post-step is a stage script and is not in the repository. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Uy7K2mzEmmjBnpxpdYNDFu
The contract of section 2.2 lists the driver's hooks. The new one is a row: not a body that moved but a constant on each side (nullptr on the original, "/api/ui" on the Ângulo), public because its caller is the web handler, and the reason it is defined in the Ângulo's class. The sentence above the table said all the hooks are private members; it now names the exception. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Uy7K2mzEmmjBnpxpdYNDFu
…top (review of F4)
Rig.__init__( ) runs _login( ), which makes this run's full-admin account
smap over the console, and only then refuse_angulo( ). On an Ângulo
answer the SystemExit came before main( )'s try, so a refused run left
the account on the device: the failure that _login( )'s docstring tells
of. The review found it by running the real __init__ with serial,
requests and the console stubbed.
The keypad read inside the run had the same gap, which the re-review
found: this tool deletes smap only in step_cleanup( ), a step that is
skipped once an earlier one raises, and main( )'s finally only writes
the results and closes the port. (panel_fulltable_test.py's finally does
delete it.) Both handlers of AnguloAnswer now send `user del smap`
before they raise.
Test first: tools/test_keypad_answer.py reads every handler of
AnguloAnswer in panel_users_hw_test.py from the source (the tool imports
serial and requests, so it is read the way the wiring is). It fails
unless there are both and each calls `self.cfg('user del smap')` before
its raise. It was red on refuse_angulo( ) before the first fix and on
the keypad read (line 500) before the second, and is green after.
Run with stubs, the real methods send ['user del smap'] and stop on an
Ângulo answer. On an original answer they send nothing; refuse_angulo( )
returns, and keypad_faces( ) returns the faces.
The CHANGELOG, in both languages, now says the check comes right after
the web login and deletes that login's account, and its tests bullet
names the new check.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uy7K2mzEmmjBnpxpdYNDFu
This was referenced Oct 31, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes F4 of v2.13.0's Known list: on the Ângulo,
GET /api/keypadno longer describes the original panel's keypad. It sends the faces, the state and the PIN policy, and names/api/uias the route that lists where the Ângulo's keys are. The two Ângulo images get 208 and 216 B smaller, and the other nine are byte for byte what they were.The defect
On an image built with the Ângulo,
WebManager::handleApiKeypad( )published the original panel's keypad geometry. A client that taps by those numbers misses the Ângulo's keys.faces,up,kb,policygrid[0..3]: keys, slots, cols, rowsgrid[4..9]: x0, y0, w, h, pitchpop(groups keyboard only)Nothing that ships reads the route:
WebUI.hdoes not, and the Ângulo's bench scripttools/tft_ui/roteiro_ui.pyreads onlyfacesandkband taps by/api/ui. The two bench tools that drive the original panel did read it.The change
The maintainer chose on 2026-10-31 among options (a) to (d) from the plan. His choice:
faces,up,kb,policyand"targets":"/api/ui", with nogridand nopop. On the original panel the answer is unchanged.How the route is switched:
#if. The hook isuiKeypadTargets( ):nullptr, insrc/ui/UiDriverMembers.h;"/api/ui", defined inside the class insrc/ui/angulo/Members.h."targets".tools/ui_texts_allow.jsonnames the route literal inMembers.has a route path, not text the panel draws.The tools:
tools/keypad_answer.pyis one reader fortools/panel_users_hw_test.pyandtools/panel_fulltable_test.py. It refuses an answer that hastargets, and its message names/api/uiandroteiro_ui.py. An answer with neithergridnortargets, as a device older than v25 sends, still gets the v24 layout.Rigchecks once at its start, right after the web login:smap.smaptoo, because this tool's own cleanup is a step that a failure skips.docs/MANUAL.pt-BR.html,AGENTS.md,docs/AUTHORIZATION.md,docs/MANUAL*.md,docs/analysis/UI_DRIVERS.md,tools/README.mdandCLAUDE.md's script count.mainalready had but miscounted. Its "Called by CI" heading said 73 over 74 rows.Tests first
tools/test_keypad_answer.py, in CI'sgatesjob. It has four cases: an original answer, an Ângulo answer (refused), an answer older than v25, and the groups keyboard with its popup. It also checks two things in the tools' source:eb192b74).5b4d8001) and is green after. Run with serial and requests stubbed:refuse_angulo( )andkeypad_faces( )senduser del smapbefore they stop;6981fd93it fails 6 checks, all on the two Ângulo images: no,"targets":", and the"pop":[%dand"grid":[format strings still there. On this branch all 49 of its checks pass. With the two hooks' values swapped, it fails again.Images
All eleven built from zero on both sides through the pio shim. A reviewer rebuilt all eleven independently and got the same sha256 and sizes.
6981fd93.binpico2_w_releasepico_wiretft_anguloSigned, as the CHANGELOG gives them (+241 B): 1,038,385 and 829,225.
handleApiKeypadgoes from 508 to 348 B on the Pico 2 W. The two budget entries intools/flash_budget.jsonand their rows indocs/analysis/PLANO_STABLE.md§3.1 come down in this change, andcheck_flash_budget.py --tableagrees.Gates
ci_gates_local.py --rasoat the tip: 76 of 77 steps passed at5b4d8001. The fuzz step fails here only because this machine has no libclang, and the AddressSanitizer step runs only in CI.Also passed:
check_authz.py, with a--listidentical tomain's;check_readme_numbers.py;check_feature_sprawl.pyat 368;check_ui_boundary.pywith 0 findings, andtest_ui_boundary.py316 of 316;check_ui_texts.pywith 0 findings;check_ui_link.pywith 0 findings on both Ângulo ELFs.The regenerated HTML.
tools/build_manual.pyis not run by CI, and this machine's pandoc drops the table row classes. A post-step restores them:mainfirst: the generator's output plus the step equals the committed file byte for byte, sha256ce4ef77f;0acd0772.The HTML's diff is 2 hunks, only the changed text.
Review
Rig.__init__( )makes the web login's full-admin account before the Ângulo check, and the refusal'sSystemExitcame before the cleanup, so a refused run leftsmapon the device.smaponly in a step that a failure skips, and itsfinallydoes not delete it. Both are fixed in5b4d8001, test first: the check now reads every handler of the Ângulo's answer in the tool.Not run on a board
No device was contacted. The answers are proved from the images' bytes and by the host tests. The bench check needs the maintainer's word to flash the Pico 2 W:
grid, andkeypad_answer.pymust accept it.pico2_w_release, open the keypad by/api/ui's targets:targets=/api/uiand nogridorpop;/api/ui's targets must reach Settings;🤖 Generated with Claude Code
https://claude.ai/code/session_01Uy7K2mzEmmjBnpxpdYNDFu