diff --git a/README.es-ES.md b/README.es-ES.md index da157bc3..8758307a 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 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: @@ -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 @@ -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 (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 d288742c..4e81fbdf 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** | | -| **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 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: @@ -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 @@ -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 (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 f462d5c6..9e30583e 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** | | -| **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 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: @@ -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 @@ -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 (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 00339d8c..1a1f718c 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:

@@ -4283,8 +4283,8 @@

O próprio PIN

- - + + @@ -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..34413800 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: @@ -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 | @@ -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/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/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..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. */ @@ -163,6 +164,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 +317,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; @@ -487,16 +491,25 @@ 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); 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/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/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/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 4887e593..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) */ @@ -4269,6 +4270,65 @@ 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"); +} + +/* "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); @@ -5065,6 +5125,9 @@ 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_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. 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 }
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í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! 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 dela