Skip to content

fix(panel): after a boot in English the Idioma screen lists the pack in /lang again, so the language has a way back - #294

Merged
angeloINTJ merged 1 commit into
mainfrom
fix/lang-english-way-back
Oct 28, 2026
Merged

angeloINTJ merged 1 commit into
mainfrom
fix/lang-english-way-back

Conversation

@angeloINTJ

Copy link
Copy Markdown
Owner

What was broken

On every image with the touch panel, choosing English was a one-way door after the next reboot:

  • the language pack in /lang loads at boot only when the saved language is not English (AppManager_Boot.cpp);
  • the Idioma screen listed the pack's row only while a pack was loaded (_activeLangLoaded ? LANG_COUNT : 1, twice);
  • and on a product image nothing else writes the language back: the emergency CLI (SIMUT_CLI_FULL=0, every product, v2.11.1 included) has no language command, and the web has no language key.

So a unit that rebooted in English listed English alone, and only a factory reset brought Portuguese back. Found on the bench on 2026-10-24, where the way back took flashing an image with the full CLI.

What changes

  • List mode. After a boot in English, the boot reads the name and code of the pack the device would load (the alphabetically first language_*.lng) without loading @DICT. Nothing stays on the heap and no log record is written. The Idioma screen lists it.
  • Choosing the pack while it is not loaded saves the language and, only once the save returned true, shows "Reboot required" (TR_REBOOT_REQ, already in both packs). The pack loads at the next boot. The panel stays in English until then: Core 0 does not call setLanguage in that branch, so the hard-coded isPt screens do not turn Portuguese while tr( ) stays English. The live branch (English, or a pack already loaded) is today's logic. An id the screen did not list is refused on Core 0.
  • Pure decisions in src/display/LangChoice.h, plus langPackIndexUsable( ) in LangPackIndex.h, so "listable" and "loadable" are one rule. 15 native cases in test_validators.
  • Six images change: pico_w_release, pico_w_test, pico_w_test_https, pico_w_asserts, pico2_w_release and pico_wiretft. pico_w_alpha and pico_wire have no Idioma screen and stay byte-identical. pico_w_air has no display and its full CLI's language pt works; it loses 16 B because its loader compiles differently, with every differing word explained. The new code is under #if SIMUT_DISPLAY_TFT: sprawl +3, accepted by the maintainer on 2026-10-24 as a recorded deviation.

Proof (host)

  • 15 new cases: test_validators 320 of 320 (305 before), native_lan 14 of 14, 726 cases in 11 suites (711). Each case fails under a mutation of the function it pins (15 mutations).
  • The nine images from scratch, on this tree and on main:
flash used .bin RAM
pico_w_release +616 +616 0
pico_w_test +624 +624 0
pico_w_test_https +624 +624 0
pico_w_asserts +608 +608 0
pico2_w_release +656 +656 0
pico_wiretft +624 +624 0
pico_w_alpha 0 (sha256 identical) 0
pico_wire 0 (sha256 identical) 0
pico_w_air −16 same size 0
  • OTA headroom: pico_w_test is left with 139 B (763 B before), and the gate warns. pico_w_test_https's FLASH region has 3,676 B (last LOAD 0x100FE1A4, readelf -l).
  • Budgets and PLANO_STABLE §3.1 updated; check_flash_budget.py OK on all nine and --table OK. CI gates run locally in a depth-1 clone of the rebased tree: 49 of 49 steps pass, the sanitizer step skipped (CI only).
  • Reviewed by an agent reviewer: COM RESSALVAS. The important finding was a heap figure that came from the Ângulo build; it is fixed (a loaded pack keeps its @DICT: at most 6,146 B, 3,294 B with pt-BR). A latent case in applyConfigLive( ), which no route reaches today, is recorded in the commit message.

Not verified on hardware yet. The maintainer approved the bench run (2026-10-24). It shows main's pico2_w_release as the control, then this branch's, on the rig's Pico 2 W:

  • English lists the pack row;
  • APPLY shows "Reboot required" with the panel still in English;
  • after a reboot, langCode is pt-BR.

The merge waits for that run.

🤖 Generated with Claude Code

https://claude.ai/code/session_01LyLm7Q1o7kCHngE63BnoTx

…in /lang again, so the language has a way back

What was broken. The bench of 2026-10-24 found that English after a reboot
has no way back on any product image. The Idioma screen counted a language
pack only when it was loaded (`_activeLangLoaded ? LANG_COUNT : 1`, in
DisplayManager_i18n.cpp and DisplayManager_Touch.cpp), and the pack loads
only at a boot in another language, so a unit that booted in English listed
English alone. Nothing else writes the language back: the emergency CLI has
no `language`, and the web has no key for it. Two smaller defects in the same
screen: a unit saved in a pack that failed to load opened the screen on a row
that is not listed, with nothing highlighted; and Core 0 took the id the
screen sent without checking it.

What changes (the six panel images):
- display/LangChoice.h (new, plain ints): langRowsListed, langRowAtOpen,
  langChoiceValid, langAppliesAtBoot.
- langPackIndexUsable( ) in LangPackIndex.h: the one rule for a usable pack
  (no directive after @webdict, a @dict with a body). The loader and the list
  both ask it, so a pack the screen offers is one the next boot can open.
- DisplayManager::listLangFile( ), called at an English boot in
  AppManager_Boot.cpp under SIMUT_DISPLAY_TFT: the same directory pass
  (firstLangName( )) and the same front half of the load (loadLangImpl<true>),
  stopped before the malloc. It keeps @name, @code and the path in
  _activeLang: no heap, no log record, no new variable (a listed pack is told
  by the path kept there). isLangListed( ) and getListedLangName/Code( ) are
  new; isLangLoaded( ) and getActiveLangName/Code( ) keep their meaning, so
  /api/perms and the web page do not change.
- The screen draws and bounds its rows with langRowsListed and opens on
  langRowAtOpen; a static_assert ties LANG_COUNT to the header's two rows.
