diff --git a/README.es-ES.md b/README.es-ES.md index cbcbae63..2c10e0b5 100644 --- a/README.es-ES.md +++ b/README.es-ES.md @@ -50,7 +50,7 @@ Los tres comparten el mismo núcleo: | **Release actual** | **v2.11.1** (12/10/2026); el [changelog](CHANGELOG.md) dice qué cambió cada versión. SIMUT salió de beta con la v2.7.0, sobre mediciones: un soak de 8,18 h sin ningún reinicio y 6 de 6 actualizaciones por el aire sin perder nada. | | **Imágenes publicadas** | Cuatro imágenes, cada una en `.uf2` y `.bin`. Para la Pico W: `release` (panel táctil TFT), `alpha` (LCD 16×2 con consola Bluetooth) y `air` (registrador a batería sin pantalla). Para la Pico 2 W (RP2350): `releasetwo`, la release, cuyo `.uf2` lleva su tabla de particiones y cuyo `.bin` arranca a prueba. Junto a ellas van los packs de idioma pt-BR y es-ES y un manifiesto de OTA. Una imagen con otro conjunto de funciones sale del [configurador de build](https://angelointj.github.io/simut/configurador/), y el CI la compila desde `main`, también para la Pico 2 W. | | **Madurez** | | -| **Pruebas** | Cada pull request ejecuta 701 casos de test en el host en 11 suites, 60 s de fuzzing y análisis estático, y compila las nueve imágenes de firmware con la caché fría. El comportamiento en hardware real se verifica en un banco — ver [Verificación en hardware](docs/VERIFICATION.es-ES.md). | +| **Pruebas** | Cada pull request ejecuta 703 casos de test en el host en 11 suites, 60 s de fuzzing y análisis estático, y compila las nueve imágenes de firmware con la caché fría. El comportamiento en hardware real se verifica en un banco — ver [Verificación en hardware](docs/VERIFICATION.es-ES.md). | **Limitaciones conocidas.** Cada una está documentada donde aplica. - **Actualización.** La actualización por el aire reformatea el sistema de archivos: @@ -303,7 +303,7 @@ El dispositivo expone una API REST en `http:///api/`: ### Tests en el host ```bash -pio test -e native # validadores, cursor de telemetría, etiquetas, parsers, paquetes de idioma, las pantallas Licencia y de actualización, el menú de Ajustes, la verificación de contraseña, la sesión web (300 casos) +pio test -e native # validadores, cursor de telemetría, etiquetas, parsers, paquetes de idioma, las pantallas Licencia y de actualización, el menú de Ajustes, la verificación de contraseña, la sesión web (302 casos) pio test -e native_history_v5 # códec del histórico V5 (62) pio test -e native_cli # parser de la CLI (34) pio test -e native_lan # parser de la CLI, ayuda del paquete, menú de Ajustes y códigos de log en una build cableada (14) diff --git a/README.md b/README.md index 5b80488a..1f760508 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ They share one core: | **Current release** | **v2.11.1** (2026-10-12); the [changelog](CHANGELOG.md) says what each version changed. SIMUT left beta with v2.7.0, on measurements: an 8.18 h soak with 0 reboots, and 6 of 6 over-the-air updates with nothing lost. | | **Published images** | Four images, each as `.uf2` and `.bin`. For the Pico W: `release` (TFT touch panel), `alpha` (16×2 LCD with a Bluetooth console) and `air` (headless battery logger). For the Pico 2 W (RP2350): `releasetwo`, the release, whose `.uf2` carries its partition table and whose `.bin` boots on trial. The pt-BR and es-ES language packs and an OTA manifest ship alongside. An image with a different set of features comes from the [build configurator](https://angelointj.github.io/simut/configurador/), and CI builds it from `main`, for the Pico 2 W too. | | **Maturity** | | -| **Tests** | Every pull request runs 701 host test cases in 11 suites, 60 s of fuzzing and static analysis, and builds all nine firmware images from a cold cache. Behaviour on real hardware is verified on a bench — see [Verification on hardware](docs/VERIFICATION.md). | +| **Tests** | Every pull request runs 703 host test cases in 11 suites, 60 s of fuzzing and static analysis, and builds all nine firmware images from a cold cache. Behaviour on real hardware is verified on a bench — see [Verification on hardware](docs/VERIFICATION.md). | **Known limitations.** Each one is documented where it applies. - **Updates.** An update over the air reformats the filesystem: @@ -301,7 +301,7 @@ The device exposes a REST API at `http:///api/`: ### Host tests ```bash -pio test -e native # validators, telemetry cursor, labels, parsers, language packs, the License and update screens, the Settings menu, the password check, the web session (300 cases) +pio test -e native # validators, telemetry cursor, labels, parsers, language packs, the License and update screens, the Settings menu, the password check, the web session (302 cases) pio test -e native_history_v5 # V5 history codec (62) pio test -e native_cli # CLI parser (34) pio test -e native_lan # CLI parser, pack help, Settings menu and log codes of a wired build (14) diff --git a/README.pt-BR.md b/README.pt-BR.md index 54e30f88..269a5606 100644 --- a/README.pt-BR.md +++ b/README.pt-BR.md @@ -48,7 +48,7 @@ Os três compartilham o mesmo núcleo: | **Release atual** | **v2.11.1** (12/10/2026); o [changelog](CHANGELOG.pt-BR.md) diz o que cada versão mudou. O SIMUT saiu do beta com a v2.7.0, com base em medições: soak de 8,18 h sem nenhum reboot e 6 de 6 atualizações pelo ar sem perder nada. | | **Imagens publicadas** | Quatro imagens, cada uma em `.uf2` e `.bin`. Para o Pico W: `release` (painel touch TFT), `alpha` (LCD 16×2 com console Bluetooth) e `air` (registrador a bateria sem display). Para o Pico 2 W (RP2350): `releasetwo`, a release, cujo `.uf2` traz a tabela de partições e cujo `.bin` sobe em teste. Junto vêm os packs de idioma pt-BR e es-ES e um manifesto de OTA. Uma imagem com outro conjunto de recursos sai do [configurador de build](https://angelointj.github.io/simut/configurador/), e o CI a compila da `main`, também para o Pico 2 W. | | **Maturidade** | | -| **Testes** | Todo pull request roda 701 casos de teste no host em 11 suítes, 60 s de fuzzing e análise estática, e compila as nove imagens de firmware com o cache frio. O comportamento no hardware real é verificado numa bancada — ver [Verificação no hardware](docs/VERIFICATION.pt-BR.md). | +| **Testes** | Todo pull request roda 703 casos de teste no host em 11 suítes, 60 s de fuzzing e análise estática, e compila as nove imagens de firmware com o cache frio. O comportamento no hardware real é verificado numa bancada — ver [Verificação no hardware](docs/VERIFICATION.pt-BR.md). | **Limitações conhecidas.** Cada uma está documentada onde se aplica. - **Atualização.** A atualização pelo ar reformata o sistema de arquivos: @@ -299,7 +299,7 @@ O aparelho expõe uma API REST em `http:///api/`: ### Testes no host ```bash -pio test -e native # validadores, cursor de telemetria, rótulos, parsers, pacotes de idioma, as telas Licença e de atualização, o menu de Configurações, a verificação de senha, a sessão web (300 casos) +pio test -e native # validadores, cursor de telemetria, rótulos, parsers, pacotes de idioma, as telas Licença e de atualização, o menu de Configurações, a verificação de senha, a sessão web (302 casos) pio test -e native_history_v5 # codec do histórico V5 (62) pio test -e native_cli # parser do CLI (34) pio test -e native_lan # parser do CLI, ajuda do pacote, menu de Configurações e códigos de log numa build cabeada (14) diff --git a/docs/analysis/PLANO_STABLE.md b/docs/analysis/PLANO_STABLE.md index fd0825b4..7df5ee33 100644 --- a/docs/analysis/PLANO_STABLE.md +++ b/docs/analysis/PLANO_STABLE.md @@ -84,14 +84,14 @@ de recebê-la pelo ar: o aparelho a recusa no stage, na validação e no aplicad | Imagem | `.bin` assinado (B) | Folga (B) | |---|---:|---:| -| `pico_w_release` | 1.035.005 | 5.379 | +| `pico_w_release` | 1.035.045 | 5.339 | | `pico_w_air` | 1.035.581 | 4.803 | | `pico_w_alpha` | 990.621 | 49.763 | -| `pico2_w_release` | 999.113 | 569.655 | -| `pico_w_test` | 1.039.429 | 955 | -| `pico_w_asserts` | 1.037.301 | 3.083 | +| `pico2_w_release` | 999.137 | 569.631 | +| `pico_w_test` | 1.039.469 | 915 | +| `pico_w_asserts` | 1.037.341 | 3.043 | | `pico_wire` | 604.393 | 435.991 | -| `pico_wiretft` | 786.865 | 253.519 | +| `pico_wiretft` | 786.913 | 253.471 | | `pico_w_test_https` | isenta | — | diff --git a/src/DisplayManager.cpp b/src/DisplayManager.cpp index 8c55614c..9394da56 100644 --- a/src/DisplayManager.cpp +++ b/src/DisplayManager.cpp @@ -926,9 +926,11 @@ void DisplayManager::beginTouch( ) { /* Inject simulated touch for automation (screenshot capture). * Set flag + coords; Core 1 sees it on the next handleTouch iteration. - * Auto-clears after holdMs (mapTouchPoint): 100 ms is a tap ('touch sim'); - * a larger holdMs ('touch hold') keeps the press down long enough for the - * long-press gestures, then releases so the release edge still fires. */ + * Auto-clears after holdMs, in loopCore1( ) right after handleTouch( ) (not in + * mapTouchPoint( ): two screens never call it for an injection, 2026-10-16): + * 100 ms is a tap ('touch sim'); a larger holdMs ('touch hold') keeps the + * press down long enough for the long-press gestures, then releases so the + * release edge still fires, on every screen. */ void DisplayManager::injectTouch(int16_t x, int16_t y, uint32_t holdMs) { __atomic_store_n(&_simTouchX, x, __ATOMIC_RELEASE); __atomic_store_n(&_simTouchY, y, __ATOMIC_RELEASE); @@ -1391,6 +1393,24 @@ void DisplayManager::loopCore1( ) { /* Process touch BEFORE rendering for same-frame response */ C1_PHASE(C1P_TOUCH_HANDLE); handleTouch( ); + /* The injected touch ENDS here, on its own clock, once per pass and AFTER + * handleTouch( ), on every screen. After, so the pass that first sees the flag + * has processed the tap by then even when that pass comes later than the hold + * (a Core 1 pause, such as a flash save): the flag clears on that pass or a + * later one once it has expired, and _rawTouchState goes false on the next, so + * the release edge comes one pass after the clear, as it always did. + * mapTouchPoint( ) used to end it, and two screens never call that for an + * injection (the touch sensitivity screen and the calibration's measuring + * phase): the flag stayed up for good, _rawTouchState with it, and the 30 s + * idle return never came. Bench, 2026-10-16, on the UI-branch image (the same + * handleTouch logic as main): an injected Cancelar on the sensitivity screen + * was ignored and the board stayed there for 42 s and beyond, until a web + * reboot. */ + if (__atomic_load_n(&_simTouchActive, __ATOMIC_ACQUIRE) && + simTouchExpired(millis( ), __atomic_load_n(&_simTouchSetMs, __ATOMIC_ACQUIRE), + __atomic_load_n(&_simTouchHoldMs, __ATOMIC_ACQUIRE))) { + __atomic_store_n(&_simTouchActive, false, __ATOMIC_RELEASE); + } /* A firmware update takes the panel from any screen (DisplayManager_Ota.cpp); * with no phase published this only notices that there is none. */ otaTick( ); diff --git a/src/DisplayManager.h b/src/DisplayManager.h index 46ac9425..6a2f4968 100644 --- a/src/DisplayManager.h +++ b/src/DisplayManager.h @@ -856,9 +856,11 @@ class DisplayManager { /* Simulated touch injection via CLI 'touch sim X Y'. * Enables automated screenshot capture for all screens via * /api/screenshot — Core 0 sets x/y/active flag, Core 1 (handleTouch - * via _ts->getPoint) checks the override. After processing, _simActive - * returns to false. Coordinates in screen-space (320x240, same as - * mapTouchPoint output). */ + * via _ts->getPoint) checks the override. loopCore1( ) returns the flag + * to false right after handleTouch( ), once _simTouchHoldMs has passed since + * _simTouchSetMs, on every screen: clearing it inside mapTouchPoint( ) left it + * up for good wherever no handler reached that call (2026-10-16). Coordinates + * in screen-space (320x240, same as mapTouchPoint output). */ volatile int16_t _simTouchX = 0; volatile int16_t _simTouchY = 0; volatile bool _simTouchActive = false; diff --git a/src/DisplayManager_Calibration.cpp b/src/DisplayManager_Calibration.cpp index 263644b6..32b7efcb 100644 --- a/src/DisplayManager_Calibration.cpp +++ b/src/DisplayManager_Calibration.cpp @@ -392,14 +392,13 @@ void DisplayManager::mapTouchPoint(TS_Point raw, int16_t &outX, int16_t &outY) { /* Bypass mapping if simulated touch is active. * CLI 'touch sim'/'touch hold' sets _simTouchActive + _simTouchX/Y; here * we return coords already in screen-space, ignoring raw ADC. - * Auto-clear after _simTouchHoldMs — 100 ms is a tap, longer is a hold. */ + * The flag is not cleared here: loopCore1( ) ends the injected touch on its + * own clock, once per pass, right after handleTouch( ) (_simTouchHoldMs: 100 ms + * is a tap, longer is a hold). Clearing it in this function ended it only on + * the screens that call it, and two never do for an injection (2026-10-16). */ if (__atomic_load_n(&_simTouchActive, __ATOMIC_ACQUIRE)) { outX = __atomic_load_n(&_simTouchX, __ATOMIC_ACQUIRE); outY = __atomic_load_n(&_simTouchY, __ATOMIC_ACQUIRE); - if (millis( ) - __atomic_load_n(&_simTouchSetMs, __ATOMIC_ACQUIRE) - > __atomic_load_n(&_simTouchHoldMs, __ATOMIC_ACQUIRE)) { - __atomic_store_n(&_simTouchActive, false, __ATOMIC_RELEASE); - } return; } diff --git a/src/DisplayManager_Touch.cpp b/src/DisplayManager_Touch.cpp index 275515c5..826f3f09 100644 --- a/src/DisplayManager_Touch.cpp +++ b/src/DisplayManager_Touch.cpp @@ -153,6 +153,12 @@ void DisplayManager::handleTouch( ) { if (!timeSince(_lastTouchTime, 15)) return; TS_Point p = _driver.ts->getPoint( ); + /* An injected touch is read ONCE per pass, here: the gates below (the sensitivity + * screen's Cancelar, the calibration's measuring phase, the pressure gate) all use + * this one value, so they cannot disagree about whether a finger is behind the + * touch. */ + const bool simNow = __atomic_load_n(&_simTouchActive, __ATOMIC_ACQUIRE); + /* ── Sensitivity calibration mode: minimum threshold ── * Uses p.z > 50 (ADC noise floor) instead of the calibrated * threshold, to capture the full user pressure range. */ @@ -171,11 +177,20 @@ void DisplayManager::handleTouch( ) { * _sensThreshold — lowest stable p.z found */ - /* CANCEL button: accepts touch at any pressure */ - if (p.z >= 50) { + /* CANCEL button: accepts touch at any pressure — an injected tap too (POST + * /api/touch, 'touch sim'). It carries no pressure (nothing is on the glass, so + * the library reads z = 0): the p.z gate shut it out, mapTouchPoint( ) was never + * reached, and it was the only place the flag was cleared. Bench, 2026-10-16, on + * the UI-branch image (the same handleTouch logic as main): the board stayed on + * this screen for 42 s and beyond, until a web reboot. It is still never a + * pressure sample: with nothing on the glass z = 0, and the p.z < 30 return below + * drops it. */ + if (p.z >= 50 || simNow) { int16_t sx, sy; mapTouchPoint(p, sx, sy); if (sy > 195 && sx < 125) { + /* Provenance before the accept, as below: stale, it blinds the mirror that sent the tap for 5 s. */ + _lastTouchInjected = simNow; if (acceptTouch(0)) { if (_sharedState.isBooting) { _calXMin = 300; _calXMax = 3800; @@ -283,7 +298,6 @@ void DisplayManager::handleTouch( ) { * _rawTouchState and mapTouchPoint bypasses the ADC mapping, but the * pressure check between them was never taught about the bypass — so * `touch sim` answered "injected" and moved nothing. */ - const bool simNow = __atomic_load_n(&_simTouchActive, __ATOMIC_ACQUIRE); if (!simNow && p.z < _sensZThreshold) return; /* Provenance of the touch that is about to open the 5 s priority window, @@ -326,6 +340,16 @@ void DisplayManager::handleTouch( ) { if (_calStep < 8) { + /* An injected touch is not a sample: it has no raw position. getPoint( ) hands + * back the library's cached x/y of the last real touch, and the pressure gate above + * lets the injection in (simNow), so it would start a hold from those stale values, + * and a hold of CAL_HOLD_MS (400 ms) or more would record a corner from them on + * release: a wedge turned into a corrupted calibration, now that the flag ends on its + * own clock (2026-10-16). Ignored before the hold starts, so no corner is ever taken + * from it. It sits after the verdict buttons (_calPhase >= 1), which an injected tap + * still presses; an injection over a real finger's hold only pauses that + * finger's samples until it ends. */ + if (simNow) return; if (!_calHolding) { /* Hold start: reset accumulators */ _calHolding = true; diff --git a/src/SystemDefs_Time.h b/src/SystemDefs_Time.h index 3fd55633..0565841d 100644 --- a/src/SystemDefs_Time.h +++ b/src/SystemDefs_Time.h @@ -181,6 +181,29 @@ inline bool timeSince(uint32_t start, uint32_t duration) { return (int32_t)(millis( ) - start) >= (int32_t)duration; } +/** + * @brief True once an injected touch has been down longer than its hold. + * + * POST /api/touch, the CLI 'touch sim' / 'touch hold' and the web mirror raise + * DisplayManager::_simTouchActive for a hold time (injectTouch( )), and + * loopCore1( ) ends it with this test on every pass, right after handleTouch( ), + * whatever the screen: the pass that first sees the flag has processed it by + * then, even one that comes later than the hold. It used to end only inside + * mapTouchPoint( ), which two screens never call for an injection, and the + * flag stayed up for good (2026-10-16). + * + * Strictly greater: a 100 ms tap is still down at 100 ms and released at 101. + * Unsigned subtraction, so the millis( ) wrap (~49.7 days) cannot hold one down. + * + * @param now millis() of this Core 1 pass. + * @param setMs millis() at which injectTouch( ) raised the flag. + * @param holdMs How long the touch stays down (100 = a tap). + * @return true if the touch has outlived its hold. + */ +inline bool simTouchExpired(uint32_t now, uint32_t setMs, uint32_t holdMs) { + return now - setMs > holdMs; +} + /** * @brief Time remaining until deadline, in milliseconds. Wrap-safe. * diff --git a/test/test_validators/test_main.cpp b/test/test_validators/test_main.cpp index e29ede81..95ebd686 100644 --- a/test/test_validators/test_main.cpp +++ b/test/test_validators/test_main.cpp @@ -559,6 +559,30 @@ void test_timeSince_basic(void) { TEST_ASSERT_FALSE(timeSince(4500, 1000)); /* só 500ms decorreu */ } +/* An injected touch ends on its own clock: simTouchExpired( ), which loopCore1( ) calls on + * every pass (mapTouchPoint( ) used to, and two screens never reach it for an injection; + * 2026-10-16). Strictly greater than the hold: a 100 ms tap is still down AT 100 ms and + * released at 101. */ +void test_simTouchExpired_tap_and_hold(void) { + TEST_ASSERT_FALSE(simTouchExpired(5000, 5000, 100)); /* the pass that first sees it */ + TEST_ASSERT_FALSE(simTouchExpired(5100, 5000, 100)); /* exactly the hold: still down */ + TEST_ASSERT_TRUE(simTouchExpired(5101, 5000, 100)); /* one past it: released */ + TEST_ASSERT_FALSE(simTouchExpired(8000, 5000, 3000)); /* 'touch hold 3000': down at 3 s */ + TEST_ASSERT_TRUE(simTouchExpired(8001, 5000, 3000)); +} + +/* The same test across the millis( ) wrap (~49.7 days of uptime): a touch set 256 ms before + * it ends on time before the wrap and after it, and one set 50 ms before it ends 50 ms after. */ +void test_simTouchExpired_wrap_safe(void) { + const uint32_t set = 0xFFFFFF00u; /* 256 ms before the wrap */ + TEST_ASSERT_FALSE(simTouchExpired(0xFFFFFF64u, set, 100)); /* before it: exactly the hold */ + TEST_ASSERT_TRUE(simTouchExpired(0xFFFFFF65u, set, 100)); /* before it: one past */ + TEST_ASSERT_TRUE(simTouchExpired(5, set, 100)); /* after it: 261 ms in */ + const uint32_t straddle = 0xFFFFFFCEu; /* 50 ms before the wrap */ + TEST_ASSERT_FALSE(simTouchExpired(50, straddle, 100)); /* exactly the hold, across it */ + TEST_ASSERT_TRUE(simTouchExpired(51, straddle, 100)); +} + /* =========================================================================== */ /* dallasCrc8 */ @@ -5019,6 +5043,8 @@ int main(int /*argc*/, char** /*argv*/) { RUN_TEST(test_timeReached_basic); RUN_TEST(test_timeReached_wrap_safe); RUN_TEST(test_timeSince_basic); + RUN_TEST(test_simTouchExpired_tap_and_hold); + RUN_TEST(test_simTouchExpired_wrap_safe); /* dallasCrc8 */ RUN_TEST(test_dallasCrc8_known_vectors); diff --git a/tools/flash_budget.json b/tools/flash_budget.json index 0fd2ee38..85689c17 100644 --- a/tools/flash_budget.json +++ b/tools/flash_budget.json @@ -535,30 +535,30 @@ "(ota_exempt, with the reason) and records no 'bin'; its budget still holds." ], "ceiling": 1044480, - "measured_on": "2026-10-05 chore/v2.11.1: SIMUT_VERSION 2.11.1, the only source change. All seven rebuilt here. The version has as many characters as 2.11.0, but the login page carries it, and its gzip came out 5 B smaller in every image: measured and .bin 8 B under the record on release, test, asserts and pico2_w_release; alpha, air and test_https equal, their alignment absorbing it. Budgets unchanged. 2026-10-12 feat/lan-switch: pico_wire joins (the SIMUT Wire, a plain Pico with the network on an ENC28J60, no driver yet), measured 587,980 B and a .bin of 600,016 B; the seven others are byte for byte what main 2a391da builds. Then twelve sensor slots (-DMAX_SENSORS=12, GP12-GP15 are the module's): 588,020 B and a .bin of 600,056 B, +40 B for clearing the slots past the twelfth on every config load; RAM 102,416 -> 102,064 B. Then eleven (the module's INT moved to GP11, 2026-10-12): 587,972 B and a .bin of 600,008 B, RAM 101,980 B, measured on 28a54c8. 2026-10-13 feat/wire-driver: the wired driver takes pico_wire to 597,448 B and a .bin of 609,480 B (RAM 104,680 B); the seven Wi-Fi images stay byte for byte what dd7efdc builds until the top-panel fix, which grows them by 32 to 64 B (release, test, test_https and asserts +48, alpha +64, air +32, pico2_w_release +56; .bin +48 on release, test and asserts, +56 on pico2_w_release, unchanged on alpha and air). All eight measured at 4912762's tree plus this change. The review's fixes (module before the sensors, the static address checks, the MAC before the start, syslog and the OTA teardown on the cable, the log latches): pico_wire 598,560 B (+1,112 B), .bin 610,592 B, RAM 104,688 B; the seven Wi-Fi images compile none of it. 2026-10-13 feat/wire-surface: pico_wiretft joins (the SIMUT Wire with the TFT panel and touch): 780,628 B and a .bin of 792,664 B (RAM 109,784 B), built on the driver's tree plus the manifest entry and no change to src/. The eight others keep their records: no flag and no source of theirs moved. Then the surface a wired build loses with the radio (the web pages, the console, the panels and the third-party list): pico_wire 595,632 B and a .bin of 607,664 B (RAM 104,684 B), pico_wiretft 777,428 B and a .bin of 789,464 B (RAM 109,788 B), both budgets brought down to them; the seven radio images byte for byte the same at each of the branch's commits, compared by sha256 (pico_w_release 8b565f66 throughout). Then the captive-portal redirect and probes, the setup AP's, compiled out of the wired builds: pico_wire 595,016 B (.bin 607,048), pico_wiretft 776,820 B (.bin 788,856), RAM unchanged; budgets brought down again. Then four log codes of the cable's own (529-532) with their three rows in the edge filter, compiled into the wired builds only: pico_wire 595,152 B (+136, .bin 607,184), pico_wiretft 776,956 B (+136, .bin 788,992); the seven radio images unchanged. 2026-10-13 feat/net-boundary: the wired link generic over LwipIntfDev (the chip in a device header, src/net/Enc28j60Device.h) and a module stopped by an update that did not happen started again by the retry: pico_wire 595,208 B (+56, .bin 607,240), pico_wiretft 777,004 B (+48, .bin 789,040), RAM unchanged. The move alone is byte for byte neutral (pico_wire sha256 3edd1440, pico_wiretft e58d65e9, before WiredLink::chipName( ) existed); the growth is the module-fail message built from the driver's name (a second concatenation in startWired( ), plus chipName( ) and started( ) now reached) and the restart path in wiredLost( ), the only two function bodies that differ in the normalized disassembly of both images; the seven radio images are byte for byte the same. Then the log line of that path names what happened (\"module stopped\", where it said \"link down\"): +48 B on both, pico_wire 595,256 B (.bin 607,288), pico_wiretft 777,052 B (.bin 789,088); the seven radio images unchanged. Then the review of that change: the stopped module is looked for first in updateWired( ), in every state, because a unit still on its way up (NET_CONNECTING: booted with the cable out, or a lease pending) never reached wiredLost( ) and was never started again: +40 B on both, pico_wire 595,296 B (.bin 607,328), pico_wiretft 777,092 B (.bin 789,128), RAM unchanged; updateWired( ) and wiredLost( ) the only two function bodies that differ in the normalized disassembly of both images. The seven radio images are byte for byte the same, pico_w_air included: its radio power-down moved into net::powerDownForSleep( ) (src/net/NetLink.h) without moving a byte. 2026-10-13 feat/net-radio-unit: the radio's state machine moved from NetworkManager.cpp into its own unit, src/net/NetworkManager_Cyw43.cpp, which the wired images do not build, and NetworkManager.cpp became the host of what every network needs, reaching the link through three methods each unit defines. The wired images lose the radio code they compiled dead: pico_wire 590,740 B (-4,556, .bin 602,776), pico_wiretft 772,852 B (-4,240, .bin 784,888), both budgets brought down. The radio images pay for the split: release +48 B (1,021,180, .bin 1,033,212), asserts +40 (1,023,468, .bin 1,035,500), pico2_w_release +120 (997,328, .bin 997,352), test_https -8 (1,030,692), test, alpha and air the same; the normalized disassembly of all seven differs in four bodies only: begin( ) and update( ) (now the host's), handleConnecting( ) and getRssi( ), which call resetReconnectLadder( ) and isConnected( ) across the unit boundary where the compiler inlined them before. Inlining the two helpers in the header instead was measured and grew the radio images more (release +88, test +56, asserts +80). 2026-10-13 fix/radio-link-in-ntp-wait: the radio watches its link inside the NTP wait, one check in its updateLinkFirst( ) and NET_READY's loss in a helper that logs it, wifiLost( ). The seven radio images grow: release +72 B (1,021,252, .bin 1,033,284), test +80 (1,025,788, .bin 1,037,820, under its budget), test_https +80 (1,030,772), asserts +80 (1,023,548, .bin 1,035,580), alpha +88 (975,060, under its budget, the .bin the same), air +80 (1,018,336, under its budget, the .bin the same), pico2_w_release +72 (997,400, .bin 997,424); the two wired images are byte for byte the same. With the log at each of the two callers instead of in the helper, release grew +96 B. 2026-10-13 fix/silent-radio-no-blind-join (#266): a reconnect scan whose start the radio did not answer, and that never finished, is not escaped by a blind join; two in a row past the first half hour restart the radio by safeReboot( ). The radio images grow: release +376 B (1,021,628, .bin 1,033,660), test +352 (1,026,140, .bin 1,038,172), test_https +352 (1,031,124), asserts +384 (1,023,932, .bin 1,035,964), alpha +280 (975,340) and air +280 (1,018,616), both .bin the same, pico2_w_release +376 (997,776, .bin 997,800). The wired images carry the two new members of the shared header and the host's reset of one: pico_wire the same size (590,740), pico_wiretft +16 (772,868, .bin 784,904). Writing the two log lines with one snprintf each instead of the String concatenations was measured: 16 B less on release, not worth the buffer. 2026-10-14 fix/link-health-in-ntp-wait: getRssi( ) answers the level of any link that is up, the NTP wait included, on the radio and on the cable (isLinkUp( ) where it read isConnected( )). The RP2040 images are the same size; pico2_w_release +8 B (997,784, .bin 997,808). With it, the Air's FLUSH waits for NTP 10 s at most (AIR_FLUSH_NTP_WAIT_MS, airFlushNetLost( )): pico_w_air +48 B (1,018,664, the .bin the same), the other eight byte for byte the same. 2026-10-14 fix/web-short-write: override 2m (webserver_short_write.patch), a write cut short ends the response and closes the client as its real type (stopClient( ), so over TLS too), and the count read out (metr.cgw, /metrics, and `show net status` with the TCP segment pool and tcp_write's memory failures). Every image grows: release +512 B (1,022,140, .bin 1,034,172), test +480 (1,026,620, .bin 1,038,652), test_https +504 (1,031,628), asserts +504 (1,024,436, .bin 1,036,468), alpha +344 (975,684) and air +352 (1,019,016), both .bin the same, pico2_w_release +520 (998,304, .bin 998,328), pico_wire +480 (591,220, .bin 603,256), pico_wiretft +504 (773,372, .bin 785,408). Of the release's 512 B, 394 are sized symbols (the status handler 72, _writeAll( ) and _shortWrite( ) 78, the console 56, the metrics 32, the three _currentClientStop( ) 32, the framework's call sites and WebManager the rest) and the rest the new strings. 2026-10-14 fix/web-fewer-writes: override 2n (webserver_fewer_writes.patch), HTTPServer collects a response's small writes in a buffer of one TCP segment and sends them together (#189). Every image grows: release +328 B (1,022,468, .bin 1,034,500), test +328 (1,026,948, .bin 1,038,980, 1,163 B of OTA headroom left), test_https +328 (1,031,956), asserts +328 (1,024,764, .bin 1,036,796), alpha +328 (976,012, the .bin the same), air +328 (1,019,344) with its .bin +4,096 (1,035,340), pico2_w_release +296 (998,600, .bin 998,624), pico_wire +328 (591,548, .bin 603,584), pico_wiretft +328 (773,700, .bin 785,736). The Air's .bin moves in 4 KB steps, as the alpha's does: their .rodata starts on a 4 KB boundary because it holds BTstack's __bluetooth_tlv (8 KB, sector-aligned), and the Air's .text ended 196 B short of the next step. Against main's release the sized symbols grow 362 B and the image 328: _outAppend( ) 140, the constructor's malloc( ) 56, _outBodySent( ) 32, _outFlush( ) 22, and the flushes and resets in send( ), sendContent( ), _prepareHeader( ), _handleRequest( ), _finalizeResponse( ) and the destructor the rest. The 1,460 B buffer is heap, allocated with the server. As an array among the class's members instead of behind a pointer at its end, it pushed every member after it past the Cortex-M0+'s short load/store offsets: release +1,152 B, against +352 B for the same code behind the pointer. The bench ran the release of the branch's first version (sha256 b1a7e7da), which had two more stores, _outLen = 0 in _shortWrite( ), which nothing reaches, and in stopClient( ), which runs but changes nothing: what it would drop goes to a client already closed, whose writes return 0. The final normalized disassembly differs from the bench image's in _shortWrite( ) and dropAbortedStream( ) only, 16 B. 2026-10-15 deps/pico-enc28j60-1.3.1: the ENC28J60 library moves from fb16ec1 to the commit of v1.3.1 (4984e2f), whose lwIP interface keeps up to eight frames in the chip and uploads one while the one before is on the wire (the library's 1.3.0; 1.3.1 changed an example and the documentation only). The wired images grow: pico_wire +568 B (592,116, .bin 604,152), pico_wiretft +624 (774,324, .bin 786,360), RAM +56 on both (the queue's state in the driver object). Nearly all of it is the new send path (_sendQueued( ) 228, _sendPoll( ), _sendFinish( ) and enc28j60_frame_send_finish_blk( ) 100 each, _sendStartNext( ) 80, against sendFrame( ) -144 and enc28j60_frame_send_blk( ) -90); the /license page, whose third-party note names the new commit, 12. The seven radio images are byte for byte main's (192e1ad), compared by sha256. 2026-10-15 fix/wire-tft-flash-render-274: the WiFiServerSecure accept override (wifiserversecure_drop_plaintext.patch, patch.sh 2b-ter) drops a non-TLS client on the TLS port before the BearSSL handshake, decided from its first byte (#274). It is compiled wherever SIMUT_WEB_HTTPS is set, so the five HTTPS images grow: pico_wiretft +56 B (774,380, .bin 786,416), release +48 (1,022,516, .bin 1,034,548), test_https +48 (1,032,004), asserts +56 (1,024,820, .bin 1,036,852), pico2_w_release +24 (998,624, .bin 998,648). pico_wire, pico_w_test, alpha and air compile no HTTPS server, so the patched accept path is not in them and they keep main's sizes (d5f7063). 2026-10-15 perf/tft-spi-clk-peri: setup( ) clocks clk_peri from clk_sys on the six images that drive the panel and SIMUT_TFT_SPI_HZ is 66.5 MHz; /api/status gains sys.clk_peri and sys.tft_hz through macros, so the images without the panel compile the very tokens they did. Flash used (.bin): release +216 (1,022,732, .bin 1,034,764), test +208 (1,027,156, .bin 1,039,188), test_https +208 (1,032,212), asserts +208 (1,025,028, .bin 1,037,060), pico2_w_release +224 (998,848, .bin 998,872), wiretft +208 (774,588, .bin 786,624). pico_wire, alpha and air are byte for byte main's (80c3c88), compared by sha256.", - "measured_at_commit": "2026-10-13 feat/wire-driver 4912762 (the seven radio images; the budgets only move here), feat/wire-surface, the log-code commit, and feat/net-boundary, B2 on 20c06b0 plus this change, then ebcf29e plus the review's fixes (pico_wire, pico_wiretft); feat/net-radio-unit on 3055b31 plus this change (all nine); fix/radio-link-in-ntp-wait on 53b6d7b plus this change (the seven radio images).; fix/silent-radio-no-blind-join on 2290638 plus this change (all nine).; fix/link-health-in-ntp-wait on 5293bae plus this change (all nine). measured is the zopfli image PlatformIO prints.; fix/web-short-write on f1b8b8f plus this change (all nine), built against a PlatformIO core that carries the override (PLATFORMIO_CORE_DIR, a hardlinked copy of ~/.platformio patched by patch.sh).; fix/web-fewer-writes on 686a1de plus this change (all nine), built against a PlatformIO core that carries overrides 2m and 2n (PLATFORMIO_CORE_DIR, a hardlinked copy of ~/.platformio patched by patch.sh).; deps/pico-enc28j60-1.3.1 on 192e1ad plus this change (all nine), built against ~/.platformio, which carries overrides 2m and 2n since 2026-10-15.; fix/wire-tft-flash-render-274 on d5f7063 plus this change (the five HTTPS images), built against ~/.platformio patched by patch.sh, which carries overrides 2m, 2n and the WiFiServerSecure accept patch (2b-ter).; perf/tft-spi-clk-peri on 80c3c88 plus this change (the six panel images), built against the ~/.platformio as main left it (its pico_w_release .bin matched the record to the byte before the change).", + "measured_on": "2026-10-05 chore/v2.11.1: SIMUT_VERSION 2.11.1, the only source change. All seven rebuilt here. The version has as many characters as 2.11.0, but the login page carries it, and its gzip came out 5 B smaller in every image: measured and .bin 8 B under the record on release, test, asserts and pico2_w_release; alpha, air and test_https equal, their alignment absorbing it. Budgets unchanged. 2026-10-12 feat/lan-switch: pico_wire joins (the SIMUT Wire, a plain Pico with the network on an ENC28J60, no driver yet), measured 587,980 B and a .bin of 600,016 B; the seven others are byte for byte what main 2a391da builds. Then twelve sensor slots (-DMAX_SENSORS=12, GP12-GP15 are the module's): 588,020 B and a .bin of 600,056 B, +40 B for clearing the slots past the twelfth on every config load; RAM 102,416 -> 102,064 B. Then eleven (the module's INT moved to GP11, 2026-10-12): 587,972 B and a .bin of 600,008 B, RAM 101,980 B, measured on 28a54c8. 2026-10-13 feat/wire-driver: the wired driver takes pico_wire to 597,448 B and a .bin of 609,480 B (RAM 104,680 B); the seven Wi-Fi images stay byte for byte what dd7efdc builds until the top-panel fix, which grows them by 32 to 64 B (release, test, test_https and asserts +48, alpha +64, air +32, pico2_w_release +56; .bin +48 on release, test and asserts, +56 on pico2_w_release, unchanged on alpha and air). All eight measured at 4912762's tree plus this change. The review's fixes (module before the sensors, the static address checks, the MAC before the start, syslog and the OTA teardown on the cable, the log latches): pico_wire 598,560 B (+1,112 B), .bin 610,592 B, RAM 104,688 B; the seven Wi-Fi images compile none of it. 2026-10-13 feat/wire-surface: pico_wiretft joins (the SIMUT Wire with the TFT panel and touch): 780,628 B and a .bin of 792,664 B (RAM 109,784 B), built on the driver's tree plus the manifest entry and no change to src/. The eight others keep their records: no flag and no source of theirs moved. Then the surface a wired build loses with the radio (the web pages, the console, the panels and the third-party list): pico_wire 595,632 B and a .bin of 607,664 B (RAM 104,684 B), pico_wiretft 777,428 B and a .bin of 789,464 B (RAM 109,788 B), both budgets brought down to them; the seven radio images byte for byte the same at each of the branch's commits, compared by sha256 (pico_w_release 8b565f66 throughout). Then the captive-portal redirect and probes, the setup AP's, compiled out of the wired builds: pico_wire 595,016 B (.bin 607,048), pico_wiretft 776,820 B (.bin 788,856), RAM unchanged; budgets brought down again. Then four log codes of the cable's own (529-532) with their three rows in the edge filter, compiled into the wired builds only: pico_wire 595,152 B (+136, .bin 607,184), pico_wiretft 776,956 B (+136, .bin 788,992); the seven radio images unchanged. 2026-10-13 feat/net-boundary: the wired link generic over LwipIntfDev (the chip in a device header, src/net/Enc28j60Device.h) and a module stopped by an update that did not happen started again by the retry: pico_wire 595,208 B (+56, .bin 607,240), pico_wiretft 777,004 B (+48, .bin 789,040), RAM unchanged. The move alone is byte for byte neutral (pico_wire sha256 3edd1440, pico_wiretft e58d65e9, before WiredLink::chipName( ) existed); the growth is the module-fail message built from the driver's name (a second concatenation in startWired( ), plus chipName( ) and started( ) now reached) and the restart path in wiredLost( ), the only two function bodies that differ in the normalized disassembly of both images; the seven radio images are byte for byte the same. Then the log line of that path names what happened (\"module stopped\", where it said \"link down\"): +48 B on both, pico_wire 595,256 B (.bin 607,288), pico_wiretft 777,052 B (.bin 789,088); the seven radio images unchanged. Then the review of that change: the stopped module is looked for first in updateWired( ), in every state, because a unit still on its way up (NET_CONNECTING: booted with the cable out, or a lease pending) never reached wiredLost( ) and was never started again: +40 B on both, pico_wire 595,296 B (.bin 607,328), pico_wiretft 777,092 B (.bin 789,128), RAM unchanged; updateWired( ) and wiredLost( ) the only two function bodies that differ in the normalized disassembly of both images. The seven radio images are byte for byte the same, pico_w_air included: its radio power-down moved into net::powerDownForSleep( ) (src/net/NetLink.h) without moving a byte. 2026-10-13 feat/net-radio-unit: the radio's state machine moved from NetworkManager.cpp into its own unit, src/net/NetworkManager_Cyw43.cpp, which the wired images do not build, and NetworkManager.cpp became the host of what every network needs, reaching the link through three methods each unit defines. The wired images lose the radio code they compiled dead: pico_wire 590,740 B (-4,556, .bin 602,776), pico_wiretft 772,852 B (-4,240, .bin 784,888), both budgets brought down. The radio images pay for the split: release +48 B (1,021,180, .bin 1,033,212), asserts +40 (1,023,468, .bin 1,035,500), pico2_w_release +120 (997,328, .bin 997,352), test_https -8 (1,030,692), test, alpha and air the same; the normalized disassembly of all seven differs in four bodies only: begin( ) and update( ) (now the host's), handleConnecting( ) and getRssi( ), which call resetReconnectLadder( ) and isConnected( ) across the unit boundary where the compiler inlined them before. Inlining the two helpers in the header instead was measured and grew the radio images more (release +88, test +56, asserts +80). 2026-10-13 fix/radio-link-in-ntp-wait: the radio watches its link inside the NTP wait, one check in its updateLinkFirst( ) and NET_READY's loss in a helper that logs it, wifiLost( ). The seven radio images grow: release +72 B (1,021,252, .bin 1,033,284), test +80 (1,025,788, .bin 1,037,820, under its budget), test_https +80 (1,030,772), asserts +80 (1,023,548, .bin 1,035,580), alpha +88 (975,060, under its budget, the .bin the same), air +80 (1,018,336, under its budget, the .bin the same), pico2_w_release +72 (997,400, .bin 997,424); the two wired images are byte for byte the same. With the log at each of the two callers instead of in the helper, release grew +96 B. 2026-10-13 fix/silent-radio-no-blind-join (#266): a reconnect scan whose start the radio did not answer, and that never finished, is not escaped by a blind join; two in a row past the first half hour restart the radio by safeReboot( ). The radio images grow: release +376 B (1,021,628, .bin 1,033,660), test +352 (1,026,140, .bin 1,038,172), test_https +352 (1,031,124), asserts +384 (1,023,932, .bin 1,035,964), alpha +280 (975,340) and air +280 (1,018,616), both .bin the same, pico2_w_release +376 (997,776, .bin 997,800). The wired images carry the two new members of the shared header and the host's reset of one: pico_wire the same size (590,740), pico_wiretft +16 (772,868, .bin 784,904). Writing the two log lines with one snprintf each instead of the String concatenations was measured: 16 B less on release, not worth the buffer. 2026-10-14 fix/link-health-in-ntp-wait: getRssi( ) answers the level of any link that is up, the NTP wait included, on the radio and on the cable (isLinkUp( ) where it read isConnected( )). The RP2040 images are the same size; pico2_w_release +8 B (997,784, .bin 997,808). With it, the Air's FLUSH waits for NTP 10 s at most (AIR_FLUSH_NTP_WAIT_MS, airFlushNetLost( )): pico_w_air +48 B (1,018,664, the .bin the same), the other eight byte for byte the same. 2026-10-14 fix/web-short-write: override 2m (webserver_short_write.patch), a write cut short ends the response and closes the client as its real type (stopClient( ), so over TLS too), and the count read out (metr.cgw, /metrics, and `show net status` with the TCP segment pool and tcp_write's memory failures). Every image grows: release +512 B (1,022,140, .bin 1,034,172), test +480 (1,026,620, .bin 1,038,652), test_https +504 (1,031,628), asserts +504 (1,024,436, .bin 1,036,468), alpha +344 (975,684) and air +352 (1,019,016), both .bin the same, pico2_w_release +520 (998,304, .bin 998,328), pico_wire +480 (591,220, .bin 603,256), pico_wiretft +504 (773,372, .bin 785,408). Of the release's 512 B, 394 are sized symbols (the status handler 72, _writeAll( ) and _shortWrite( ) 78, the console 56, the metrics 32, the three _currentClientStop( ) 32, the framework's call sites and WebManager the rest) and the rest the new strings. 2026-10-14 fix/web-fewer-writes: override 2n (webserver_fewer_writes.patch), HTTPServer collects a response's small writes in a buffer of one TCP segment and sends them together (#189). Every image grows: release +328 B (1,022,468, .bin 1,034,500), test +328 (1,026,948, .bin 1,038,980, 1,163 B of OTA headroom left), test_https +328 (1,031,956), asserts +328 (1,024,764, .bin 1,036,796), alpha +328 (976,012, the .bin the same), air +328 (1,019,344) with its .bin +4,096 (1,035,340), pico2_w_release +296 (998,600, .bin 998,624), pico_wire +328 (591,548, .bin 603,584), pico_wiretft +328 (773,700, .bin 785,736). The Air's .bin moves in 4 KB steps, as the alpha's does: their .rodata starts on a 4 KB boundary because it holds BTstack's __bluetooth_tlv (8 KB, sector-aligned), and the Air's .text ended 196 B short of the next step. Against main's release the sized symbols grow 362 B and the image 328: _outAppend( ) 140, the constructor's malloc( ) 56, _outBodySent( ) 32, _outFlush( ) 22, and the flushes and resets in send( ), sendContent( ), _prepareHeader( ), _handleRequest( ), _finalizeResponse( ) and the destructor the rest. The 1,460 B buffer is heap, allocated with the server. As an array among the class's members instead of behind a pointer at its end, it pushed every member after it past the Cortex-M0+'s short load/store offsets: release +1,152 B, against +352 B for the same code behind the pointer. The bench ran the release of the branch's first version (sha256 b1a7e7da), which had two more stores, _outLen = 0 in _shortWrite( ), which nothing reaches, and in stopClient( ), which runs but changes nothing: what it would drop goes to a client already closed, whose writes return 0. The final normalized disassembly differs from the bench image's in _shortWrite( ) and dropAbortedStream( ) only, 16 B. 2026-10-15 deps/pico-enc28j60-1.3.1: the ENC28J60 library moves from fb16ec1 to the commit of v1.3.1 (4984e2f), whose lwIP interface keeps up to eight frames in the chip and uploads one while the one before is on the wire (the library's 1.3.0; 1.3.1 changed an example and the documentation only). The wired images grow: pico_wire +568 B (592,116, .bin 604,152), pico_wiretft +624 (774,324, .bin 786,360), RAM +56 on both (the queue's state in the driver object). Nearly all of it is the new send path (_sendQueued( ) 228, _sendPoll( ), _sendFinish( ) and enc28j60_frame_send_finish_blk( ) 100 each, _sendStartNext( ) 80, against sendFrame( ) -144 and enc28j60_frame_send_blk( ) -90); the /license page, whose third-party note names the new commit, 12. The seven radio images are byte for byte main's (192e1ad), compared by sha256. 2026-10-15 fix/wire-tft-flash-render-274: the WiFiServerSecure accept override (wifiserversecure_drop_plaintext.patch, patch.sh 2b-ter) drops a non-TLS client on the TLS port before the BearSSL handshake, decided from its first byte (#274). It is compiled wherever SIMUT_WEB_HTTPS is set, so the five HTTPS images grow: pico_wiretft +56 B (774,380, .bin 786,416), release +48 (1,022,516, .bin 1,034,548), test_https +48 (1,032,004), asserts +56 (1,024,820, .bin 1,036,852), pico2_w_release +24 (998,624, .bin 998,648). pico_wire, pico_w_test, alpha and air compile no HTTPS server, so the patched accept path is not in them and they keep main's sizes (d5f7063). 2026-10-15 perf/tft-spi-clk-peri: setup( ) clocks clk_peri from clk_sys on the six images that drive the panel and SIMUT_TFT_SPI_HZ is 66.5 MHz; /api/status gains sys.clk_peri and sys.tft_hz through macros, so the images without the panel compile the very tokens they did. Flash used (.bin): release +216 (1,022,732, .bin 1,034,764), test +208 (1,027,156, .bin 1,039,188), test_https +208 (1,032,212), asserts +208 (1,025,028, .bin 1,037,060), pico2_w_release +224 (998,848, .bin 998,872), wiretft +208 (774,588, .bin 786,624). pico_wire, alpha and air are byte for byte main's (80c3c88), compared by sha256. 2026-10-16 fix/injected-touch-never-ends: the injected touch (POST /api/touch, `touch sim`, `touch hold`) ends in loopCore1( ) right after handleTouch( ) instead of inside mapTouchPoint( ); the sensitivity screen's Cancelar takes an injection and records its provenance (_lastTouchInjected) before the accept, and the calibration's measuring phase ignores it, with simNow read once per handleTouch( ). Flash used (.bin): release +40 (1,022,772, .bin 1,034,804), test +40 (1,027,196, .bin 1,039,228), test_https +40 (1,032,252), asserts +40 (1,025,068, .bin 1,037,100), pico2_w_release +24 (998,872, .bin 998,896), wiretft +48 (774,636, .bin 786,672). The provenance store is 16 B of that on release, test, test_https, asserts and wiretft and 8 B on pico2_w_release. The same test placed before _rawTouchState, the change's first version, was 8 B smaller on release and test_https and the same on the other four; it released a tap unseen when the first Core 1 pass came later than its hold, so it was dropped. pico_wire, alpha and air come out the size of their records, flash used and .bin; they were not compared by sha256.", + "measured_at_commit": "2026-10-13 feat/wire-driver 4912762 (the seven radio images; the budgets only move here), feat/wire-surface, the log-code commit, and feat/net-boundary, B2 on 20c06b0 plus this change, then ebcf29e plus the review's fixes (pico_wire, pico_wiretft); feat/net-radio-unit on 3055b31 plus this change (all nine); fix/radio-link-in-ntp-wait on 53b6d7b plus this change (the seven radio images).; fix/silent-radio-no-blind-join on 2290638 plus this change (all nine).; fix/link-health-in-ntp-wait on 5293bae plus this change (all nine). measured is the zopfli image PlatformIO prints.; fix/web-short-write on f1b8b8f plus this change (all nine), built against a PlatformIO core that carries the override (PLATFORMIO_CORE_DIR, a hardlinked copy of ~/.platformio patched by patch.sh).; fix/web-fewer-writes on 686a1de plus this change (all nine), built against a PlatformIO core that carries overrides 2m and 2n (PLATFORMIO_CORE_DIR, a hardlinked copy of ~/.platformio patched by patch.sh).; deps/pico-enc28j60-1.3.1 on 192e1ad plus this change (all nine), built against ~/.platformio, which carries overrides 2m and 2n since 2026-10-15.; fix/wire-tft-flash-render-274 on d5f7063 plus this change (the five HTTPS images), built against ~/.platformio patched by patch.sh, which carries overrides 2m, 2n and the WiFiServerSecure accept patch (2b-ter).; perf/tft-spi-clk-peri on 80c3c88 plus this change (the six panel images), built against the ~/.platformio as main left it (its pico_w_release .bin matched the record to the byte before the change).; fix/injected-touch-never-ends on ea4fcb1 plus this change (all nine), built against the ~/.platformio as main left it (its pico_wiretft .bin matched the record to the byte before the change: 786,624 B, sha256 3aff19e8).", "ota_exempt": { "pico_w_test_https": "A bench image: the one with the full console and the TLS server together, for the HTTPS suites (AGENTS.md). It reaches the rig over USB, and release-ota.yml does not publish it. #232 (2026-10-02) left it 147 B under OTA_APP_SAFE_MAX_SIZE, and keeping it there meant cutting code the product images ship too, or moving more of its pages to LittleFS. Over the ceiling the device refuses to stage it, which costs only installing this image over the air; its flash budget still holds. Exempt since 2026-10-02 (docs/analysis/PLANO_STABLE.md, B6)." }, "envs": { "pico_w_release": { - "budget": 1022732, - "measured": 1022732, - "bin": 1034764 + "budget": 1022772, + "measured": 1022772, + "bin": 1034804 }, "pico_w_test": { - "budget": 1027156, - "measured": 1027156, - "bin": 1039188 + "budget": 1027196, + "measured": 1027196, + "bin": 1039228 }, "pico_w_test_https": { - "budget": 1032212, - "measured": 1032212 + "budget": 1032252, + "measured": 1032252 }, "pico_w_asserts": { - "budget": 1025028, - "measured": 1025028, - "bin": 1037060 + "budget": 1025068, + "measured": 1025068, + "bin": 1037100 }, "pico_w_alpha": { "budget": 976012, @@ -571,9 +571,9 @@ "bin": 1035340 }, "pico2_w_release": { - "budget": 998848, - "measured": 998848, - "bin": 998872, + "budget": 998872, + "measured": 998872, + "bin": 998896, "ceiling": 3145728, "slot": 1568768 }, @@ -583,9 +583,9 @@ "bin": 604152 }, "pico_wiretft": { - "budget": 774588, - "measured": 774588, - "bin": 786624 + "budget": 774636, + "measured": 774636, + "bin": 786672 } } }