From 7f290dceacbfda7698edb9fc4d114c396c8266f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=82ngelo=20Mois=C3=A9s=20Alves?= Date: Sun, 4 Oct 2026 23:39:07 -0300 Subject: [PATCH 1/4] fix(panel): a PIN shorter than a stricter policy still identifies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Raising the PIN minimum marks every account holding a PIN to choose a new one (markPinsBelowPolicy), so that an account whose PIN is now too short identifies with it and goes to the change. The keypad refused that entry as too short, against the new minimum, before Core 0 saw it, and the account was out of the panel. Core 0 resolves any length from PIN_LEN_MIN. PinKb::entryMinLen( ) is the shortest entry a screen takes: PIN_LEN_MIN to prove a PIN, the policy's minimum to choose one. pinSubmit( ) and the row of dots use it. That refusal was also the one thing that held a PIN the web set below the minimum to the policy: the web does not check it (finding 7 of docs/analysis/PLANO_REVISAO_EXTERNA.md), and no tightening marks such a PIN. So PinKb::changeAfterProof( ) sends a PIN that proves shorter than the policy to the change, marked or not. A tap is a character, so the entry's length is the PIN's. An entry of 4 characters or more is now a real attempt, and a wrong one counts on the account's ladder; one shorter than the policy used to cost nothing. Narrowing the alphabet or lowering the length ceiling still strands a PIN with a letter that left the cards, or longer than the ceiling: the cards cannot offer the one, and Core 0 cannot resolve the other in time. Manual chapters 8 and 30 say so. Tests first, in the native suite (286 of 286, under ASan and UBSan too): - test_identification_takes_a_pin_shorter_than_the_policy failed with main's rule ("Expected 4 Was 5. identifying"); - test_a_pin_below_the_policy_is_changed_once_it_proves failed with the mark alone ("shorter than the policy, unmarked"). On the rig, pico_w_test from the S1 dump. The policy went from 4/3/0 to 5/3/0 over pshort (PIN 1357) and plong (24680); the web then gave pweb 2468: - main 8388f32: pshort and pweb got "PIN muito curto (mín. 4)" and no SEC_PIN_OK, so they stayed out; plong identified and went to Novo PIN; - this branch: all three identified and went to Novo PIN. pshort chose 13579 and got the menu, and from then on its new PIN opened the menu directly. Manual chapters 8 and 30 describe identification and the policy as they are now; MANUAL.pt-BR.html regenerated. The READMEs count the two cases. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01BniVDEDSHGQgcoqmCRn5um --- README.es-ES.md | 4 ++-- README.md | 4 ++-- README.pt-BR.md | 4 ++-- docs/MANUAL.pt-BR.html | 37 +++++++++++++++++++----------- docs/_manual/cap08-contas.md | 9 +++++--- docs/_manual/cap30-solucao.md | 3 ++- src/AppManager_Panel.cpp | 7 ++++-- src/DisplayManager.h | 3 +++ src/DisplayManager_Users.cpp | 17 +++++++++----- src/PinKeypad.h | 22 ++++++++++++++++++ test/test_validators/test_main.cpp | 34 +++++++++++++++++++++++++++ 11 files changed, 112 insertions(+), 32 deletions(-) diff --git a/README.es-ES.md b/README.es-ES.md index da157bc3..fdf2f068 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.0** (04/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`; el configurador todavía no compila para la Pico 2 W. | | **Madurez** | | -| **Pruebas** | Cada pull request ejecuta 636 casos de test en el host en 9 suites, 60 s de fuzzing y análisis estático, y compila las siete 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 638 casos de test en el host en 9 suites, 60 s de fuzzing y análisis estático, y compila las siete 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 (284 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 (286 casos) pio test -e native_history_v5 # códec del histórico V5 (62) pio test -e native_cli # parser de la CLI (33) pio test -e native_logpolicy # persistencia de log por transición, franjas de la autopsia, lectura del táctil (57) diff --git a/README.md b/README.md index d288742c..8f1b35f7 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ They share one core: | **Current release** | **v2.11.0** (2026-10-04); 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`; the configurator does not build for the Pico 2 W yet. | | **Maturity** |
  • `release`: **stable**.
  • `alpha`: published and bench-tested, its 16×2 LCD included since 2026-09-26.
  • `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.
  • `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.