- EVT_APPLY_LANG: Core 0 refuses an id the screen does not list. A pack
  chosen while it is not loaded is saved, and "Reboot required" (TR_REBOOT_REQ;
  no new LangKey, TRL or log code) is shown only after saveConfiguration( )
  returned true; otherwise the panel says "Error" and the language stays as it
  was. setLanguage( ) is not called in that branch, so the screens with text of
  their own do not turn Portuguese while tr( ) stays in English. Log 441 as
  before. Every other choice runs the same statements in the same order as
  before. The pack loads at the next boot and the unit does not restart by
  itself. The permission behaviour of EVT_APPLY_LANG and EVT_APPLY_THEME, and
  the comment at AppManager_Events.cpp:328-331, are left as they are.
- The boot comment now states what a loaded pack keeps on the heap: its
  @dict, at most LANG_DICT_MAX + 2 = 6,146 B (DisplayManager_LangParser.cpp),
  3,294 B with the shipped pt-BR. The 11,488 B the bench measured on
  2026-10-24 (heap_f median in English against pt-BR) came from the Pico 2 W's
  Angulo build on feat/ui-drivers, which keeps the Angulo's own texts too; it
  is not this tree's figure.
- Manual chapter 11 and its regenerated HTML, the CHANGELOG (en, pt-BR) and
  the OTA headroom table of PLANO_STABLE 3.1.

Host tests, 2026-10-24. 15 new cases in test_validators (11 for LangChoice.h,
4 for langPackIndexUsable): 320 of 320 (305 before); native_lan 14 of 14;
ci_gates_local.py --raso on the tree rebased onto 6adf102: 49 of 49 steps
passed, 1 skipped (the sanitizer step, CI only); 726 cases in 11 suites (711 before). Each case fails under a
mutation of the function it pins (15 mutations, control 320 of 320). Dropping
the packFoundAtBoot term (the old rule) fails exactly
english_boot_with_a_pack_in_lang_lists_two_rows; dropping the tail check in
langPackIndexUsable fails exactly refuses_a_directive_after_webdict; "always
false" in langChoiceValid, langAppliesAtBoot and langPackIndexUsable fails
exactly valid_choices, a_pack_chosen_while_not_loaded_waits_for_the_boot and
accepts_a_shipped_like_pack.

Images, the nine built from scratch on this tree and on be56633
(2026-10-24); bytes:
                   flash used                  .bin                        RAM (data+bss)
pico_w_release     1,022,908 -> 1,023,524 +616   1,034,940 -> 1,035,556 +616   118,072 +0
pico_w_test        1,027,348 -> 1,027,972 +624   1,039,380 -> 1,040,004 +624   115,840 +0
pico_w_test_https  1,028,148 -> 1,028,772 +624   1,040,180 -> 1,040,804 +624   115,856 +0
pico_w_asserts     1,025,212 -> 1,025,820 +608   1,037,244 -> 1,037,852 +608   118,072 +0
pico2_w_release      999,016 ->   999,672 +656     999,040 ->   999,696 +656   113,940 +0
pico_wiretft         774,612 ->   775,236 +624     786,648 ->   787,272 +624   116,144 +0
pico_w_alpha         976,228, +0 (sha256 b5d7d079... identical to main's)      132,648 +0
pico_wire            592,300, +0 (sha256 3438b89a... identical to main's)      111,160 +0
pico_w_air         1,019,480 -> 1,019,464  -16   1,035,340, same size          122,968 +0
pico_w_air is not byte for byte main's (sha256 e5935de2... -> 4291184e...): its
loader is compiled differently. loadLangFile (18 B) and loadLangFile [clone
.part.0] (1,048 B) become loadLangImpl<false> (1,052 B), and findAndLoadLangFile
starts 16 B lower. No other sized symbol changed size; 4,120 functions are
identical once each address is replaced by the symbol it points into; the ten
that differ are findAndLoadLangFile and nine objects that hold code addresses
(six __pre_init_runtime_init_* entries, the .init_array, sleep_timeout_worker,
__global_locale), where every differing word is the address of the same symbol,
16 B lower.

Budgets. tools/flash_budget.json: budget, measured and bin of the six panel
images set to the values above, and pico_w_air's budget and measured lowered
by 16 B in the same change. check_flash_budget.py <env> build.log on each of
the nine, in the tree that built it: OK; --table: OK (9 images, 1 exempt);
test_flash_budget.py: 24 ok. OTA headroom: pico_w_test is left with 139 B
(signed .bin 1,040,245 B against the 1,040,384 B ceiling; 763 B before), which
the gate warns about (less than one flash sector); pico_w_asserts 2,291 B,
pico_w_release 4,587 B, pico2_w_release 568,831 B, pico_wiretft 252,871 B.

Outside the plan's list, each because a gate asks: tools/feature_sprawl.json
(three new `#if SIMUT_DISPLAY_TFT` sites: AppManager_Boot.cpp 4 to 5,
AppManager_Events.cpp 2 to 3, DisplayManager_LangParser.cpp 0 to 1, accepted
by the maintainer on 2026-10-24 as a recorded deviation: pico_w_air links
LangParser and i18n, and the /lang scan must stay off its wake path); the three
READMEs (check_readme_numbers: 711 to 726 cases, 305 to 320 in native, release
97.9 to 98.0 % of the slot); CHANGELOG.pt-BR.md (the twin of the English entry).

Proved on the host by the pure decisions only: list mode (loadLangImpl<true>)
and Core 0's save-then-announce order were checked by reading and by the
images. List mode keeps nothing on the heap; the directory pass makes the same
transient String per entry and File handles as findAndLoadLangFile always did.

Left as it is, recorded: WebManager::applyConfigLive( ) calls setLanguage( )
whenever a commit raises CFG_DISPLAY, which with a pack chosen but not yet
loaded would turn the hard-coded Portuguese screens on while tr( ) stays in
English. No route raises it today: commit_all has no theme or language key,
and /api/save_sys sets the theme without going through it.

Not verified on hardware: this ran on the host and in builds only. The bench
still has to show that English lists English and the pack row; that choosing
the pack and APPLY shows "Reboot required" with the panel still in English
(log 441); that after a reboot langCode is pt-BR and the panel is in Portuguese;
and the boot time and heap_f in English before and after.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LyLm7Q1o7kCHngE63BnoTx
@github-actions github-actions Bot added documentation Improvements or additions to documentation tests Testing infrastructure and test coverage display Display, TFT, touchscreen, and themes i18n Internationalization and translations tools Build tools, scripts, and developer tooling code Firmware source changes (src/, WebUI.h) labels Oct 25, 2026
@angeloINTJ

Copy link
Copy Markdown
Owner Author

Bench, 2026-10-28: the defect on main, the fix on this branch (Pico 2 W)

Rig: the Pico 2 W at 192.168.3.33, original touch UI, with a PicoHand for RESET. Three factory UF2s were flashed in turn: main's pico2_w_release (6adf102's build input, the control), this PR's (d2c366b), then the rig's usual image back. Each UF2's slot A was checked equal to its build's firmware.bin, and all partition tables are the same. A bench tool drove the panel through /api/touch and judged it from effects: the log records, /api/perms langCode and the uptime. Screens were judged from /api/screenshot.

Language pack: the bench used main's data/lang/language_pt-BR.lng (42,038 B, byte-equal to this branch's). It also checked that a pack over LANG_FILE_MAX (49,152) is neither loaded nor listed. listLangFile( ) goes through the same loadLangImpl<true> size check, so the Idioma screen never offers a pack the next boot cannot open.

