Repository navigation
fix(panel): after a boot in English the Idioma screen lists the pack in /lang again, so the language has a way back - #294
Conversation
…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
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 Language pack: the bench used main's Control, main:
This PR, starting from that English boot: 56 checks, 0 failed.
Guard:
Not covered by the bench: the RP2040 panel images (no such board on the rig), and es-ES (only pt-BR was installed). |
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
…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
…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
… 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
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
What was broken
On every image with the touch panel, choosing English was a one-way door after the next reboot:
/langloads at boot only when the saved language is not English (AppManager_Boot.cpp);_activeLangLoaded ? LANG_COUNT : 1, twice);SIMUT_CLI_FULL=0, every product, v2.11.1 included) has nolanguagecommand, 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
language_*.lng) without loading@DICT. Nothing stays on the heap and no log record is written. The Idioma screen lists it.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 callsetLanguagein that branch, so the hard-codedisPtscreens do not turn Portuguese whiletr( )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.src/display/LangChoice.h, pluslangPackIndexUsable( )inLangPackIndex.h, so "listable" and "loadable" are one rule. 15 native cases intest_validators.language ptworks; 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)
test_validators320 of 320 (305 before),native_lan14 of 14, 726 cases in 11 suites (711). Each case fails under a mutation of the function it pins (15 mutations)..binreadelf -l).check_flash_budget.pyOK on all nine and--tableOK. CI gates run locally in a depth-1 clone of the rebased tree: 49 of 49 steps pass, the sanitizer step skipped (CI only).@DICT: at most 6,146 B, 3,294 B with pt-BR). A latent case inapplyConfigLive( ), 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:
langCodeis pt-BR.The merge waits for that run.
🤖 Generated with Claude Code
https://claude.ai/code/session_01LyLm7Q1o7kCHngE63BnoTx