Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.es-ES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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** | <ul><li>`release`: **estable**.</li><li>`alpha`: publicado y probado en el banco, con su LCD 16×2 incluido desde el 26/09/2026.</li><li>`air`: **experimental**. Su único soak largo falló: un sueño en el ciclo 119 nunca despertó (F28). Hoy lo mitiga un watchdog a lo largo del despertar; la causa raíz no está confirmada.</li><li>`releasetwo`: **nueva** en la v2.11.0, probada en el banco en una Pico 2 W (RP2350 A2). Se actualiza por el aire en el otro slot, a prueba, y vuelve sola cuando la actualización no se confirma.</li></ul> |
| **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:
Expand Down Expand Up @@ -303,7 +303,7 @@ El dispositivo expone una API REST en `http://<ip-del-dispositivo>/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)
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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** | <ul><li>`release`: **stable**.</li><li>`alpha`: published and bench-tested, its 16×2 LCD included since 2026-09-26.</li><li>`air`: **experimental**. Its one long soak failed: a sleep in cycle 119 never woke (F28). A watchdog across the wake now mitigates it; the root cause is not confirmed.</li><li>`releasetwo`: **new** in v2.11.0, bench-tested on one Pico 2 W (RP2350 A2). It updates over the air into its other slot, on trial, and goes back by itself when the update does not keep itself.</li></ul> |
| **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:
Expand Down Expand Up @@ -301,7 +301,7 @@ The device exposes a REST API at `http://<device-ip>/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)
Expand Down
4 changes: 2 additions & 2 deletions README.pt-BR.md
Original file line number Diff line number Diff line change
Expand Up @@ -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** | <ul><li>`release`: **estável**.</li><li>`alpha`: publicado e testado na bancada, com o LCD 16×2 incluído desde 26/09/2026.</li><li>`air`: **experimental**. O único soak longo dele falhou: um sono no ciclo 119 nunca acordou (F28). Um watchdog ao longo do wake hoje mitiga o problema; a causa raiz não foi confirmada.</li><li>`releasetwo`: **nova** na v2.11.0, testada na bancada num Pico 2 W (RP2350 A2). Atualiza pelo ar no outro slot, em teste, e volta sozinha quando a atualização não se confirma.</li></ul> |
| **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:
Expand Down Expand Up @@ -299,7 +299,7 @@ O aparelho expõe uma API REST em `http://<ip-do-dispositivo>/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)
Expand Down
10 changes: 5 additions & 5 deletions docs/analysis/PLANO_STABLE.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,14 +84,14 @@ de recebê-la pelo ar: o aparelho a recusa no stage, na validação e no aplicad
<!-- ota-headroom:begin -->
| 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 | — |
<!-- ota-headroom:end -->

Expand Down
26 changes: 23 additions & 3 deletions src/DisplayManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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( );
Expand Down
8 changes: 5 additions & 3 deletions src/DisplayManager.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
9 changes: 4 additions & 5 deletions src/DisplayManager_Calibration.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
30 changes: 27 additions & 3 deletions src/DisplayManager_Touch.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand All @@ -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;
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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;
Expand Down
23 changes: 23 additions & 0 deletions src/SystemDefs_Time.h
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down
Loading
Loading