Control, main:

  • From pt-BR, English was applied: APP_UI_LANG_CHANGED (441) once.
  • After a RESET, langCode was "" and there was no I18N_PACK_LOADED.
  • The defect: "Settings > Language" draws English alone. A tap where the second row would be changes nothing.

This PR, starting from that English boot: 56 checks, 0 failed.

  • The Idioma screen lists two rows: English and "Portugues pt-BR".
  • Choosing the pack:
    • APPLY shows "Reboot required" and logs 441 once.
    • langCode stays "" and the device does not reboot (no live switch).
  • After a RESET:
    • the board was back in 26 s;
    • langCode was pt-BR / Portugues, with one I18N_PACK_LOADED (620) and no I18N_PACK_FAILED;
    • the panel was in Portuguese.
  • Live switching while the pack is loaded: English and the pack each apply at once (441 each, no message, no reboot).

Guard:

  • Config and alarms were equal before and after each phase and across the whole bench.
  • There was one 441 per APPLY.
  • No alarm, PIN lockout or refused-pack record appeared.
  • The throwaway panel account was deleted, and the rig's pack and image were restored.

Not covered by the bench: the RP2040 panel images (no such board on the rig), and es-ES (only pt-BR was installed).

@angeloINTJ
angeloINTJ merged commit 567140f into main Oct 28, 2026
13 checks passed
angeloINTJ pushed a commit that referenced this pull request Oct 28, 2026
Main's #289-#295, each hunk where this branch moved its code (plan step 1). First parent: 5f2cdaf, the
docs step's tip; its eleven images are sha256-equal to 3d433d6's, so the C0 references (stages/sync6_c0)
hold. What lands from main: #294 (an English boot lists the pack found in /lang; choosing it saves the
language and waits for a boot: display/LangChoice.h, listLangFile( ), loadLangImpl<kListOnly>), #295 (the
panel half of tools/check_authz.py, and the guard arm that asks for PERM_SYS_CONFIG before the theme and
the language), the flash-budget gate that reads the .bin, test_air_web_session.py, the savings workflow,
the manual and the changelogs.

Conflicts (12), as resolved
- src/DisplayManager_Touch.cpp: the branch's whole file (stage D moved the body to src/ui/original/
  UiTouchChain.h; main's side would bring ~1,300 moved lines back). Main's one hunk, activeSlots =
  langRowsListed( isLangLoaded( ), isLangListed( ) ) and its comment, is applied at UiTouchChain.h:438-441.
  Against the docs tip the file differs by its :25 include comment only.
- src/DisplayManager_i18n.cpp: the branch's side (drawSettingsLang left this file in S10a). Main's draw
  hunk (getListedLangName/Code( ), langRowsListed) is applied to src/DisplayManager_i18nUi.cpp:19-30, the
  unit that holds the original's draw. Main's clean hunks (static_assert LANG_COUNT == LANG_CHOICE_ROWS,
  showSettingsLang's langRowAtOpen) stay in the core; the header now says where the draw lives.
- test/test_validators/test_main.cpp: both includes (SettingsSteps.h, LangChoice.h); main's cases clean.
- tools/check_authz.py: main's root-based structure (routes_files, http_half, panel_half of root; main(argv);
  exit 2 on CannotRead) plus the branch's UI-driver audit as driver_files(root) and driver_half(root);
  DRIVER_EXTS kept; http_half keeps main's (rows, failures). The HTTP "clean (N routes ...)" line is
  printed once, then the drivers line, then the panel line; --list: routes, totals, drivers, panel table;
  rc 1 on a finding in any half.
- tools/test_check_authz.py (add/add), D2: main's file keeps the name (25 cases; its check (h) now counts
  the routes before the totals line, because --list prints the driver's route after them). The branch's
  becomes tools/test_check_authz_drivers.py (39 checks + 11 mutants), adapted only where the merged gate
  forces it: make_base( ) copies src/SystemDefs_Records.h, make_tree( ) and gate_exts( ) copy
  tools/check_net_boundary.py (the panel half reads both, or every run exits 2), findings( ) skips the
  "  - panel" lines, and mutant m7 is re-targeted (its old text, "failures += [...]", is gone; it now
  turns the returned findings into []).