| -| **Tests** | Every pull request runs 636 host test cases in 9 suites, 60 s of fuzzing and static analysis, and builds all seven 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 638 host test cases in 9 suites, 60 s of fuzzing and static analysis, and builds all seven 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 (284 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 (286 cases) pio test -e native_history_v5 # V5 history codec (62) pio test -e native_cli # CLI parser (33) pio test -e native_logpolicy # edge-triggered log persistence, autopsy bands, touch wake (57) diff --git a/README.pt-BR.md b/README.pt-BR.md index f462d5c6..7ea25378 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.0** (04/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`; o configurador ainda não compila para o Pico 2 W. | | **Maturidade** |
  • `release`: **estável**.
  • `alpha`: publicado e testado na bancada, com o LCD 16×2 incluído desde 26/09/2026.
  • `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.
  • `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.
| -| **Testes** | Todo pull request roda 636 casos de teste no host em 9 suítes, 60 s de fuzzing e análise estática, e compila as sete 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 638 casos de teste no host em 9 suítes, 60 s de fuzzing e análise estática, e compila as sete 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 (284 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 (286 casos) pio test -e native_history_v5 # codec do histórico V5 (62) pio test -e native_cli # parser do CLI (33) pio test -e native_logpolicy # persistência de log por transição, faixas da autópsia, leitura do toque (57) diff --git a/docs/MANUAL.pt-BR.html b/docs/MANUAL.pt-BR.html index 00339d8c..8322a7f2 100644 --- a/docs/MANUAL.pt-BR.html +++ b/docs/MANUAL.pt-BR.html @@ -4065,7 +4065,7 @@

PIN pela web

Pela web, o PIN tem sempre de 4 a 8 dígitos, qualquer que seja a política de PIN do aparelho. O aparelho recusa um PIN que outra conta já use.

Motivos de recusa

A página não mostra estas recusas: a gravação continua e a ação recusada não aparece. Um script que grava contas pela interface de programação recebe a lista em rejected (capítulo 26).

@@ -4213,7 +4213,7 @@

O teclado

Figura 60. O teclado ordenado alfanumérico, com a janela de um grupo aberta.
Figura 61. O teclado numérico ordenado, usado para definir um PIN.

PIN errado e bloqueios

-

Um PIN errado faz o painel mostrar PIN inválido!, tocar o som de erro e distribuir os cartões de novo. Um PIN mais curto que o mínimo da política não chega a ser conferido: o painel mostra PIN muito curto (mín. 4).

+

Um PIN errado faz o painel mostrar PIN inválido!, tocar o som de erro e distribuir os cartões de novo. Para identificar, o painel confere qualquer PIN a partir de 4 caracteres, mesmo abaixo do mínimo da política: um PIN assim pode ter sido definido antes de a política mudar, ou pela web. Se ele estiver certo, a conta vai direto para a troca (O PIN de fábrica). Com menos de 4 caracteres, o painel mostra PIN muito curto (mín. 4) e não confere. Até a v2.11.0, um PIN mais curto que o mínimo da política nunca era conferido, e a conta ficava sem o painel.

As tentativas erradas são contadas por conta:

@@ -4356,7 +4356,11 @@

A política de PIN

Ao mudar um ajuste, o painel corrige os outros para uma combinação possível. SAIR descarta. A política vale na próxima tela de PIN, sem reiniciar, e o log registra o código 459, Política de PIN alterada pelo painel.

Na web. Os três campos ficam na seção Hardware da página Configurações, que exige a permissão Sistema (capítulo 6). A dica do tamanho mínimo mostra a faixa permitida, como 4 – 8. Pela web, a mudança vale com Aplicar agora, sem reiniciar (até a v2.9.0, exigia Salvar e reiniciar). O log registra o mesmo código 459, com a marca [web].

-

Quando a política fica mais exigente em qualquer ajuste (mínimo maior, alfabeto menor ou teto menor), toda conta com PIN é marcada para trocá-lo. A conta continua se identificando com o PIN antigo e é levada a Novo PIN logo depois (O PIN de fábrica). Afrouxar a política não marca ninguém.

+

Quando a política fica mais exigente em qualquer ajuste (mínimo maior, alfabeto menor ou teto menor), toda conta com PIN é marcada para trocá-lo, e é levada a Novo PIN logo depois de se identificar (O PIN de fábrica). Afrouxar a política não marca ninguém.

+
    +
  • Mínimo maior: a conta se identifica com o PIN antigo, mesmo mais curto que o mínimo novo. Até a v2.11.0, o teclado recusava esse PIN como curto, e a conta ficava sem o painel.
  • +
  • Alfabeto menor ou teto menor: um PIN antigo com uma letra que saiu dos cartões, ou mais longo que o teto novo, não pode mais ser digitado: o teto não sobe para um PIN antigo, porque é o tempo de conferir. Defina um PIN novo para essa conta no item Usuários do painel, na página Usuários da web ou no console.
  • +
Figura 66. A tela Segurança do PIN, com a política de fábrica: tamanho mínimo 4, 3 caracteres por tecla (máximo 8) e só dígitos.
Figura 67. Os campos da política de PIN na página Configurações.

Usuários no painel

@@ -23363,51 +23367,56 @@

Contas, senhas e PIN

- - - + + + + + + + + - + - + - + - + - + - + - + - + diff --git a/docs/_manual/cap08-contas.md b/docs/_manual/cap08-contas.md index 8f2e47c5..d7d72781 100644 --- a/docs/_manual/cap08-contas.md +++ b/docs/_manual/cap08-contas.md @@ -201,7 +201,7 @@ O reset troca só a senha. As permissões e o PIN continuam como estavam. Uma se Pela web, o PIN tem sempre de 4 a 8 dígitos, qualquer que seja a política de PIN do aparelho. O aparelho recusa um PIN que outra conta já use. ::: atencao -**Um PIN definido pela web pode não servir no painel.** Com uma política de mínimo acima de 4, um PIN mais curto que o mínimo é aceito pela web, mas o teclado do painel o recusa com **PIN muito curto (mín. 4)** antes de conferir. Com a política de letras ou de mais de 8 caracteres, defina o PIN no painel ([O próprio PIN](#cap-08-proprio-pin)). +**Um PIN definido pela web pode não seguir a política do painel.** Com uma política de mínimo acima de 4, a web aceita um PIN mais curto que o mínimo. Ele identifica a conta no painel, e o painel pede um PIN novo logo em seguida, como na troca obrigatória ([O PIN de fábrica](#cap-08-pin-fabrica)). Até a v2.11.0, o teclado do painel recusava esse PIN com **PIN muito curto (mín. 4)** antes de conferir. Com a política de letras ou de mais de 8 caracteres, defina o PIN no painel ([O próprio PIN](#cap-08-proprio-pin)). ::: ### Motivos de recusa {#cap-08-recusas} @@ -299,7 +299,7 @@ O teclado numérico ordenado, usado para definir um PIN. ### PIN errado e bloqueios {#cap-08-bloqueios} -Um PIN errado faz o painel mostrar **PIN inválido!**, tocar o som de erro e distribuir os cartões de novo. Um PIN mais curto que o mínimo da política não chega a ser conferido: o painel mostra **PIN muito curto (mín. 4)**. +Um PIN errado faz o painel mostrar **PIN inválido!**, tocar o som de erro e distribuir os cartões de novo. Para identificar, o painel confere qualquer PIN a partir de 4 caracteres, mesmo abaixo do mínimo da política: um PIN assim pode ter sido definido antes de a política mudar, ou pela web. Se ele estiver certo, a conta vai direto para a troca ([O PIN de fábrica](#cap-08-pin-fabrica)). Com menos de 4 caracteres, o painel mostra **PIN muito curto (mín. 4)** e não confere. Até a v2.11.0, um PIN mais curto que o mínimo da política nunca era conferido, e a conta ficava sem o painel. As tentativas erradas são contadas por conta: @@ -388,7 +388,10 @@ Ao mudar um ajuste, o painel corrige os outros para uma combinação possível. **Na web.** Os três campos ficam na seção **Hardware** da página **Configurações**, que exige a permissão **Sistema** ([capítulo 6](#cap-06-hardware)). A dica do tamanho mínimo mostra a faixa permitida, como `4 – 8`. Pela web, a mudança vale com **Aplicar agora**, sem reiniciar (até a v2.9.0, exigia **Salvar e reiniciar**). O log registra o mesmo código 459, com a marca `[web]`. -Quando a política fica mais exigente em qualquer ajuste (mínimo maior, alfabeto menor ou teto menor), toda conta com PIN é marcada para trocá-lo. A conta continua se identificando com o PIN antigo e é levada a **Novo PIN** logo depois ([O PIN de fábrica](#cap-08-pin-fabrica)). Afrouxar a política não marca ninguém. +Quando a política fica mais exigente em qualquer ajuste (mínimo maior, alfabeto menor ou teto menor), toda conta com PIN é marcada para trocá-lo, e é levada a **Novo PIN** logo depois de se identificar ([O PIN de fábrica](#cap-08-pin-fabrica)). Afrouxar a política não marca ninguém. + +- **Mínimo maior:** a conta se identifica com o PIN antigo, mesmo mais curto que o mínimo novo. Até a v2.11.0, o teclado recusava esse PIN como curto, e a conta ficava sem o painel. +- **Alfabeto menor ou teto menor:** um PIN antigo com uma letra que saiu dos cartões, ou mais longo que o teto novo, não pode mais ser digitado: o teto não sobe para um PIN antigo, porque é o tempo de conferir. Defina um PIN novo para essa conta no item **Usuários** do painel, na página **Usuários** da web ou no console. ::: {.figura #fig-08-politica tipo="tft" arquivo="08-politica.png" captura="menu > Segurança do PIN, sessão admin; política de fábrica; linha Teclado selecionada"} A tela Segurança do PIN, com a política de fábrica: tamanho mínimo 4, 3 caracteres por tecla (máximo 8) e só dígitos. diff --git a/docs/_manual/cap30-solucao.md b/docs/_manual/cap30-solucao.md index 777981b6..d4408433 100644 --- a/docs/_manual/cap30-solucao.md +++ b/docs/_manual/cap30-solucao.md @@ -139,7 +139,8 @@ Detalhes no [capítulo 8](#cap-08). | Entro na web e vejo só `Access Denied` | A conta não tem a permissão **Painel** | Digite o endereço de uma página permitida, ou recrie a conta com **Painel** | | Backup e atualização de firmware recusados para uma conta com todas as permissões | Essas ações exigem o nível de administrador completo | Use a conta `admin` | | Minha conta não aparece em **Quem está usando o painel?** | A conta não tem PIN | Defina um PIN pelo botão **PIN** da página **Usuários** ou no item **Usuários** do painel | -| **PIN muito curto (mín. 4)** com um PIN definido pela web | A política exige mais caracteres que o PIN tem; a web aceita 4 a 8 dígitos sem olhar a política | Defina o PIN no painel, no item **Alterar Senha** ou em **Usuários** | +| O painel pede **Novo PIN** logo depois de um PIN definido pela web | O PIN é mais curto que o mínimo da política; a web aceita 4 a 8 dígitos sem olhar a política | Escolha um PIN que siga a política. Até a v2.11.0, o painel recusava esse PIN com **PIN muito curto (mín. 4)** | +| Um PIN antigo não pode mais ser digitado depois de mudar a política | A política tirou as letras dos cartões, ou o teto ficou menor que o PIN | Defina um PIN novo para a conta no item **Usuários** do painel, na página **Usuários** ou no console | | **PIN já em uso!** | Outra conta tem esse PIN | Escolha outro PIN | | Conta aparece com **bloqueado** no painel | Seis PINs errados seguidos dessa conta | Reinicie o aparelho ou use outra conta | | Tela vermelha **ACESSO BLOQUEADO** | 20 PINs errados desde a última entrada certa | Reinicie o aparelho | diff --git a/src/AppManager_Panel.cpp b/src/AppManager_Panel.cpp index 5eca5828..318aec80 100644 --- a/src/AppManager_Panel.cpp +++ b/src/AppManager_Panel.cpp @@ -165,8 +165,11 @@ void AppManager::panelIdentify( ) { * shorter than the new minimum or it uses characters the new alphabet does * not have. The account still identifies (its digest did not change), and * the first thing it does is choose a PIN that complies. The admin's factory - * "1234" arrives here the same way. */ - if (_storageMgr->pinMustChange(slot)) { + * "1234" arrives here the same way. So does a PIN that proved shorter than + * the policy without a mark, as one the web set (PinKb::changeAfterProof): + * the keypad takes any length since 2026-10-04, and this is what still holds + * it to the policy. */ + if (PinKb::changeAfterProof(_storageMgr->pinMustChange(slot), n, _displayMgr->pinMinLen( ))) { _displayMgr->showPinEntry(DisplayManager::PIN_FOR_OWN, -1, true); LOG_CODE(LOG_WARN, "SEC", SEC_UNAUTHORIZED, (int)slot, TRL("PIN does not meet the policy; forcing change.")); diff --git a/src/DisplayManager.h b/src/DisplayManager.h index 1eb6875e..7d7972d7 100644 --- a/src/DisplayManager.h +++ b/src/DisplayManager.h @@ -606,6 +606,9 @@ class DisplayManager { uint8_t pinKeypadMode( ) const; uint8_t pinAlphabet( ) const; uint8_t pinMinLen( ) const; + /** The shortest entry this screen takes: the policy's minimum to choose + * a PIN, PIN_LEN_MIN to prove one (PinKb::entryMinLen). */ + uint8_t pinEntryMinLen( ) const; /** The policy editor (PERM_USER_MGR): length, keypad, alphabet. */ void showPinPolicy( ); #endif /* SIMUT_PANEL_PIN — an image with no touch panel proves nobody */ diff --git a/src/DisplayManager_Users.cpp b/src/DisplayManager_Users.cpp index d4e3f007..5bb18dcd 100644 --- a/src/DisplayManager_Users.cpp +++ b/src/DisplayManager_Users.cpp @@ -163,6 +163,9 @@ uint8_t DisplayManager::pinAlphabet( ) const { uint8_t DisplayManager::pinMinLen( ) const { uint8_t minLen, keypad, alphabet; pinPolicy(minLen, keypad, alphabet); return minLen; } +uint8_t DisplayManager::pinEntryMinLen( ) const { + return PinKb::entryMinLen(pinIsIdentifying( ), pinMinLen( )); +} /* The four cards, for `show display keypad`. The bench drives the panel from * outside and cannot find a digit whose position it was never told; the cards @@ -313,7 +316,7 @@ void DisplayManager::pinCancel( ) { /* OK from the keypad. For identification the PIN goes to Core 0 as is; for a * new PIN it is typed twice, compared here, and only then handed over. */ void DisplayManager::pinSubmit( ) { - if (_pinLen < pinMinLen( )) { + if (_pinLen < pinEntryMinLen( )) { _pinMsg = TR_PIN_TOO_SHORT; _errorSoundPending = true; _repaintSettings = true; @@ -492,11 +495,13 @@ void DisplayManager::drawPinDotsInto(GFXcanvas16* cv, int16_t oy) { cv->print(m); return; } - /* The row is as wide as the policy's minimum, or as what has been typed - * past it. Sixteen dots at the old 20-px pitch is 320 px, one wider than - * the safe area allows (measured 2026-09-20), so both pitch and radius - * step down past fourteen. */ - const int floorLen = (int)pinMinLen( ); + /* The row is as wide as the shortest entry this screen takes, or as what + * has been typed past it: the policy's minimum to choose a PIN, the + * absolute floor to prove one, which a PIN set under an older policy may + * be. Sixteen dots at the old 20-px pitch is 320 px, one wider than the + * safe area allows (measured 2026-09-20), so both pitch and radius step + * down past fourteen. */ + const int floorLen = (int)pinEntryMinLen( ); const int n = (_pinLen > floorLen) ? _pinLen : floorLen; const int spacing = dotSpacing(n); const int16_t r = dotRadius(n); diff --git a/src/PinKeypad.h b/src/PinKeypad.h index 858caa11..5e875774 100644 --- a/src/PinKeypad.h +++ b/src/PinKeypad.h @@ -116,6 +116,28 @@ inline uint8_t maxLenFor(uint8_t keypad) { return (keypad >= KB_MIN && keypad <= KB_MAX) ? LEN_MAX[keypad] : LEN_MAX[KB_SET3]; } +/** The shortest entry the keypad takes. Choosing a PIN follows the policy. + * Proving one takes any length a PIN may still have, from PIN_LEN_MIN up: + * a stricter policy marks every PIN to be changed (markPinsBelowPolicy) so + * that an account whose PIN is now too short identifies with it and is sent + * to the change, and Core 0 resolves any length from PIN_LEN_MIN. The + * keypad refused that entry as too short instead, and the account was out + * of the panel (on the rig, 2026-10-04: 4 digits under a minimum of 5). */ +inline uint8_t entryMinLen(bool identifying, uint8_t policyMin) { + return identifying ? PIN_LEN_MIN : policyMin; +} + +/** After a PIN is proved, must the account choose a new one before it goes + * on? When a stricter policy marked it, and whenever the PIN just proved is + * shorter than the policy's minimum: a tap is a character, so the entry's + * length is the PIN's. The second covers a PIN no tightening marked, as one + * the web set below the policy (the web does not check it), which the + * keypad's refusal used to hold to the policy and entryMinLen( ) now lets + * through. */ +inline bool changeAfterProof(bool marked, uint8_t entryLen, uint8_t policyMin) { + return marked || entryLen < policyMin; +} + /* ── The lockout ladder ────────────────────────────────────────────────── * Six failures take the ACCOUNT out until reboot; PANEL_FAIL_CEILING failures * across all accounts take the PANEL out. Both are needed: only-per-panel is diff --git a/test/test_validators/test_main.cpp b/test/test_validators/test_main.cpp index 4887e593..edd45bc1 100644 --- a/test/test_validators/test_main.cpp +++ b/test/test_validators/test_main.cpp @@ -4269,6 +4269,38 @@ static void test_alpha_marquee_blanks_nothing(void) { * The TFT top bar's format, now shared with the 16x2. The property that lets * the alpha show it at all: nothing a uint16_t holds is wider than the three * free columns of its second line. */ +/* ══ the PIN keypad's shortest entry ══════════════════════════════════════ + * + * A stricter policy marks every account holding a PIN to choose a new one + * (markPinsBelowPolicy), so that an account whose PIN is now shorter than the + * minimum identifies with it and is sent to the change. The keypad refused + * that entry as too short before Core 0 saw it: on the rig on 2026-10-04, a + * 4-digit PIN under a minimum of 5 could not identify at all. Proving a PIN + * takes any length a PIN may still have; choosing one follows the policy. + */ +static void test_identification_takes_a_pin_shorter_than_the_policy(void) { + using namespace PinKb; + for (uint8_t m = PIN_LEN_MIN; m <= 8; m++) { + TEST_ASSERT_EQUAL_UINT8_MESSAGE(PIN_LEN_MIN, entryMinLen(true, m), "identifying"); + TEST_ASSERT_EQUAL_UINT8_MESSAGE(m, entryMinLen(false, m), "choosing"); + } +} + +/* Proving a shorter PIN must not be the way around the policy. The keypad's + * refusal used to be the one thing that held a PIN the web set below the + * minimum to the policy (the web does not check it: finding 7 of + * docs/analysis/PLANO_REVISAO_EXTERNA.md), and no tightening marks such a + * PIN. A tap is a character, so the entry's length is the PIN's: a PIN + * shorter than the policy identifies and is sent straight to the change. */ +static void test_a_pin_below_the_policy_is_changed_once_it_proves(void) { + using namespace PinKb; + TEST_ASSERT_TRUE_MESSAGE(changeAfterProof(true, 6, 4), "marked by a tightening"); + TEST_ASSERT_TRUE_MESSAGE(changeAfterProof(false, 4, 5), "shorter than the policy, unmarked"); + TEST_ASSERT_TRUE_MESSAGE(changeAfterProof(true, 4, 5), "both"); + TEST_ASSERT_FALSE_MESSAGE(changeAfterProof(false, 5, 5), "at the minimum"); + TEST_ASSERT_FALSE_MESSAGE(changeAfterProof(false, 8, 4), "above it"); +} + static void test_pending_label_reads_like_the_top_bar(void) { char b[PENDING_LABEL_MAX]; pendingLabel(1, b, sizeof(b)); TEST_ASSERT_EQUAL_STRING("1", b); @@ -5065,6 +5097,8 @@ int main(int /*argc*/, char** /*argv*/) { RUN_TEST(test_alpha_marquee_fills_exactly_at_the_width); RUN_TEST(test_alpha_marquee_scrolls_and_wraps_through_the_gap); RUN_TEST(test_alpha_marquee_blanks_nothing); + RUN_TEST(test_identification_takes_a_pin_shorter_than_the_policy); + RUN_TEST(test_a_pin_below_the_policy_is_changed_once_it_proves); RUN_TEST(test_pending_label_reads_like_the_top_bar); RUN_TEST(test_pending_label_fits_three_columns); RUN_TEST(test_alpha_wifi_grows_left_to_right); From f86e9537004822e786f7ffac246f3992f87f9f9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=82ngelo=20Mois=C3=A9s=20Alves?= Date: Sun, 4 Oct 2026 23:39:10 -0300 Subject: [PATCH 2/4] fix(panel): the too-short PIN line says the policy's minimum MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "PIN too short (min 4)" said 4 under any policy: the number was written into the line, in the firmware and in both packs. On the rig, with the minimum at 5, Novo PIN answered three digits with "PIN muito curto (mín. 4)". The line carries "{n}" now, and display/CountInText.h puts the screen's shortest entry there: the policy's minimum to choose a PIN, 4 to prove one. Not printf on the line: a pack is uploaded, and a pack line is never a format string, so a "%s" in it prints as "%s". A pack older than the marker shows its line as it is. check_lang_packs.py, which runs before every build, now refuses a pack line that drops a marker the firmware's line has, or carries one it does not. Tests first: - test_count_in_text_puts_the_number_where_the_line_says, in the native suite, failed while the helper copied the line ("Expected 'PIN too short (min 5)' Was 'PIN too short (min {n})'"); 287 of 287 now; - with "{n}" in the firmware's English, the gate failed on both packs until their lines carried it. On the rig, this branch with the new pt-BR pack uploaded: "PIN muito curto (mín. 5)" (screenshot). Manual chapter 8's table says N; MANUAL.pt-BR.html regenerated. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01BniVDEDSHGQgcoqmCRn5um --- README.es-ES.md | 4 +-- README.md | 4 +-- README.pt-BR.md | 4 +-- data/lang/language_es-ES.lng | 2 +- data/lang/language_pt-BR.lng | 2 +- docs/MANUAL.pt-BR.html | 4 +-- docs/_manual/cap08-contas.md | 2 +- src/DisplayManager_Users.cpp | 8 +++++ src/DisplayManager_i18n.cpp | 2 +- src/display/CountInText.h | 34 +++++++++++++++++++ test/test_validators/test_main.cpp | 29 +++++++++++++++++ tools/check_lang_packs.py | 52 ++++++++++++++++++++++++++++++ 12 files changed, 135 insertions(+), 12 deletions(-) create mode 100644 src/display/CountInText.h diff --git a/README.es-ES.md b/README.es-ES.md index fdf2f068..c83bbb72 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.0** (04/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`; el configurador todavía no compila para la Pico 2 W. | | **Madurez** |
  • `release`: **estable**.
  • `alpha`: publicado y probado en el banco, con su LCD 16×2 incluido desde el 26/09/2026.
  • `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.
  • `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.
| -| **Pruebas** | Cada pull request ejecuta 638 casos de test en el host en 9 suites, 60 s de fuzzing y análisis estático, y compila las siete 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 639 casos de test en el host en 9 suites, 60 s de fuzzing y análisis estático, y compila las siete 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 (286 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 (287 casos) pio test -e native_history_v5 # códec del histórico V5 (62) pio test -e native_cli # parser de la CLI (33) pio test -e native_logpolicy # persistencia de log por transición, franjas de la autopsia, lectura del táctil (57) diff --git a/README.md b/README.md index 8f1b35f7..10c6fd6f 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ They share one core: | **Current release** | **v2.11.0** (2026-10-04); 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`; the configurator does not build for the Pico 2 W yet. | | **Maturity** |
  • `release`: **stable**.
  • `alpha`: published and bench-tested, its 16×2 LCD included since 2026-09-26.
  • `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.
  • `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.
| -| **Tests** | Every pull request runs 638 host test cases in 9 suites, 60 s of fuzzing and static analysis, and builds all seven 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 639 host test cases in 9 suites, 60 s of fuzzing and static analysis, and builds all seven 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 (286 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 (287 cases) pio test -e native_history_v5 # V5 history codec (62) pio test -e native_cli # CLI parser (33) pio test -e native_logpolicy # edge-triggered log persistence, autopsy bands, touch wake (57) diff --git a/README.pt-BR.md b/README.pt-BR.md index 7ea25378..4cc7a3b3 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.0** (04/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`; o configurador ainda não compila para o Pico 2 W. | | **Maturidade** |
  • `release`: **estável**.
  • `alpha`: publicado e testado na bancada, com o LCD 16×2 incluído desde 26/09/2026.
  • `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.
  • `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.
| -| **Testes** | Todo pull request roda 638 casos de teste no host em 9 suítes, 60 s de fuzzing e análise estática, e compila as sete 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 639 casos de teste no host em 9 suítes, 60 s de fuzzing e análise estática, e compila as sete 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 (286 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 (287 casos) pio test -e native_history_v5 # codec do histórico V5 (62) pio test -e native_cli # parser do CLI (33) pio test -e native_logpolicy # persistência de log por transição, faixas da autópsia, leitura do toque (57) diff --git a/data/lang/language_es-ES.lng b/data/lang/language_es-ES.lng index ed3cfcd9..f938261a 100644 --- a/data/lang/language_es-ES.lng +++ b/data/lang/language_es-ES.lng @@ -131,7 +131,7 @@ SEGUIR Ingrese su PIN Nuevo PIN Confirme el PIN -PIN muy corto (mín. 4) +PIN muy corto (mín. {n}) ¡Los PIN no coinciden! ¡PIN ya en uso! ¡PIN guardado! diff --git a/data/lang/language_pt-BR.lng b/data/lang/language_pt-BR.lng index a292d185..141aa978 100644 --- a/data/lang/language_pt-BR.lng +++ b/data/lang/language_pt-BR.lng @@ -131,7 +131,7 @@ SEGUIR Digite seu PIN Novo PIN Confirme o PIN -PIN muito curto (mín. 4) +PIN muito curto (mín. {n}) Os PINs não coincidem! PIN já em uso! PIN salvo! diff --git a/docs/MANUAL.pt-BR.html b/docs/MANUAL.pt-BR.html index 8322a7f2..1a1f718c 100644 --- a/docs/MANUAL.pt-BR.html +++ b/docs/MANUAL.pt-BR.html @@ -4283,8 +4283,8 @@

O próprio PIN

- - + + diff --git a/docs/_manual/cap08-contas.md b/docs/_manual/cap08-contas.md index d7d72781..34413800 100644 --- a/docs/_manual/cap08-contas.md +++ b/docs/_manual/cap08-contas.md @@ -349,7 +349,7 @@ Qualquer conta identificada troca o próprio PIN pelo item **Alterar Senha** do |---|---| | **PIN salvo!** | Gravado; vale na próxima identificação | | **Os PINs não coincidem!** | As duas digitações diferem; o painel volta a **Novo PIN** | -| **PIN muito curto (mín. 4)** | Menos caracteres que o mínimo da política | +| **PIN muito curto (mín. N)** | Menos caracteres que o mínimo da política, que a mensagem diz no lugar de N. Até a v2.11.0, ela dizia 4 com qualquer mínimo | | **PIN inválido!** | Fora da política | | **PIN já em uso!** | Outra conta já tem esse PIN. Nada é gravado | diff --git a/src/DisplayManager_Users.cpp b/src/DisplayManager_Users.cpp index 5bb18dcd..00867b22 100644 --- a/src/DisplayManager_Users.cpp +++ b/src/DisplayManager_Users.cpp @@ -30,6 +30,7 @@ #include "UiWidgets.h" #include "SystemDefs_Validate.h" /* PIN_MIN_LEN / PIN_MAX_LEN */ #include "StorageManager.h" /* userHasPin */ +#include "display/CountInText.h" /* the minimum inside the too-short line */ #include "sensors/SensorChannelTable.h" /* Geometry and character set: PinKeypad.h, shared with the CLI readout. */ @@ -490,6 +491,13 @@ void DisplayManager::drawPinDotsInto(GFXcanvas16* cv, int16_t oy) { cv->setFont(&simutFont9pt); cv->setTextColor(err ? C_TEMP_HOT : C_TEXT_SUB); String m = tr(err ? _pinMsg : TR_LOADING); + if (_pinMsg == TR_PIN_TOO_SHORT) { + /* The line says where its minimum goes ("{n}"); it used to carry a + * 4 that no policy could change. */ + char line[64]; + countInText(m.c_str( ), pinEntryMinLen( ), line, sizeof(line)); + m = line; + } cv->getTextBounds(m, 0, 0, &bx, &by, &bw, &bh); cv->setCursor((320 - bw) / 2, (int16_t)(y + 19)); cv->print(m); diff --git a/src/DisplayManager_i18n.cpp b/src/DisplayManager_i18n.cpp index 8fe628de..177151c7 100644 --- a/src/DisplayManager_i18n.cpp +++ b/src/DisplayManager_i18n.cpp @@ -74,7 +74,7 @@ static const char* const DICTIONARY_EN[TR_KEYS_COUNT] = { "Enter your PIN", "New PIN", "Confirm PIN", - "PIN too short (min 4)", + "PIN too short (min {n})", "PINs don't match!", "PIN already in use!", "PIN saved!", diff --git a/src/display/CountInText.h b/src/display/CountInText.h new file mode 100644 index 00000000..e3737774 --- /dev/null +++ b/src/display/CountInText.h @@ -0,0 +1,34 @@ +/** + * @file display/CountInText.h + * @brief A translated line with a number in it. + * + * The panel's lines come from a language pack, which an administrator + * uploads. A line that needs a number carries "{n}" where the number goes, + * and this puts it there. Not snprintf on the line: a pack line is never a + * format string, so a "%s" in it prints as "%s" instead of reading whatever + * the stack holds. A line without the marker comes back as it is, which is + * what a pack older than the marker shows. Header-only, so a host test + * reaches it (test_validators). + * + * @project SIMUT — Integrated Universal Monitoring and Telemetry System + * @license MIT License + */ + +#pragma once + +#include +#include +#include + +/** `text` with its first "{n}" replaced by `n`, into `out`, cut to `cap`. */ +inline void countInText(const char* text, unsigned n, char* out, size_t cap) { + if (!out || cap == 0) return; + out[0] = '\0'; + if (!text) return; + const char* at = strstr(text, "{n}"); + if (!at) { + snprintf(out, cap, "%s", text); + return; + } + snprintf(out, cap, "%.*s%u%s", (int)(at - text), text, n, at + 3); +} diff --git a/test/test_validators/test_main.cpp b/test/test_validators/test_main.cpp index edd45bc1..efdedc8e 100644 --- a/test/test_validators/test_main.cpp +++ b/test/test_validators/test_main.cpp @@ -51,6 +51,7 @@ #include "display/SettingsMenu.h" /* the panel's Settings menu: rows, order */ #include "PasswordCheck.h" /* one derivation per password check */ #include "SessionCheck.h" /* a session against its live account */ +#include "display/CountInText.h" /* a number inside a translated line */ #include "PemBlocks.h" /* the PEM splitting POST /api/tls does */ #include "WebCommitSections.h" /* per-section authz for /api/commit_all */ #include "FsSecretPath.h" /* /config download guard (A-4) */ @@ -4301,6 +4302,33 @@ static void test_a_pin_below_the_policy_is_changed_once_it_proves(void) { TEST_ASSERT_FALSE_MESSAGE(changeAfterProof(false, 8, 4), "above it"); } +/* "PIN too short (min 4)" said 4 whatever the policy's minimum was: the + * number was written into the line, in the firmware and in both packs. The + * line carries "{n}" now, and the number is put there, never by handing the + * pack's line to printf. */ +static void test_count_in_text_puts_the_number_where_the_line_says(void) { + char b[48]; + countInText("PIN too short (min {n})", 5, b, sizeof(b)); + TEST_ASSERT_EQUAL_STRING("PIN too short (min 5)", b); + countInText("PIN muito curto (m\xC3\xADn. {n})", 12, b, sizeof(b)); + TEST_ASSERT_EQUAL_STRING("PIN muito curto (m\xC3\xADn. 12)", b); + /* a pack older than the marker: its line as it is */ + countInText("PIN muito curto (m\xC3\xADn. 4)", 5, b, sizeof(b)); + TEST_ASSERT_EQUAL_STRING("PIN muito curto (m\xC3\xADn. 4)", b); + /* a pack line is not a format string */ + countInText("%s%n {n} %d", 7, b, sizeof(b)); + TEST_ASSERT_EQUAL_STRING("%s%n 7 %d", b); + /* the first marker only */ + countInText("{n}{n}", 3, b, sizeof(b)); + TEST_ASSERT_EQUAL_STRING("3{n}", b); + /* cut to the buffer, and terminated */ + char s[8]; + countInText("PIN too short (min {n})", 5, s, sizeof(s)); + TEST_ASSERT_EQUAL_STRING("PIN too", s); + countInText(nullptr, 5, s, sizeof(s)); + TEST_ASSERT_EQUAL_STRING("", s); +} + static void test_pending_label_reads_like_the_top_bar(void) { char b[PENDING_LABEL_MAX]; pendingLabel(1, b, sizeof(b)); TEST_ASSERT_EQUAL_STRING("1", b); @@ -5099,6 +5127,7 @@ int main(int /*argc*/, char** /*argv*/) { RUN_TEST(test_alpha_marquee_blanks_nothing); RUN_TEST(test_identification_takes_a_pin_shorter_than_the_policy); RUN_TEST(test_a_pin_below_the_policy_is_changed_once_it_proves); + RUN_TEST(test_count_in_text_puts_the_number_where_the_line_says); RUN_TEST(test_pending_label_reads_like_the_top_bar); RUN_TEST(test_pending_label_fits_three_columns); RUN_TEST(test_alpha_wifi_grows_left_to_right); diff --git a/tools/check_lang_packs.py b/tools/check_lang_packs.py index dc6c4eed..f58589bb 100644 --- a/tools/check_lang_packs.py +++ b/tools/check_lang_packs.py @@ -10,6 +10,11 @@ Also refuses a key inserted anywhere but the end while packs are unchanged, since that shifts every string after it without changing any line count. + +And a line whose marker does not match the firmware's own: the panel puts a +number where a line says "{n}" (display/CountInText.h), so a pack line that +drops the marker shows no number, or a stale one written into the text, which +is how "PIN too short (min 4)" said 4 under any policy until 2026-10-04. """ import json import os @@ -27,6 +32,7 @@ ROOT = Path(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) HEADER = ROOT / "src" / "DisplayManager.h" +I18N = ROOT / "src" / "DisplayManager_i18n.cpp" PARSER = ROOT / "src" / "DisplayManager_LangParser.cpp" PACKS = sorted((ROOT / "data" / "lang").glob("*.lng")) @@ -132,6 +138,46 @@ def enum_keys(): return keys[:-1] +def english_dict(): + """DICTIONARY_EN, the firmware's own lines, in LangKey order. An entry is + every literal up to the next comma, so adjacent literals join as the + compiler joins them. The markers are all this is read for, so escapes are + left as they are written.""" + src = I18N.read_text(encoding="utf-8") + m = re.search(r"DICTIONARY_EN\[TR_KEYS_COUNT\]\s*=\s*\{(.*?)\n\};", src, re.S) + if not m: + raise SystemExit("check_lang_packs: DICTIONARY_EN not found in DisplayManager_i18n.cpp") + body = re.sub(r"/\*.*?\*/", "", m.group(1), flags=re.S) + body = re.sub(r"//[^\n]*", "", body) + out, cur = [], None + for tok in re.finditer(r'"((?:[^"\\]|\\.)*)"|,', body): + if tok.group(0) == ",": + if cur is not None: + out.append(cur) + cur = None + else: + cur = (cur or "") + tok.group(1) + if cur is not None: + out.append(cur) + return out + + +MARKER = re.compile(r"\{[a-z]+\}") + + +def check_markers(pack, lines, english, keys): + ok = True + for i, key in enumerate(keys): + want, have = sorted(MARKER.findall(english[i])), sorted(MARKER.findall(lines[i])) + if want != have: + print(f"[lang-packs] FAIL {pack.name}: {key} carries {have or 'no marker'}, " + f"the firmware's line {want or 'none'} — the panel puts the number " + f"where the marker is, so this line would show none, or a stale one", + file=sys.stderr) + ok = False + return ok + + def dict_lines(path): """Lines of the @DICT block. @@ -308,6 +354,10 @@ def main(): dict_warn_at = int(dict_ceil * CEIL_WARN_FRAC) trl_live = trl_literals() wkeys, wprefixes = web_keys() + english = english_dict() + if len(english) != want: + raise SystemExit(f"check_lang_packs: read {len(english)} lines out of DICTIONARY_EN, " + f"TR_KEYS_COUNT is {want}: the reader is wrong, not the table") failed = False for pack in PACKS: size = pack.stat().st_size @@ -365,6 +415,8 @@ def main(): failed = True else: print(f"[lang-packs] OK {pack.name}: {len(lines)} strings") + if not check_markers(pack, lines, english, keys): + failed = True # @DICT is the only section the count check can see. @TRL and # @WEBDICT rot differently — a key goes missing and just that one # string falls back to English, which no count catches. From 2fcbf325e4b1fe00dd7e0a7e0f07b1036b183a3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=82ngelo=20Mois=C3=A9s=20Alves?= Date: Sun, 4 Oct 2026 23:39:10 -0300 Subject: [PATCH 3/4] build: flash budgets for the PIN entry fix All seven images built from this branch, on main 8388f32: - release, test and asserts: 'measured' and the .bin +152 B; - test_https and pico2_w_release: +160 B; - alpha and Air: unchanged, as neither has a panel PIN. Each budget that moves moves by its image's growth, which keeps its margin: release 1,021,012 -> 1,021,164, test 1,025,660 -> 1,025,812, test_https 1,030,492 -> 1,030,652, asserts 1,023,268 -> 1,023,420, pico2_w_release 997,128 -> 997,288. On release: drawPinDotsInto( ) +104 B, the marker filled in; the new pinEntryMinLen( ) +28 B; panelIdentify( ) +8 B. The OTA headroom table of docs/analysis/PLANO_STABLE.md section 3.1 follows: release 7,019 B, test 2,443 B (already under one sector, and warned), asserts 4,723 B, pico2_w_release 571,343 B against its slot. check_flash_budget.py passes on the seven builds, and --table passes. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01BniVDEDSHGQgcoqmCRn5um --- docs/analysis/PLANO_STABLE.md | 8 ++++---- tools/flash_budget.json | 32 ++++++++++++++++---------------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/docs/analysis/PLANO_STABLE.md b/docs/analysis/PLANO_STABLE.md index e0a96b72..6f8878d7 100644 --- a/docs/analysis/PLANO_STABLE.md +++ b/docs/analysis/PLANO_STABLE.md @@ -84,12 +84,12 @@ 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.033.213 | 7.171 | +| `pico_w_release` | 1.033.365 | 7.019 | | `pico_w_air` | 1.031.485 | 8.899 | | `pico_w_alpha` | 990.621 | 49.763 | -| `pico2_w_release` | 997.265 | 571.503 | -| `pico_w_test` | 1.037.789 | 2.595 | -| `pico_w_asserts` | 1.035.509 | 4.875 | +| `pico2_w_release` | 997.425 | 571.343 | +| `pico_w_test` | 1.037.941 | 2.443 | +| `pico_w_asserts` | 1.035.661 | 4.723 | | `pico_w_test_https` | isenta | — | diff --git a/tools/flash_budget.json b/tools/flash_budget.json index ab1b53a5..3f7616a5 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-04 fix/high-findings-batch: findings 1, 8, 9 and 25 of docs/analysis/PLANO_REVISAO_EXTERNA.md, and the Files page's reason for v=6. 'measured' and the .bin +280 B on release, +272 B on test and asserts, +264 B on pico2_w_release; 'measured' +208 B on test_https, +40 B on alpha and +48 B on air, whose .bin keep their size (inside the 4 KiB pad before .data). The budgets of release, test_https, asserts and pico2_w_release move by their growth, which keeps their margins; test, alpha and air stay under theirs.", - "measured_at_commit": "2026-10-04 fix/high-findings-batch on main e5438e7 (#254): the seven images built from this branch. measured is the zopfli image PlatformIO prints.", + "measured_on": "2026-10-04 fix/pin-entry-below-policy: a PIN shorter than a stricter policy identifies at the panel and is sent to the change, and the too-short line says the policy's minimum. 'measured' and the .bin +152 B on release, test and asserts, +160 B on test_https and pico2_w_release; alpha and air do not change (no panel PIN). The budgets of release, test, test_https, asserts and pico2_w_release move by their growth, which keeps their margins.", + "measured_at_commit": "2026-10-04 fix/pin-entry-below-policy on main 8388f32 (#255): the seven images built from this branch. measured is the zopfli image PlatformIO prints.", "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": 1021012, - "measured": 1020940, - "bin": 1032972 + "budget": 1021164, + "measured": 1021092, + "bin": 1033124 }, "pico_w_test": { - "budget": 1025660, - "measured": 1025516, - "bin": 1037548 + "budget": 1025812, + "measured": 1025668, + "bin": 1037700 }, "pico_w_test_https": { - "budget": 1030492, - "measured": 1030492 + "budget": 1030652, + "measured": 1030652 }, "pico_w_asserts": { - "budget": 1023268, - "measured": 1023236, - "bin": 1035268 + "budget": 1023420, + "measured": 1023388, + "bin": 1035420 }, "pico_w_alpha": { "budget": 975276, @@ -571,9 +571,9 @@ "bin": 1031244 }, "pico2_w_release": { - "budget": 997128, - "measured": 997000, - "bin": 997024, + "budget": 997288, + "measured": 997160, + "bin": 997184, "ceiling": 3145728, "slot": 1568768 } From 7eac7b194e6f354fa18cdb418ffcca5440ad9e03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=82ngelo=20Mois=C3=A9s=20Alves?= Date: Sun, 4 Oct 2026 23:46:42 -0300 Subject: [PATCH 4/4] docs: the READMEs quote the release at 97.8 % of its slot The +152 B of this PR take pico_w_release from 1,020,940 to 1,021,092 used bytes of the 1,044,480 B slot: 97.75 % to 97.76 %, which rounds up. check_readme_numbers.py failed in CI on the three READMEs; it passed locally only because it ran before the flash record moved. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01BniVDEDSHGQgcoqmCRn5um --- README.es-ES.md | 2 +- README.md | 2 +- README.pt-BR.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.es-ES.md b/README.es-ES.md index c83bbb72..8758307a 100644 --- a/README.es-ES.md +++ b/README.es-ES.md @@ -262,7 +262,7 @@ simut/ > clave por dispositivo que se muestra en la consola y, donde la hay, en la > pantalla. Ver [SECURITY.md](SECURITY.md) §2 y §8. -> No hay entorno de depuración. `pico_w_debug` se eliminó en la v2.4.1 tras no enlazar nunca: en `-Og` la imagen desbordaba el slot de 1020 KB en ~100 KB. La flash va justa. La imagen release usa el 97,7 % del slot de programa de 1.044.480 B (el valor medido vive en `tools/flash_budget.json`), y el CI comprueba cada `.bin`, con sus 241 B de firma, contra el techo de actualización por el aire, de 1.040.384 B. Un objetivo de GDB habría que montarlo recortando funcionalidades. Para el tripwire de concurrencia en hardware, usa `pico_w_asserts`. +> No hay entorno de depuración. `pico_w_debug` se eliminó en la v2.4.1 tras no enlazar nunca: en `-Og` la imagen desbordaba el slot de 1020 KB en ~100 KB. La flash va justa. La imagen release usa el 97,8 % del slot de programa de 1.044.480 B (el valor medido vive en `tools/flash_budget.json`), y el CI comprueba cada `.bin`, con sus 241 B de firma, contra el techo de actualización por el aire, de 1.040.384 B. Un objetivo de GDB habría que montarlo recortando funcionalidades. Para el tripwire de concurrencia en hardware, usa `pico_w_asserts`. ### Flags de compilación - `-Os` — optimización por tamaño diff --git a/README.md b/README.md index 10c6fd6f..4e81fbdf 100644 --- a/README.md +++ b/README.md @@ -260,7 +260,7 @@ simut/ > The setup access point is WPA2 on every image, with a per-device key shown on > the console and, where there is one, on the display. See [SECURITY.md](SECURITY.md) §2 and §8. -> There is no debug environment. `pico_w_debug` was removed in v2.4.1 after never once linking: at `-Og` the image overflowed the 1020 KB app slot by ~100 KB. Flash is tight. The release image uses 97.7 % of the 1,044,480 B program slot (`tools/flash_budget.json` keeps the measured value), and CI checks every `.bin`, with its 241 B signature, against the 1,040,384 B over-the-air ceiling. A GDB target would have to be built by cutting features. For the concurrency tripwire on hardware, use `pico_w_asserts`. +> There is no debug environment. `pico_w_debug` was removed in v2.4.1 after never once linking: at `-Og` the image overflowed the 1020 KB app slot by ~100 KB. Flash is tight. The release image uses 97.8 % of the 1,044,480 B program slot (`tools/flash_budget.json` keeps the measured value), and CI checks every `.bin`, with its 241 B signature, against the 1,040,384 B over-the-air ceiling. A GDB target would have to be built by cutting features. For the concurrency tripwire on hardware, use `pico_w_asserts`. ### Build flags - `-Os` — optimize for size diff --git a/README.pt-BR.md b/README.pt-BR.md index 4cc7a3b3..9e30583e 100644 --- a/README.pt-BR.md +++ b/README.pt-BR.md @@ -258,7 +258,7 @@ simut/ > O ponto de acesso de configuração é WPA2 em todas as imagens, com chave por > aparelho mostrada no console e, onde houver, no display. Ver [SECURITY.md](SECURITY.md) §2 e §8. -> Não há ambiente de depuração. O `pico_w_debug` foi removido na v2.4.1 depois de nunca ter linkado: em `-Og` a imagem estourava o slot de 1020 KB em ~100 KB. A flash é apertada. A imagem release usa 97,7 % do slot de programa de 1.044.480 B (o valor medido mora em `tools/flash_budget.json`), e o CI confere cada `.bin`, com os 241 B da assinatura, contra o teto de atualização pelo ar, de 1.040.384 B. Um alvo de GDB teria de ser montado cortando funcionalidades. Para o tripwire de concorrência no hardware, use `pico_w_asserts`. +> Não há ambiente de depuração. O `pico_w_debug` foi removido na v2.4.1 depois de nunca ter linkado: em `-Og` a imagem estourava o slot de 1020 KB em ~100 KB. A flash é apertada. A imagem release usa 97,8 % do slot de programa de 1.044.480 B (o valor medido mora em `tools/flash_budget.json`), e o CI confere cada `.bin`, com os 241 B da assinatura, contra o teto de atualização pelo ar, de 1.040.384 B. Um alvo de GDB teria de ser montado cortando funcionalidades. Para o tripwire de concorrência no hardware, use `pico_w_asserts`. ### Flags de build - `-Os` — otimização por tamanho
Defina um PIN pelo botão PIN da página Usuários ou no item Usuários do painel
PIN muito curto (mín. 4) com um PIN definido pela webA política exige mais caracteres que o PIN tem; a web aceita 4 a 8 dígitos sem olhar a políticaDefina o PIN no painel, no item Alterar Senha ou em UsuáriosO painel pede Novo PIN logo depois de um PIN definido pela webO PIN é mais curto que o mínimo da política; a web aceita 4 a 8 dígitos sem olhar a políticaEscolha um PIN que siga a política. Até a v2.11.0, o painel recusava esse PIN com PIN muito curto (mín. 4)
Um PIN antigo não pode mais ser digitado depois de mudar a políticaA política tirou as letras dos cartões, ou o teto ficou menor que o PINDefina um PIN novo para a conta no item Usuários do painel, na página Usuários ou no console
PIN já em uso! Outra conta tem esse PIN Escolha outro PIN
Conta aparece com bloqueado no painel Seis PINs errados seguidos dessa conta Reinicie o aparelho ou use outra conta
Tela vermelha ACESSO BLOQUEADO 20 PINs errados desde a última entrada certa Reinicie o aparelho
Contagem Aguarde N segundos… no teclado Terceira, quarta ou quinta tentativa errada de uma conta Espere; a espera vale para qualquer conta nesse período
O painel pede Novo PIN logo depois de entrar PIN de fábrica 1234 do admin, ou política de PIN mais exigente Escolha um PIN novo que atenda à política
Depois de mudar a política pela web, a mudança não valeu A política pela web só vale depois de Salvar e reiniciar Grave com Salvar e reiniciar, ou mude no painel, que aplica na hora
Operador conseguiu se dar permissões de painel No painel, quem tem Usuários concede as três permissões de painel sem ter todas Dê Usuários só a quem pode ter também as permissões de painel
Bloqueio de 300 s ao entrar pela web Nove ou mais senhas erradas seguidas a partir do mesmo computador Espere a contagem; ela zera numa entrada certa
Limite de sessões atingido. Tente mais tarde. Três outras contas com sessão aberta Feche uma sessão com Sair ou espere 15 min de ociosidade delaAs duas digitações diferem; o painel volta a Novo PIN
PIN muito curto (mín. 4)Menos caracteres que o mínimo da políticaPIN muito curto (mín. N)Menos caracteres que o mínimo da política, que a mensagem diz no lugar de N. Até a v2.11.0, ela dizia 4 com qualquer mínimo
PIN inválido!