- .github/workflows/build.yml: every step of both sides. "Authorization gate tests (UI drivers)" runs
  test_check_authz_drivers.py, "Test the authorization gate" runs test_check_authz.py, and main's "Bench
  web client session tests" lands clean.
- README.md, README.pt-BR.md, README.es-ES.md: the branch's sentences with the numbers
  check_readme_numbers.py measures: 765 cases in 11 suites (750 + main's 15), native 359 (344 + 15).
- docs/analysis/PLANO_STABLE.md section 3.1 and tools/flash_budget.json: from the eleven builds
  (tools_s10c/budget_set.py; pico_w_test_https by hand, its entry has no "bin"); main's measured_on and
  measured_at_commit sentences are kept.
- tools/README.md: both sides' rows; test_check_authz.py is main's row, test_check_authz_drivers.py the
  branch's, renamed. The count goes 199 -> 201 by the README's own definition (its table rows:
  grep -c '^| `' tools/README.md); CLAUDE.md quotes it and follows.

D1, change of plan: the deletion of langSlotCount moves to step 3. Here the original's touch takes main's
langRowsListed( isLangLoaded( ), isLangListed( ) ), as main's Touch.cpp hunk does; the Angulo's touch
(ui/angulo/DisplayManager_UiA_Menus.cpp:387) keeps langSlotCount( _activeLangLoaded, LANG_COUNT ) untouched,
so that step 3's host test T10 can still fail before its fix. langSlotCount (display/SettingsSteps.h:37)
and its test in test_validators stay until then.

D3, the sprawl: tools/feature_sprawl.json merged clean but is not trusted as text; check_feature_sprawl.py
gives 368 = 365 + 3. That is the maintainer's decision of 2026-10-24 (~22h40: #294's +3 accepted as a
recorded deviation) arriving on the branch, not a new one.

Merged clean, wrong on this branch, corrected (the nine files re-read)
- src/AppManager_Events.cpp: the guard arm's comment named DisplayManager_Touch.cpp as the sender of the
  theme and language events; the senders are actApplyTheme( ) and actApplyLang( ) in
  DisplayManager_Actions.h, which both UIs call.
- src/AppManager_Boot.cpp, src/display/LangChoice.h: the heap sentence was main's pack alone. Now: @dict
  3,292 B (3,294 B of heap) by check_lang_packs.py, and on the Angulo images also the @angulo table, 8,416
  B in RAM of a 9,536 B section (host_telas.py); measured 2026-10-28 on this branch's pt-BR.
- docs/AUTHORIZATION.md :18 names test_check_authz_drivers.py (the panel rules keep test_check_authz.py);
  MODELO_DE_RECURSOS.md R-R17 cites LangParser.cpp:27, :305-314 and check_lang_packs.py:341-344, :631-643
  (its struck-through history row is left as written); CLAUDE.md count; the .pre-commit-config.yaml comment.
- tools/tft_ui/test_textos.py: its check that the language hook is called right after the block that loads
  the pack failed on main's "#if SIMUT_DISPLAY_TFT else { listLangFile( ); } #endif" between the two; it
  now accepts exactly that arm (105 checks, 0 failed). The T5 loop was silent at C0 and failed here.
- Kept as is, after checking that every "return false" of loadLangImpl comes before the path is written
  (:157, :267) and unloadLang( ) (:118) clears the rest, the @angulo offsets included: src/DisplayManager.h,
  DisplayManager_LangParser.cpp, LangPackIndex.h; and the main-only files.

Proofs (host only; each number from its tool, 2026-10-28; records in stages/sync6_c1)
T1 The eleven images from scratch (pio shim first in PATH, no PLATFORMIO_*, under flock; check_flash_budget.py
   right after each build), then once more on the staged tree this commit records: sha256 equal, 11 of 11. C0 -> C1, and main's
   growth for the family (Angulo images: wiretft's +688, pico2_w_release's +680):
   pico_w_release       1,035,116 -> 1,035,796  +680 B (main +680)  dd741c9f32f5
   pico_w_test          1,039,556 -> 1,040,244  +688 B (main +688)  aec3fb47774f
   pico_w_test_https    1,040,356 -> 1,041,052  +696 B (main +696)  50d4c14dad14
   pico_w_asserts       1,037,412 -> 1,038,092  +680 B (main +680)  34db1605f4c1
   pico_w_alpha           990,380 ->   990,380    +0 B (main +0)  sha256 equal to C0 and to main's pair  ae687ec4b692
   pico_w_air           1,035,340 -> 1,035,340    +0 B (main +0)  f0bdf19bb78d
   pico2_w_release        999,096 ->   999,768  +672 B (main +680)  6104b438caf5
   pico_wire              604,296 ->   604,296    +0 B (main +0)  sha256 equal to C0 and to main's pair  577216451343
   pico_wiretft           786,808 ->   787,488  +680 B (main +688)  be6e2c44762d
   pico_wiretft_angulo    826,736 ->   827,336  +600 B (main +688)  2c9775fdf832
   pico2_w_angulo       1,036,016 -> 1,036,600  +584 B (main +680)  89d1f444d4aa
   The functions that changed are main's pair for the family, name for name (elf_funcoes + reloc_only +
   objdump decode, stages/sync6_c1/cmp_c0, t1_vs_main_pair.txt): setup, core0Yield, findAndLoadLangFile
   (147 -> 86 instructions; 138 -> 77 on pico2), getActiveHelpText, webDictValue, handleTouch,
   showSettingsLang, drawSettingsLang; nine added (isLangLoaded, isLangListed, getListedLangName,
   getListedLangCode, listLangFile, loadLangImpl<false>, loadLangImpl<true>, LangPackScanner::feed,
   firstLangName); loadLangFile and its .part clone gone. pico_w_air: findAndLoadLangFile alone. The two
   Angulo images: main's pair minus handleTouch and drawSettingsLang (not linked) and getListedLangName/Code
   (only drawSettingsLang calls them). Link artifacts, decoded: _fini padding; SensorManager::SensorManager()
   (masked listing equal, a literal-pool word follows the growth); __binary_info_header_end (+672 and +584,
   the two pico2 images' growth). Two instruction counts differ from main's, decoded: handleTouch on
   pico2_w_release +6 against +7 (the activeSlots hunk is the same; the rest is a nop and literal-pool
   words), and setup on pico2_w_angulo +3 against the release's +4 (a nop). pico_w_asserts: its decoded
   changes equal pico_w_release's line for line, so no __LINE__ constant moved.
T3 V-PIX pt-BR, six groups: 951 of 951 frames equal to the records (948 + D7's 3); the control (one frame
   changed, one missing, one extra) bites.
T4 The eleven native suites pass, 765 cases = 750 + main's 15 (native 359, history_v5 62, cli 34, lan 14,
   logpolicy 59, otasig 83, alarmqueue 54, network 51, wired 19, air 17, sensors 13).
T5 tools/tft_ui/test_*.py: 7 files, silent.
T6 check_authz.py rc 0: "62 routes — 52 gated, 10 public-by-design" (once), "UI drivers, src/ui/**: 1 route,
   1 gated", "32 events — 14 gated, 14 ungated, 4 registered deviations", 4 warnings; --list 103 lines.
T7/T8 test_check_authz.py 25 of 25; test_check_authz_drivers.py "OK: 39 checks and 11 mutants".
T11 check_readme_numbers.py OK (765 cases in 11 suites, release 98.0 % of the slot, 170 event codes, 62
   routes, all three READMEs); check_feature_sprawl.py 368; check_lang_packs.py OK (pt-BR @dict 3292 B,
   @angulo 376 texts, 9536 B); test_flash_budget.py 24 ok; check_flash_budget.py --table OK (11 images, 1
   exempt); test_lang_gate.py 52; test_air_web_session.py 4 ok.
T12 ci_gates_local.py --raso: 65 of 66 steps passed, 1 skipped (CI only). The one failure is the fuzz step
   (clang++ cannot load libclang-cpp.so.18.1), as at C0 (63 of 64). New steps: "Authorization gate tests
   (UI drivers)" (renamed), "Test the authorization gate", "Bench web client session tests".
Also: git diff --check clean; no conflict markers; drawSettingsLang is defined in the same three files as
at C0 (DisplayManager_Alpha.cpp, DisplayManager_i18nUi.cpp, DisplayManager_None.cpp).

Budgets (flash_budget.json used = budget = measured, and "bin"; PLANO_STABLE 3.1 headroom), old -> new
   pico_w_release       1,023,084 -> 1,023,764 (+680), bin 1,035,116 -> 1,035,796, headroom 5,027 -> 4,347
   pico_w_test          1,027,524 -> 1,028,212 (+688), bin 1,039,556 -> 1,040,244, headroom 587 -> -101
   pico_w_test_https    1,028,324 -> 1,029,020 (+696), no bin recorded (ota_exempt)
   pico_w_asserts       1,025,380 -> 1,026,060 (+680), bin 1,037,412 -> 1,038,092, headroom 2,731 -> 2,051
   pico_w_alpha, pico_wire: unchanged (976,228 / 990,380 and 592,260 / 604,296)
   pico_w_air           1,019,480 -> 1,019,464 (-16), bin 1,035,340 unchanged, headroom 4,803
   pico2_w_release        999,072 ->   999,744 (+672), bin 999,096 -> 999,768, headroom 569,431 -> 568,759
   pico_wiretft           774,772 ->   775,452 (+680), bin 786,808 -> 787,488, headroom 253,335 -> 252,655
   pico_wiretft_angulo    814,700 ->   815,300 (+600), bin 826,736 -> 827,336, headroom 213,407 -> 212,807
   pico2_w_angulo       1,035,992 -> 1,036,576 (+584), bin 1,036,016 -> 1,036,600, headroom 532,511 -> 531,927
No image grows by more than main's growth for its family (D4).

NEEDS A DECISION: pico_w_test is 101 B over the OTA ceiling. Its signed size is 1,040,485 B against a
ceiling of 1,040,384 B, so "check_flash_budget.py pico_w_test <log>" fails (rc 1) and the 3.1 row records
-101. The growth is main's own (+688 B), but this branch's test image had 587 B of headroom at C0 (main's:
763 B before #294, 75 B after), so main's growth does not fit. pico_w_asserts keeps 2,051 B (the gate
warns: under one flash sector). Not fixed here.

Not verified on hardware.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uy7K2mzEmmjBnpxpdYNDFu
angeloINTJ pushed a commit that referenced this pull request Oct 28, 2026
…d marks the saved language (#294 on the Angulo; T10, D1; S10c, sync 6, C3)

#294 made the Idioma screen list a pack it has only FOUND in /lang when the unit booted in English (isLangListed( )). The original's draw and touch took it
in C1; the Angulo's two sites in ui/angulo/DisplayManager_UiA_Menus.cpp kept reading _activeLangLoaded, so an English boot listed English alone and no tap
could reach the pack. No frame shows it (the samples build the screen state directly), so the proof is T10, a host test that runs the firmware's own text.

The fix (plan step 3, D1 and D5)
- Draw: `if ( isLangListed( ) ) nomeDoPacote( getListedLangName( ), getListedLangCode( ), ... )`. Touch: `i < langRowsListed( isLangLoaded( ), isLangListed( ) )`, the
  expression the original and Core 0 use. Mark (D5, the maintainer's answer of 2026-10-28, "A escolhida, pt-BR"): the saved language's row, from _previewLangIdx,
  which the core's showSettingsLang( ) sets with langRowAtOpen( ) when Core 0 opens the screen (AppManager_Events.cpp, MENU_LANG; T10 checks that call in the
  source), not from _currentLangIdx: an English boot that saved the pack still runs in English, so the old mark stayed on English after the choice.
- Comments: UiA_Menus.cpp :43 (the include is now LangChoice.h: nothing else of SettingsSteps.h is used there), nomeDoPacote's, the draw's and the touch's;
  telas/MenusConfig.h:117 and, not in the plan's list but the same stale claim, MenusConfig.cpp:251 ("loaded" -> "listed"). tools/tft_ui/ESPEC_UI_A.md:191 is not
  edited: neither docs/README.md nor the file marks it Living.
- D1: langSlotCount( ) and its test_validators case leave src/display/SettingsSteps.h (volumeStep, offsetStep, limitStepMag and DISPLAY_OFFSET_LIMIT stay); the
  header's brief and the include comment at DisplayManager_Touch.cpp:25 stop naming it. The READMEs' counts follow: 764 cases in 11 suites, native 358.

T10: tools/tft_ui/test_idioma_pacote.py and host/test_idioma_pacote_main.cpp (written before the fix, stages/sync6_c3/TESTS_FIRST.txt)
It cuts nomeDoPacote( ), the braced draw case, the statements before the touch switch, and the UNBRACED touch case (its own cutter: it refuses a text without the
site, with it twice, with a smaller block, without its break, or with the case going on) out of the live file at every run, plus the statement of the core's
showSettingsLang( ) that sets _previewLangIdx, and runs them against a stand-in DisplayManager, the real LangChoice.h and the kit's own MenusConfig.cpp
(alvosIdioma( ) says how many rows the glass lists). Wired into build.yml; tools/README.md has its row (202 scripts, CLAUDE.md follows).
- On C2's tree (the reproduction, t10_red_on_c2.final.txt): case 1, an English boot with a pack listed, 9 of 36 checks fail (the glass lists 1 row, the pack's row
  reads "", a tap on row 1 applies nothing); case 4, the saved language reopened, 15 of 24 fail (the mark is on English, expected the pack); cases 2, 3 and 5
  (no pack, a pack loaded and in use, Back) pass: 134 checks, 24 failed.
- After the fix (t10_green.txt): 134 checks, 0 failed, 12 of 12 checks, and the three mutants of the fixed text (the draw, the touch, the mark turned back to
  _activeLangLoaded / _currentLangIdx) each fail the program.

Proofs (host only, 2026-10-28; records in stages/sync6_c3)
- The eleven images from scratch (shim first in PATH, no PLATFORMIO_*, under flock): the nine products sha256-equal to C1's (11 images: 9 of 11 equal, the two
  Angulo images changed). pico_wiretft_angulo 827,336 -> 827,392 B (+56, e82f4f8ed84d), pico2_w_angulo 1,036,600 -> 1,036,656 B (+56, c5b80985fe48). Functions
  changed (elf_funcoes + reloc_only, cmp_c1/summary.txt): uaMontarMenus (859 -> 857 instructions on RP2040, 533 -> 531 on pico2) and uaToqueMenus (362 -> 365,
  291 -> 294), the two holding the sites; getListedLangName( ) and getListedLangCode( ) newly linked (only the Angulo's draw calls them there); link artifacts:
  _fini padding (RP2040) and __binary_info_header_end (pico2, the growth). The hunk-level objdump decode files are in cmp_c1/decode_*.txt; I did not read them
  line by line in this run.
- Budgets (budget_set.py): pico_wiretft_angulo 815,300 -> 815,356, bin 827,336 -> 827,392, headroom 212,807 -> 212,751; pico2_w_angulo 1,036,576 -> 1,036,632,
  bin 1,036,600 -> 1,036,656, headroom 531,927 -> 531,871. check_flash_budget.py rc 0 for both after the edit (rc 1 before it, as it must), --table OK (11
  images, 1 exempt), test_flash_budget.py 24 ok. The other nine rows are untouched. pico_w_test stays 101 B over the OTA ceiling (C1's note; not touched here).
- V-PIX pt-BR 951 of 951 frames equal to the records, control bites. The eleven native suites pass: 764 cases (765 - 1). tools/tft_ui/test_*.py: 8 files, silent.
- check_authz.py rc 0 with C1's counts (62 routes - 52 gated, 10 public-by-design; drivers 1 route, 1 gated; 32 events - 14 gated, 14 ungated, 4 registered
  deviations; 4 warnings); test_check_authz.py 28 of 28; test_check_authz_drivers.py 39 checks and 11 mutants. check_feature_sprawl.py 368, check_lang_packs.py OK,
  test_lang_gate.py 52, test_air_web_session.py 4 ok.
- ci_gates_local.py --raso ran on this tree BEFORE the READMEs' numbers were fixed: its summary reads "65 of 67 steps passed; 1 skipped (own env, CI only)" and it
  prints the failure blocks of two steps: the READMEs' numbers (765/359, D1's -1; fixed in this commit, check_readme_numbers.py rerun alone: rc 0) and the fuzz
  step (clang++ cannot load libclang-cpp.so.18.1, as at C0 and C1). The whole of --raso was not rerun after the fix.

Not verified on hardware. The plan's step 5 (an English boot with /lang pt-BR on the Angulo image: the list, the choice, "Reboot required", the pt panel after the
reboot) is the bench, not done here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uy7K2mzEmmjBnpxpdYNDFu
angeloINTJ pushed a commit that referenced this pull request Oct 28, 2026
…0c, sync 6, C5)

The review of the merge C1 3ce6bd3 (REV_SYNC6_C1_RESULT.md, COM RESSALVAS, no blocker) left three minor things for a later commit. Comments keep their files' line
counts, and the eleven images stay sha256-equal to C4's.

1. tools/flash_budget.json, measured_at_commit: the entry for this merge. sync6/ui-drivers-main, the merge of feat/ui-drivers 5f2cdaf and main 9f4da77 (tree 3e55b57),
   the eleven images from scratch on it and again on the staged tree, the two Angulo images rebuilt after their Idioma screen took #294, and the toolchain: PlatformIO
   6.1.19 through the shim of stages/s10c_d6_0 (a Python 3.14 venv over the pipx venv's sources, the apt python3-zopfli 0.4.1 where the pipx one had 0.4.3, whose pico_wire
   gave the base's sha256 before any image was trusted). The last entry was main's d2c366b and no longer described the file's numbers. json.load reads the file and
   every other key is equal to what it was.
2. docs/analysis/PLANO_STABLE.md section 3.1: one bullet under the table (the table between its markers is not touched). pico_w_test is over the OTA ceiling on this
   branch, the -101 B row: check_flash_budget.py pico_w_test exits 1 ("firmware.bin + 241 B of signature is 1040485 B, the OTA ceiling is 1040384 B - over by 101 B",
   measured again in this run), so the required CI job of the image would be red; the maintainer chose on 2026-10-28 to open room in it by serving its licence page from
   LittleFS on main (9eb4252 on chore/test-image-license-fs), which the next sync brings, and the row changes then. check_flash_budget.py --table: OK (11 images, 1 exempt).
3. Comments. src/AppManager_Boot.cpp and src/display/LangChoice.h name the host_telas.py command in full, python3 tools/tft_ui/host_telas.py --idioma pt menus (run again
   for this commit: "@angulo loaded, 376 texts, 8416 B in RAM, section of 9536 B"); LangChoice.h's paragraph is reflowed inside its twelve lines (the sentence C1 inserted had
   left an orphan line). src/ui/angulo/DisplayManager_UiA_Textos.cpp :13-15 replaces B2a's projection (10,413 B, 408 texts, 9,197 B) with the shipped pt-BR's measured
   figures and their date: a section of 9,536 B and 376 texts (check_lang_packs.py), a table of 8,416 B in RAM, so a peak of 9,536 + 8,416 = 17,952 B. src/ui/angulo/Textos.h
   :50-51 still carries the same projection in the sizing argument of UA_TEXTOS_MAX; it is not touched here.

Proof (host only, 2026-10-28; records in stages/sync6_c5)
- The eleven images built from scratch (shim first in PATH, no PLATFORMIO_*, under flock): 11 of 11 equal to C4's, sha256 and size (imgs/compare.txt).
- git diff --numstat of the three src files has equal added and deleted columns (4/4, 8/8, 2/2) and each keeps its line count (1559, 63 and 62, equal to HEAD's).
- git diff --check clean; flash_budget.json parses; check_flash_budget.py --table, test_flash_budget.py (24 ok), check_readme_numbers.py and check_angulo.py rc 0;
  tools/tft_ui/test_textos.py, which cuts a function out of DisplayManager_UiA_Textos.cpp, 105 checks and 0 failed.
- check_flash_budget.py exits 0 for the other ten images and 1 for pico_w_test, the one it did before this commit (C1's note): this commit does not change that.

Not verified on hardware.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uy7K2mzEmmjBnpxpdYNDFu
angeloINTJ pushed a commit that referenced this pull request Oct 28, 2026
… and a host test's stand-ins (S10c, sync 6, C7)

The review of C2-C5 was COM RESSALVAS with three minor findings; none of them touches a byte of any image.

1. test/test_validators/test_main.cpp :5559-5560. C3 deleted test_lang_slot_count_is_english_alone_until_a_pack_is_loaded and its RUN_TEST and left its comment ("English alone until a pack
   is loaded ... a constant 1, a constant 2 ... each fail") above the next test's. The two lines and the blank after them are gone (numstat: 0 added, 3 deleted).
2. docs/analysis/UI_DRIVERS.md :807 (Living). The C8 record of 2.11 still said src/display/SettingsSteps.h holds langSlotCount( packLoaded, langCount ). A dated note at the end of that bullet
   says C3 (459d02e) removed it (decision D1: one rows helper, main's langRowsListed( ) in src/display/LangChoice.h) and that the lines naming it (that one, the C8 line, correction 3 and "Not done")
   are S8a as it stood; no figure of the record changes.
3. tools/tft_ui/host/test_idioma_pacote_main.cpp :43-44. The stand-ins of getListedLangName( ) and getListedLangCode( ) returned _activeLang.name and .code bare, where the real accessors
   (src/DisplayManager_LangParser.cpp :430-431) answer "" unless isLangListed( ). Every case sets the name and the code only when the pack is listed, so the difference showed in no check, and no text
   control tied the stand-ins to the real ones (only showSettingsLang( ) is tied). The stand-ins now carry the guard, and test_idioma_pacote.py compares the body of each accessor as text between the
   firmware's file and the stand-in (acessores_amarrados( ): comments out, whitespace apart, each found exactly once on both sides). The comparison is bitten on every run by eight controls: the
   firmware's name accessor without its guard, its code accessor without it, a guard by _activeLangLoaded (the shape before #294), the code accessor answering the name, a fallback that is not "",
   an accessor gone (renamed), and the stand-in's two accessors without the guard (their text before this commit); each must be reported with the reason of its kind.

Proof (host only, 2026-10-28; records in stages/sync6_c7)
- git diff --stat HEAD~1 -- src is empty: no file of src/ changes, so the eleven images cannot move. No build was run.
- Finding 1: pio test -e native (shim first in PATH, under flock) prints "358 test cases: 358 succeeded", exit 0, on the file as committed (its sha256 was recorded before the run, and the RUN_TEST
  lines the run prints are the edited file's); check_readme_numbers.py: 764 cases in 11 suites, exit 0.
- Finding 3, before: the script was 12 of 12 on the tree of C6, blind to the difference. After: 14 of 14 (the tie and its control are the two new), exit 0, and the five cases and the host program
  keep their counts (36, 24, 48, 24 and 2 checks; 134, 0 failed).
- Reproduction: the tie run against the stand-ins of C6 reports both accessors (the stand-in's body {return_activeLang.name;} against the firmware's guarded one); against this commit's, none
  (tie_before.txt).
- Mutant of the real text in a copy: a copy of the tree (src/ and the script copied, the rest of tools/tft_ui linked) is 14 of 14, exit 0; with `isLangListed( ) ? _activeLang.name : ""` replaced by
  `_activeLang.name` in the copy's DisplayManager_LangParser.cpp it exits 1, 2 of 14 failed: the tie, naming getListedLangName( ) and both bodies, and the control, whose premise text left the copy
  (mutant_copy.txt). The live tree was not touched.
- check_angulo.py, check_readme_numbers.py, check_ui_boundary.py (it reads UI_DRIVERS.md) and test_ui_boundary.py exit 0; git diff --check is clean.

Not verified on hardware.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uy7K2mzEmmjBnpxpdYNDFu
angeloINTJ pushed a commit that referenced this pull request Oct 28, 2026
docs/analysis/UI_DRIVERS_funcoes.tsv no longer matched the code at ccc46e8:
tsv_check.py read 388 rows and 36 findings (28 in DisplayManager_LangParser.cpp,
22 of them waiting for the sync since B4b-0 and 6 definitions that #294 added;
8 one-line offsets from main's hunks). It reads 394 rows and 0 findings now
(2026-10-28, measured on this tree). No file under src/ changes, so no image can
move; nothing was built.

Rows by unit (394 = 388 + 6 added, none removed; 40 lines of the table change,
46 insertions and 40 deletions):
- DisplayManager_LangParser.cpp, 22 -> 28: 22 moved to the lines the code has
  (loadLangFile among them, see below) and 6 added.
- DisplayManager_i18n.cpp 5 moved (four by one line, showSettingsLang 167-176 ->
  171-182); DisplayManager_i18nUi.cpp 1 (drawSettingsLang 12-96 -> 12-97);
  DisplayManager.h 1 (CLOCK_COL_X, CLOCK_COL_W 268-269 -> 269-270);
  display/SettingsMenu.h 1 (menuLabelNoNumber 156-161 -> 158-163).
- ui/original/UiTouchChain.h, 10 per-mode rows: #294 replaced three lines of the
  Idioma branch by four (438-441), so MODE_SETTINGS_LANG now ends at 475 (was
  474) and the nine rows from MODE_SETTINGS_PASSWORD to MODE_OTA_UPDATE move by
  one line; MODE_SETTINGS_STATUS, the only mixed row there whose ranges are not
  192e1ad's (no STALE mark), shifts its two range lists with it. tsv_check.py
  passed all ten at ccc46e8: it holds a per-mode row's start to a window of
  six lines (a start one line early passes, one line late is refused) and does
  not read its end.

Added, all class core like the unit's other 22 rows (none draws or reads the UI
mode):
- loadLangImpl 117-274: the body the loadLangFile row described (103-243 at
  5f2cdaf), now a template on kListOnly; it takes that row's class and sentence.
- firstLangName 282-302: file-static, called only by findAndLoadLangFile and
  listLangFile.
- listLangFile 420-426: TFT builds only; called by AppManager_Boot.cpp, no UI.
- isLangListed 429, getListedLangName 430, getListedLangCode 431: TFT builds
  only; read by both UIs' Idioma screens and by core code (showSettingsLang,
  AppManager_Events.cpp), like isLangLoaded and getActiveLangName.

Two things that differ from what was assumed. loadLangFile was not removed: it
is a one-line wrapper at 276 (loadLangImpl<false>), so its row moves there and
its note says so. LangPackScanner::feed gets no row: it lives in
src/LangPackIndex.h, which is not a unit of the table (a row for it would be a
"no such definition" finding).

Method. tsv_check.py --refresh against 5f2cdaf was tried and not used: besides
the lines tsv_check.py flags it moves MODE_SETTINGS_LANG to 437-468 (the code has
475), appends the S10b sentence to handleTouch's note a second time, leaves 17
per-mode rows UNRESOLVED (exit 1) and does not keep the table's row order. The
change is a script instead (apply_tsv_sync6.py, scratch): the lines come from
the checker's own locate( ), the fragment's rows from the line map of
`git diff -U0 5f2cdaf HEAD`, and every change is one line of its log (40 rows
moved, three field edits, 6 rows added).

Proof (2026-10-28, host only):
- tsv_check.py: 388 rows, 36 findings -> 394 rows, 0 findings.
- Controls on scratch copies of the new table, exactly one finding each:
  listLangFile's line +1; menuLabelNoNumber's line +1; DisplayManager::tr back
  at 148-164; loadLangFile back at 103-243; getListedLangCode's row dropped.
- permode_check.py (scratch; reads the head line and the closing brace of each
  per-mode block, not the checker): 5f2cdaf's fragment with the old table, 0 of
  19 refused; ccc46e8's with the old table, exactly the ten; with the new
  table, 0. A start one line early (the old table's error) is not seen by
  tsv_check.py and is refused by permode_check.py.
- tools/check_ui_boundary.py (0 findings), tools/test_ui_boundary.py (311 of 311),
  tools/check_angulo.py and tools/check_ui_link.py --selftest (85 cases, 0
  failed): exit 0, same as at ccc46e8. bytes_original.py's reader loads 394 rows.
- The one sentence of UI_DRIVERS.md that counted this (section 2.18, "Left for
  later" (3): "Only those 22 rows wait for the sync with main") now says what the
  sync did.

Not done: tsv_check.py --selftest still raises StopIteration on these trees (as
recorded in UI_DRIVERS.md); the checker is a scratch tool and is not changed here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uy7K2mzEmmjBnpxpdYNDFu
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

code Firmware source changes (src/, WebUI.h) display Display, TFT, touchscreen, and themes documentation Improvements or additions to documentation i18n Internationalization and translations tests Testing infrastructure and test coverage tools Build tools, scripts, and developer tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant