From 18d8a4d13ff1b32cfa6a009390c5c79b7a48f882 Mon Sep 17 00:00:00 2001 From: Matheus Nespolo Date: Sat, 3 Oct 2026 17:57:14 -0300 Subject: [PATCH 01/21] docs: spec do firmware Uno v3 (supervisao por marcos e robustez) Co-Authored-By: Claude Opus 5.5 --- .../2026-10-03-firmware-uno-v3-design.md | 196 ++++++++++++++++++ 1 file changed, 196 insertions(+) create mode 100644 docs/superpowers/specs/2026-10-03-firmware-uno-v3-design.md diff --git a/docs/superpowers/specs/2026-10-03-firmware-uno-v3-design.md b/docs/superpowers/specs/2026-10-03-firmware-uno-v3-design.md new file mode 100644 index 0000000..ffc0e25 --- /dev/null +++ b/docs/superpowers/specs/2026-10-03-firmware-uno-v3-design.md @@ -0,0 +1,196 @@ +# Firmware Arduino Uno v3.0 — Supervisão por marcos e robustez — Design + +Data: 03/10/2026 · Branch: `feat/firmware-uno-v3` · Base: `fddde08` (PR #42) · Idioma: português do Brasil. + +## 1. Objetivo + +Tornar o firmware do Arduino Uno (`arduino/data_flow_inventory/`) robusto como produto e resolver os problemas observados em bancada em 02–03/10/2026, **sem mudar a conexão com o ESP32 nem com o servidor**. + +Problemas de bancada que este design precisa resolver ou tornar diagnosticáveis: + +| # | Observado | Causa provável | +|---|---|---| +| P1 | Esteira B entregou a peça, mas o estoque não foi debitado | o sensor de junção J2 não registrou a passagem (sempre em LOW/HIGH, mal calibrado) ou o pulso foi perdido; o firmware terminou em `timeout` sem débito | +| P2 | Esteira C mal move a peça e dá timeout | PWM baixo, atrito estático, motor travando; o motor ficava 12,5 s puxando corrente de rotor bloqueado | +| P3 | Uno travou / reiniciou junto com a Serial Monitor | queda ou ruído de alimentação com o motor travado (hardware) e/ou auto-reset ao abrir a Serial Monitor (DTR) | +| P4 | Estoque voltou a 15 sem aviso | reset do Uno; o estoque não é persistido | +| P5 | LCD apagado | hardware (alimentação/fiação/módulo); o firmware não pode travar por causa dele | + +## 2. Decisões tomadas com a equipe + +| # | Decisão | +|---|---| +| D1 | Escopo de **robustez de produto** (reestruturar o firmware), não só remendos. | +| D2 | **Não mexer na conexão com o ESP32 nem com o servidor.** As mensagens JSON atuais continuam idênticas; só entram **campos opcionais novos** e **novos valores de `tipo`** em eventos de erro. Os comandos aceitos continuam `CMD:PECA:X` e `CMD:RESET`. | +| D3 | **Estoque não é persistido**: volta a `ESTOQUE_INICIAL` (15) no boot, mas o evento `inicio` informa a causa do reset. | +| D4 | **Uma peça por vez** sobre o sensor de topo: o topo serve de indicador de avanço. | +| D5 | Falha de entrega supervisionada **por marcos** (partida → trânsito → saída), com prazos por esteira. | +| D6 | **Arquitetura modular (abordagem A)**: lógica pura testável no PC + camada de hardware fina. | +| D7 | **Débito na confirmação da junção** (momento mostrado no fluxograma oficial), seguido de 3 s de saída. | +| D8 | Sem nova tentativa automática após falha: o operador confere e envia RESET. | + +Fora do escopo: ESP32, servidor, dashboard, persistência em EEPROM, mudança de pinos, separador (motor de passo). + +## 3. Arquitetura + +### 3.1 Arquivos + +``` +arduino/data_flow_inventory/ +├── data_flow_inventory.ino # fino: setup() e loop() chamam os módulos +├── config.h # pinos, parâmetros por esteira, constantes +└── src/ + ├── logica/ # C++ puro, SEM Arduino.h → testável no PC + │ ├── filtro_entrada.h/.cpp # debounce por tempo + bordas + │ ├── supervisor_entrega.h/.cpp # marcos M1/M2/M3 + │ └── fsm.h/.cpp # 5 estados, comandos, estoque, saídas + └── hw/ # depende do Arduino + ├── motores.h/.cpp # kick-start, PWM de regime, parada segura + ├── sensores.h/.cpp # amostragem dos 6 TCRT5000 + captura de bordas da junção por PCINT + ├── comunicacao.h/.cpp # parser de comando (char[]) + publicação JSON + ├── lcd.h/.cpp # detecção 0x27/0x3F, redesenho só quando muda + └── diagnostico.h/.cpp # causa do reset, RAM livre, watchdog +``` + +A Arduino IDE e o `arduino-cli` compilam recursivamente a pasta `src/` do sketch. Os arquivos de `src/logica/` não incluem `Arduino.h` nem usam tipos do Arduino (só ``), para compilar também no PC. + +### 3.2 Ciclo do `loop()` (não bloqueante) + +1. A cada 5 ms: amostrar os sensores e atualizar os filtros. +2. Ler os bytes disponíveis da serial e montar comandos (sem bloquear). +3. Entregar à FSM o tempo atual (`agora_ms`), os sensores filtrados e o comando recebido; a FSM devolve as **saídas** (motor a ligar/parar, eventos a publicar, mudança de LCD). +4. Aplicar as saídas nos motores; em ERRO, a "saída segura" força todos os motores em 0 a cada ciclo. +5. Publicar uma mensagem periódica escalonada (status, estoque, sensores, esteiras: uma a cada 250 ms, cada uma 1x por segundo — comportamento atual). +6. Atualizar o LCD se o conteúdo mudou. +7. `wdt_reset()`. + +### 3.3 Memória (2 KB de RAM) + +- Nenhuma `String` no caminho normal; buffers fixos (`char` de 32 B para comando; documentos JSON pequenos). +- Textos fixos em flash quando possível (`F()`/`PROGMEM`). +- Orçamento: variáveis globais ≤ **65%** da RAM (hoje 55%), verificado na compilação. +- O código precisa compilar com **ArduinoJson 6** (versão fixada no CI: 6.21.5) **e 7** (instalada na máquina da equipe). + +## 4. Supervisão da entrega e regras da FSM + +### 4.1 Estados + +1. **AGUARDANDO_PEDIDO**: aceita `CMD:PECA:X`. `CMD:RESET` é ignorado. +2. **VERIFICANDO_ESTOQUE** (instantâneo). Em ordem; qualquer falha vai para **ERRO sem ligar o motor** (fase `verificacao`): + 1. contador da peça > 0, senão `sem_estoque`; + 2. sensor de topo ocupado, senão `sem_peca_topo`; + 3. sensor de junção livre, senão `juncao_obstruida`. +3. **ACIONANDO_ESTEIRA**: liga o motor com kick-start; inicia o supervisor. +4. **ENTREGANDO_PECA**: marcos (prazos contados a partir da partida do motor): + - **M1 Partida** — topo livre (filtrado) em até `prazo_partida_ms` (3000). Falha → `motor_sem_avanco` (fase `partida`), motor parado, **sem débito**. + - **M2 Trânsito** — borda válida na junção (pulso ≥ `pulso_min_juncao_ms`, 20 ms) em até `timeout_ms` (12500). Falha → `timeout` (fase `transito`), motor parado, **sem débito**; LCD pede para retirar a peça. + - **Confirmação** — **debita na hora** e publica o evento `entrega` e o estoque. + - **M3 Saída** — motor ligado por `saida_ms` (3000); no fim, junção livre. Falha → `peca_presa_saida` (fase `saida`), motor parado; o débito **já foi feito** (a peça saiu do estoque). + - Sucesso: motor parado, pausa de 1500 ms (LCD "Entregue"), volta a AGUARDANDO. +5. **ERRO**: motores sempre em 0; só sai com `CMD:RESET` (volta a AGUARDANDO, limpa o supervisor, republica estado e estoque). + +Rejeições que **não** mudam o estado: `ocupado` (pedido fora de AGUARDANDO; `peca` = peça pedida), `peca_invalida`, `comando_desconhecido`. Formato exato dos comandos: `CMD:PECA:` + 1 letra (A/B/C) e `CMD:RESET`. + +**Invariante:** o estoque só é decrementado na confirmação válida da junção da esteira da peça pedida. + +### 4.2 Captura da junção + +- As bordas dos três sensores de junção (J1 = A3/PCINT11, J2 = D2/PCINT18, J3 = D4/PCINT20) são registradas por **interrupção de mudança de pino**: a ISR só anota o instante da mudança, sem lógica. Um pulso curto não se perde mesmo com o loop ocupado (serial, LCD). +- A lógica valida a largura do pulso (≥ 20 ms) e conta as bordas observadas durante a entrega (`bordas_juncao`), usado no diagnóstico. +- Sensores de topo continuam amostrados a cada 5 ms com o mesmo filtro por tempo. + +### 4.3 Motores + +- Partida com **kick-start**: PWM 255 por `kick_ms` (150 ms) e depois `pwm_regime` da esteira (200). +- Parada imediata em qualquer erro e no fim da saída. +- No `setup()`, os pinos dos motores são os primeiros configurados (saída em 0), antes de qualquer outra inicialização. + +### 4.4 Parâmetros por esteira (`config.h`) + +| Parâmetro | Padrão (A, B e C) | +|---|---| +| `pwm_regime` | 200 | +| `kick_ms` | 150 | +| `prazo_partida_ms` | 3000 | +| `timeout_ms` | 12500 | +| `saida_ms` | 3000 | +| `pulso_min_juncao_ms` | 20 | + +## 5. Diagnóstico e recuperação + +### 5.1 Watchdog + +- Watchdog de **2 s**, alimentado no fim de cada `loop()`; nenhuma operação do laço bloqueia mais que algumas dezenas de ms (a tela de abertura deixa de usar `delay(2000)`). +- Modo interrupção-e-reset: a ISR do watchdog grava a marca `watchdog` em uma variável `.noinit` antes do reset. +- O watchdog é **desligado logo no início do boot** (seção `.init3`), porque após um reset por watchdog ele continua ativo com o prazo mínimo e reiniciaria o Uno em laço. + +### 5.2 Causa do reset + +O bootloader do Uno (Optiboot) costuma zerar o `MCUSR` antes do sketch. Combinação usada: + +1. `MCUSR` lido no `.init3`, quando ainda vier preenchido (pode indicar `brownout`); +2. assinatura em `.noinit`: ausente → `energia` (RAM apagada); presente → `reinicio` (botão, queda breve, auto-reset ao abrir a porta serial); +3. marca gravada pela ISR do watchdog → `watchdog`. + +Publicado no `inicio` como `reset: energia | reinicio | watchdog | brownout`. + +### 5.3 Periféricos + +- **LCD:** no boot, procura o módulo em 0x27 e 0x3F; se nenhum responder, segue sem LCD (`lcd: false` no `inicio`) e não tenta escrever de novo. `Wire.setWireTimeout` continua ativo. +- **RAM livre:** medida e publicada no `inicio` e no `status`. + +### 5.4 Campos novos (todos opcionais) + +| Mensagem | Campos novos | +|---|---| +| `evento: inicio` | `versao: "3.0"`, `reset`, `lcd`, `ram_livre` (os atuais `msg`, `driver` continuam) | +| `type: status` | `ram_livre` | +| `evento: erro` | `fase`, `t_ms`, `bordas_juncao` | + +Novos valores de `tipo` em `evento: erro`: `sem_peca_topo`, `juncao_obstruida`, `motor_sem_avanco`, `peca_presa_saida`. Valores atuais mantidos: `sem_estoque`, `timeout`, `ocupado`, `peca_invalida`, `comando_desconhecido`. + +Compatibilidade conferida no código: o ESP32 repassa a linha JSON inteira (parse em 512 B, MQTT em 1024 B); o servidor só usa `evento` e `peca` (valor desconhecido nas métricas vira `outro`); o dashboard exibe `tipo` como texto e ignora campos desconhecidos. Cada mensagem do Uno deve continuar abaixo de 200 B. + +### 5.5 LCD por situação (16 colunas) + +| Situação | Linha 1 | Linha 2 | +|---|---|---| +| Repouso | `Estoque:` | `A:15 B:15 C:15` | +| Entregando | `Entregando B` | `Aguarda juncao` | +| `motor_sem_avanco` | `ERRO: Motor C` | `Sem avanco` | +| `timeout` | `ERRO: Timeout B` | `Retire a peca` | +| `juncao_obstruida` | `ERRO: Juncao J2` | `Obstruida` | +| `sem_peca_topo` | `ERRO: Topo B` | `Sem peca` | +| `sem_estoque` | `ERRO: Estoque B` | `Zerado` | +| `peca_presa_saida` | `ERRO: Saida B` | `Peca presa` | + +### 5.6 Recuperação após reset + +O sistema volta em AGUARDANDO, motores em 0, estoque em 15. Uma peça esquecida no meio da esteira é pega pela checagem prévia (`juncao_obstruida`) ou pelo M1; nada é debitado às cegas. + +## 6. Testes e validação + +1. **Lógica no PC** (`test/firmware_uno/`): mini-framework de asserts próprio (sem dependência externa), tempo simulado (inclui estouro de `millis()`), compilado com g++ — localmente via Docker (`gcc:14`), no CI em um job novo `firmware-logica` (os jobs atuais não mudam). Cenários obrigatórios, escritos antes do código: + - entrega normal A/B/C (débito na confirmação, saída de 3 s, volta a AGUARDANDO); + - `sem_estoque`, `sem_peca_topo`, `juncao_obstruida` sem ligar o motor; + - M1 → `motor_sem_avanco` sem débito (caso P2); + - M2 com `bordas_juncao: 0` → `timeout` sem débito (caso P1); + - pulso < 20 ms ignorado; ≥ 20 ms confirma; + - M3 → `peca_presa_saida` com débito feito; + - comandos: `ocupado` com a peça pedida, `peca_invalida`, `comando_desconhecido`, RESET em e fora de ERRO, formato exato; + - saída segura em ERRO; + - propriedade: nenhum débito sem confirmação em todos os cenários. +2. **Compilação real**: Uno com `arduino-cli` (local e CI), orçamento de RAM ≤ 65%, ArduinoJson 6 e 7. Suítes atuais do servidor e do frontend como rede de segurança. +3. **Bancada**: roteiro novo em `docs/testes/roteiros/` (boot e causa do reset; leitura de J1–J3 vazias e com peça, calibrando a J2 antes de tudo; uma entrega por esteira; C travada à mão → `motor_sem_avanco` em ~3 s sem reset; timeout; peça presa na saída; junção obstruída; 10 entregas seguidas com `ram_livre` estável). +4. **Documentação**: `docs/ARCHITECTURE.md` (erros, marcos, campos), tabela da FSM no `README.md`, `docs/CHANGELOG.md` e um diagrama Mermaid dos marcos. + +## 7. Riscos + +| Risco | Mitigação | +|---|---| +| RAM insuficiente com módulos e JSON | sem `String`, textos em flash, orçamento de 65% checado na compilação | +| Diferenças entre ArduinoJson 6 e 7 | usar só a API comum às duas (`StaticJsonDocument`, `serializeJson`); compilar com as duas | +| Prazo do M1 curto demais para a esteira real | parâmetro por esteira; calibrar na bancada (passo do roteiro) | +| Causa de reset imprecisa no Optiboot | documentado; o campo distingue ao menos energia, reinício e watchdog | +| Watchdog em laço após reset | desligado no `.init3` | +| Hardware (fiação, IRF520, fonte) segue com defeito | o firmware para o motor rápido e diz onde falhou; a correção física é da equipe | From 117b2a04c89a832182a4f2121b777b84b88fdedc Mon Sep 17 00:00:00 2001 From: Matheus Nespolo Date: Sat, 3 Oct 2026 18:23:27 -0300 Subject: [PATCH 02/21] docs: plano de implementacao do firmware Uno v3 Co-Authored-By: Claude Opus 5.5 --- .../plans/2026-10-03-firmware-uno-v3.md | 2991 +++++++++++++++++ 1 file changed, 2991 insertions(+) create mode 100644 docs/superpowers/plans/2026-10-03-firmware-uno-v3.md diff --git a/docs/superpowers/plans/2026-10-03-firmware-uno-v3.md b/docs/superpowers/plans/2026-10-03-firmware-uno-v3.md new file mode 100644 index 0000000..2354546 --- /dev/null +++ b/docs/superpowers/plans/2026-10-03-firmware-uno-v3.md @@ -0,0 +1,2991 @@ +# Firmware do Uno v3.0 — Supervisão por marcos e robustez — Plano de Implementação + +> **Para agentes de execução:** SUB-SKILL OBRIGATÓRIA: use superpowers:subagent-driven-development (recomendado) ou superpowers:executing-plans para executar este plano tarefa a tarefa. Os passos usam caixas de seleção (`- [ ]`). + +**Objetivo:** Reestruturar o firmware do Arduino Uno em módulos (lógica pura testável no PC + camada de hardware fina) com supervisão da entrega por marcos, watchdog, causa do reset e tolerância a falhas de periféricos, mantendo intacta a conversa com o ESP32 e o servidor. + +**Arquitetura:** `src/logica/` contém regras em C++ puro (protocolo, filtro, supervisor, FSM, telas, causa do reset), compiladas e testadas no PC com g++ (Docker local, Ubuntu no CI). `src/hw/` liga essas regras aos pinos, à serial, ao LCD e ao watchdog. O `.ino` só orquestra. Textos fixos ficam na flash (`TXT()`), e o JSON é montado sem ArduinoJson, com testes de contrato que reproduzem byte a byte as mensagens do firmware v2.x. + +**Stack:** Arduino Uno (ATmega328P, AVR core 1.8.x), C++11, `LiquidCrystal_I2C`, `Wire`, `avr/wdt.h`; g++ 14 (imagem Docker `gcc:14`) para os testes; `arduino-cli` para compilar. + +**Spec:** `docs/superpowers/specs/2026-10-03-firmware-uno-v3-design.md` (autoridade; em conflito, o spec vence). + +## Refinamentos em relação ao spec + +- **Sem ArduinoJson no Uno:** o JSON é montado por `src/logica/protocolo` (sem printf), com testes de contrato byte a byte contra o formato v2.x. Isso elimina o risco "ArduinoJson 6 vs 7" do spec e libera RAM. O CI continua instalando a biblioteca (o ESP32 usa). +- **Textos na flash (`TXT()`):** a primeira versão compilada usava 85% da RAM porque, no AVR, todo literal ocupa RAM. Com `PROGMEM` a RAM global caiu para 46% (o v2.x usava 55%). +- **Módulos lógicos a mais:** `protocolo`, `telas`, `causa_reset` e `texto_flash` saíram de dentro dos módulos de hardware para serem testáveis no PC. Os nomes de erro viraram `enum` (`TipoErro`). +- **Código já validado:** todo o código deste plano foi compilado para o Uno e testado no PC antes de o plano ser escrito; as contagens de testes de cada tarefa (17, 21, 27, 37, 38) foram conferidas. + +## Restrições Globais + +- **Nada pode quebrar o funcionamento parcial atual:** as mensagens JSON do v2.x (`status`, `estoque`, `sensores`, `esteiras`, eventos `pedido`, `entrega`, `erro`, `inicio`) continuam **idênticas byte a byte**; só entram campos opcionais **no fim** e novos valores de `tipo`. Os testes de contrato da Tarefa 1 guardam isso. +- **Não mexer no ESP32 nem no servidor** (`esp32/`, `server/`, `frontend/`, `simulator/` intocados). +- Comandos aceitos: `CMD:PECA:` + uma letra (A, B ou C) e `CMD:RESET`. Serial a **9600 baud**, linhas terminadas em `\r\n`. +- Pinos inalterados: motores A=9, B=10, C=11; topo A0/A1/A2; junção J1=A3, J2=D2, J3=D4; LCD I²C (A4/A5). +- Estoque inicial **15**, sem persistência; o `inicio` informa a causa do reset. +- Débito **somente** na confirmação válida da junção da esteira pedida. +- Parâmetros por esteira (padrão A, B e C): PWM 200, kick 150 ms, partida 3000 ms, timeout 12500 ms, saída 3000 ms, pulso mínimo da junção 20 ms. +- RAM global do Uno **≤ 65%** (verificado na compilação). Arquivos de `src/logica/` **não** incluem `Arduino.h`. +- Idioma de código, comentários, mensagens e documentação: **português do Brasil**. +- Trailer de todo commit: `Co-Authored-By: Claude Opus 5.5 `. +- Não colar cercas de markdown (três crases) em arquivos que não sejam `.md`. +- `core.autocrlf=true`: o aviso "LF will be replaced by CRLF" é esperado. +- Nunca matar processos que você não iniciou. + +## Como rodar as verificações + +- **Testes da lógica (PC):** Docker Desktop precisa estar aberto. Na raiz do repositório: + +```bash +MSYS_NO_PATHCONV=1 docker run --rm -v "$(cygpath -w "$PWD"):/w" -w /w gcc:14 make -C test/firmware_uno test +``` + +- **Compilação do Uno:** o `arduino-cli` vem com a Arduino IDE nesta máquina: + +```bash +CLI="/c/Users/matheusn/AppData/Local/Programs/Arduino IDE/resources/app/lib/backend/resources/arduino-cli.exe" +"$CLI" compile --fqbn arduino:avr:uno --warnings all arduino/data_flow_inventory +``` + +## Mapa de arquivos + +| Arquivo | Tarefa | Responsabilidade | +|---|---|---| +| `test/firmware_uno/teste.h`, `principal.cpp`, `Makefile` | 1 | mini-framework de testes e build no PC | +| `src/logica/texto_flash.h` | 1 | `TXT()`/`lerTxt()`: textos na flash do AVR | +| `src/logica/protocolo.h/.cpp` + `teste_protocolo.cpp` | 1 | comandos e mensagens JSON (contrato v2.x) | +| `src/logica/filtro_entrada.h/.cpp`, `causa_reset.h/.cpp` + `teste_filtro_reset.cpp` | 2 | debounce por tempo; causa do reset | +| `src/logica/supervisor_entrega.h/.cpp` + `teste_supervisor.cpp` | 3 | marcos M1/M2/M3 | +| `src/logica/fsm.h/.cpp` + `teste_fsm.cpp` | 4 | 5 estados, comandos, estoque, eventos | +| `src/logica/telas.h/.cpp` + `teste_telas.cpp` | 5 | textos do LCD 16x2 | +| `config.h`, `src/hw/*`, `data_flow_inventory.ino`, CI | 6 | hardware, troca do firmware, CI | +| `docs/...` | 7 | documentação e roteiro de bancada | +| — | 8 | verificação final | + +Caminhos `src/...` acima são relativos a `arduino/data_flow_inventory/`. + +--- + +### Tarefa 1: Infra de testes no PC e protocolo serial (contrato v2.x) + +**Arquivos:** +- Criar: `test/firmware_uno/teste.h`, `test/firmware_uno/principal.cpp`, `test/firmware_uno/Makefile` +- Criar: `arduino/data_flow_inventory/src/logica/texto_flash.h`, `protocolo.h`, `protocolo.cpp` +- Teste: `test/firmware_uno/teste_protocolo.cpp` +- Modificar: `.gitignore` (binário dos testes), `.github/workflows/lint-and-security.yaml` (job `firmware-logica`) + +**Interfaces:** +- Produz: `ComandoTipo interpretarComando(const char* linha, char* peca)`; `LeitorLinha` + `leitorIniciar`/`leitorAlimentar`; `formatarStatus/Estoque/Sensores/Esteiras/Pedido/Entrega/Erro/Inicio(char* buf, size_t cap, ...)` (devolvem o tamanho ou 0); macro `TXT(s)` e `lerTxt(p)`. Textos fixos passados às funções `formatar*` são `TXT(...)`. + +**Contexto:** o firmware atual (v2.x, `arduino/data_flow_inventory/data_flow_inventory.ino`) publica JSON com ArduinoJson. Os testes desta tarefa fixam o formato exato dessas mensagens; o novo código monta o JSON à mão (sem printf, sem ArduinoJson) e precisa reproduzi-lo byte a byte. O `.ino` atual **não muda** nesta tarefa (a troca é na Tarefa 6). + +- [ ] **Passo 1: Mini-framework e build** + +Crie `test/firmware_uno/teste.h` com exatamente este conteúdo: + +```cpp +// Mini-framework de testes do firmware (sem dependências externas). +#pragma once +#include +#include + +namespace teste { +inline int& falhas() { static int n = 0; return n; } +inline int& total() { static int n = 0; return n; } +typedef void (*FuncaoTeste)(); +struct Registro { const char* nome; FuncaoTeste f; Registro* prox; }; +inline Registro*& lista() { static Registro* r = nullptr; return r; } +struct Registrar { + Registro r; + Registrar(const char* nome, FuncaoTeste f) { + r.nome = nome; r.f = f; r.prox = nullptr; + Registro** p = &lista(); + while (*p) p = &(*p)->prox; // mantém a ordem de declaração + *p = &r; + } +}; +} // namespace teste + +#define TESTE(nome) \ + static void nome(); \ + static teste::Registrar registro_##nome(#nome, nome); \ + static void nome() + +#define VERIFICA(cond) \ + do { \ + teste::total()++; \ + if (!(cond)) { \ + teste::falhas()++; \ + printf(" FALHA %s:%d: %s\n", __FILE__, __LINE__, #cond); \ + } \ + } while (0) + +#define VERIFICA_TEXTO(a, b) \ + do { \ + teste::total()++; \ + if (strcmp((a), (b)) != 0) { \ + teste::falhas()++; \ + printf(" FALHA %s:%d:\n obtido: %s\n esperado: %s\n", \ + __FILE__, __LINE__, (a), (b)); \ + } \ + } while (0) +``` + +Crie `test/firmware_uno/principal.cpp` com exatamente este conteúdo: + +```cpp +// Executor dos testes do firmware: roda todos os TESTE(...) registrados. +#include "teste.h" + +int main() { + int nTestes = 0; + for (teste::Registro* r = teste::lista(); r; r = r->prox) { + int antes = teste::falhas(); + r->f(); + nTestes++; + printf("%s %s\n", teste::falhas() == antes ? "ok " : "FALHOU", r->nome); + } + printf("\n%d testes, %d verificacoes, %d falhas\n", nTestes, teste::total(), teste::falhas()); + return teste::falhas() == 0 ? 0 : 1; +} +``` + +Crie `test/firmware_uno/Makefile` com exatamente este conteúdo: + +```make +# Testes da lógica pura do firmware do Uno (compila no PC com g++). +# Uso: make -C test/firmware_uno test +LOGICA := ../../arduino/data_flow_inventory/src/logica +CXX ?= g++ +CXXFLAGS := -std=c++11 -Wall -Wextra -Werror -O1 -I$(LOGICA) + +FONTES_LOGICA := $(wildcard $(LOGICA)/*.cpp) +FONTES_TESTE := principal.cpp $(wildcard teste_*.cpp) + +.PHONY: test clean +test: testes + ./testes + +testes: $(FONTES_LOGICA) $(FONTES_TESTE) teste.h $(wildcard $(LOGICA)/*.h) + $(CXX) $(CXXFLAGS) -o $@ $(FONTES_TESTE) $(FONTES_LOGICA) + +clean: + rm -f testes +``` + +Acrescente ao final do `.gitignore`: + +``` +# Binário dos testes da lógica do firmware (test/firmware_uno) +test/firmware_uno/testes +``` + +- [ ] **Passo 2: Escrever os testes de contrato (falham: o módulo não existe)** + +Crie `test/firmware_uno/teste_protocolo.cpp` com exatamente este conteúdo: + +```cpp +// Contrato do protocolo serial. As strings "esperado" reproduzem byte a +// byte o que o firmware v2.x (ArduinoJson) publicava: se um destes testes +// quebrar, o ESP32/servidor/dashboard podem deixar de entender o Uno. +#include "protocolo.h" +#include "teste.h" + +static char buf[160]; // mesmo tamanho do buffer do firmware (src/hw/comunicacao.cpp) + +// ---- Mensagens existentes (formato v2.x, sem campos novos) ---------------- + +TESTE(status_igual_v2) { + formatarStatus(buf, sizeof buf, "AGUARDANDO_PEDIDO", 0, 42, -1); + VERIFICA_TEXTO(buf, "{\"type\":\"status\",\"estado\":\"AGUARDANDO_PEDIDO\",\"pecaSolicitada\":0,\"uptime\":42}"); +} + +TESTE(estoque_igual_v2) { + const int est[3] = {15, 14, 0}; + formatarEstoque(buf, sizeof buf, est); + VERIFICA_TEXTO(buf, "{\"type\":\"estoque\",\"pecaA\":15,\"pecaB\":14,\"pecaC\":0}"); +} + +TESTE(sensores_igual_v2) { + const bool topo[3] = {true, false, true}; + const bool jun[3] = {false, true, false}; + formatarSensores(buf, sizeof buf, topo, jun); + VERIFICA_TEXTO(buf, "{\"type\":\"sensores\",\"topo\":{\"A\":1,\"B\":0,\"C\":1},\"juncao\":{\"J1\":0,\"J2\":1,\"J3\":0}}"); +} + +TESTE(esteiras_igual_v2) { + const bool sec[3] = {false, true, false}; + formatarEsteiras(buf, sizeof buf, sec); + VERIFICA_TEXTO(buf, "{\"type\":\"esteiras\",\"principal\":1,\"secA\":0,\"secB\":1,\"secC\":0}"); +} + +TESTE(pedido_igual_v2) { + formatarPedido(buf, sizeof buf, 'B'); + VERIFICA_TEXTO(buf, "{\"type\":\"evento\",\"evento\":\"pedido\",\"peca\":\"B\"}"); +} + +TESTE(entrega_igual_v2) { + const int est[3] = {15, 14, 15}; + formatarEntrega(buf, sizeof buf, 'B', est); + VERIFICA_TEXTO(buf, "{\"type\":\"evento\",\"evento\":\"entrega\",\"peca\":\"B\",\"estoqueA\":15,\"estoqueB\":14,\"estoqueC\":15}"); +} + +TESTE(erro_igual_v2_com_e_sem_peca) { + formatarErro(buf, sizeof buf, "ocupado", 'C', nullptr, -1, -1); + VERIFICA_TEXTO(buf, "{\"type\":\"evento\",\"evento\":\"erro\",\"tipo\":\"ocupado\",\"peca\":\"C\"}"); + formatarErro(buf, sizeof buf, "peca_invalida", 0, nullptr, -1, -1); + VERIFICA_TEXTO(buf, "{\"type\":\"evento\",\"evento\":\"erro\",\"tipo\":\"peca_invalida\"}"); +} + +TESTE(inicio_igual_v2_sem_campos_novos) { + formatarInicio(buf, sizeof buf, "2.1", nullptr, -1, -1); + VERIFICA_TEXTO(buf, "{\"type\":\"evento\",\"evento\":\"inicio\",\"msg\":\"Sistema iniciado\",\"versao\":\"2.1\",\"driver\":\"IRF520\"}"); +} + +// ---- Campos novos: sempre DEPOIS dos atuais e só quando informados --------- + +TESTE(status_com_ram_livre) { + formatarStatus(buf, sizeof buf, "ERRO", 2, 4294967295UL, 812); + VERIFICA_TEXTO(buf, "{\"type\":\"status\",\"estado\":\"ERRO\",\"pecaSolicitada\":2,\"uptime\":4294967295,\"ram_livre\":812}"); +} + +TESTE(erro_com_diagnostico) { + formatarErro(buf, sizeof buf, "timeout", 'B', "transito", 12503, 0); + VERIFICA_TEXTO(buf, "{\"type\":\"evento\",\"evento\":\"erro\",\"tipo\":\"timeout\",\"peca\":\"B\",\"fase\":\"transito\",\"t_ms\":12503,\"bordas_juncao\":0}"); +} + +TESTE(inicio_com_campos_novos) { + formatarInicio(buf, sizeof buf, "3.0", "watchdog", 0, 640); + VERIFICA_TEXTO(buf, "{\"type\":\"evento\",\"evento\":\"inicio\",\"msg\":\"Sistema iniciado\",\"versao\":\"3.0\",\"driver\":\"IRF520\",\"reset\":\"watchdog\",\"lcd\":false,\"ram_livre\":640}"); +} + +TESTE(maiores_mensagens_cabem_no_buffer_de_160_bytes) { + const bool t[3] = {true, true, true}; + VERIFICA(formatarSensores(buf, sizeof buf, t, t) > 0); + VERIFICA(formatarErro(buf, sizeof buf, "peca_presa_saida", 'C', "transito", 2147483647L, 255) > 0); + VERIFICA(formatarInicio(buf, sizeof buf, "3.0", "reinicio", 1, 2048) > 0); + VERIFICA(strlen(buf) < 160); +} + +TESTE(buffer_pequeno_devolve_zero_e_string_vazia) { + char pequeno[10]; + VERIFICA(formatarPedido(pequeno, sizeof pequeno, 'A') == 0); + VERIFICA_TEXTO(pequeno, ""); +} + +// ---- Comandos ------------------------------------------------------------------- + +TESTE(comandos_validos) { + char p; + VERIFICA(interpretarComando("CMD:PECA:A", &p) == CMD_PECA && p == 'A'); + VERIFICA(interpretarComando("CMD:PECA:C", &p) == CMD_PECA && p == 'C'); + VERIFICA(interpretarComando(" CMD:PECA:B \r", &p) == CMD_PECA && p == 'B'); + VERIFICA(interpretarComando("CMD:RESET", &p) == CMD_RESET && p == 0); +} + +TESTE(comandos_invalidos) { + char p; + VERIFICA(interpretarComando("", &p) == CMD_NENHUM); + VERIFICA(interpretarComando(" ", &p) == CMD_NENHUM); + VERIFICA(interpretarComando("CMD:PECA:", &p) == CMD_PECA_INVALIDA); + VERIFICA(interpretarComando("CMD:PECA:Z", &p) == CMD_PECA_INVALIDA && p == 0); + VERIFICA(interpretarComando("CMD:PECA:AB", &p) == CMD_PECA_INVALIDA); + VERIFICA(interpretarComando("CMD:PECA:a", &p) == CMD_PECA_INVALIDA); + VERIFICA(interpretarComando("CMD:RESETX", &p) == CMD_DESCONHECIDO); + VERIFICA(interpretarComando("{\"type\":\"x\"}", &p) == CMD_DESCONHECIDO); +} + +TESTE(leitor_monta_linhas_e_ignora_cr) { + LeitorLinha l; + leitorIniciar(l); + const char* entrada = "CMD:PE"; + for (const char* c = entrada; *c; c++) VERIFICA(!leitorAlimentar(l, *c)); + const char* resto = "CA:A\r\n"; + bool pronto = false; + for (const char* c = resto; *c; c++) pronto = leitorAlimentar(l, *c); + VERIFICA(pronto); + VERIFICA_TEXTO(l.buf, "CMD:PECA:A"); +} + +TESTE(leitor_descarta_linha_longa_inteira) { + LeitorLinha l; + leitorIniciar(l); + for (int i = 0; i < 50; i++) VERIFICA(!leitorAlimentar(l, 'x')); + VERIFICA(!leitorAlimentar(l, '\n')); // a linha longa some inteira + const char* prox = "CMD:RESET\n"; + bool pronto = false; + for (const char* c = prox; *c; c++) pronto = leitorAlimentar(l, *c); + VERIFICA(pronto); + VERIFICA_TEXTO(l.buf, "CMD:RESET"); +} +``` + +- [ ] **Passo 3: Rodar e ver falhar** + +Rode (na raiz do repositório, Git Bash): + +```bash +MSYS_NO_PATHCONV=1 docker run --rm -v "$(cygpath -w "$PWD"):/w" -w /w gcc:14 make -C test/firmware_uno test +``` + +Esperado: erro de compilação `protocolo.h: No such file or directory`. + +- [ ] **Passo 4: Implementar** + +Crie `arduino/data_flow_inventory/src/logica/texto_flash.h` com exatamente este conteúdo: + +```cpp +// ============================================================ +// DATA FLOW INVENTORY — Textos fixos na memória flash +// ------------------------------------------------------------ +// No AVR, um literal "..." ocupa RAM (o Uno só tem 2 KB). TXT("...") +// grava o texto na flash (PROGMEM) e lerTxt() lê um caractere dele. +// No PC (testes) os dois viram o comportamento normal de C++. +// Convenção: todo `const char*` de texto FIXO nas APIs de src/logica +// é um TXT (flash no AVR). Textos montados em RAM (buffers) não são. +// ============================================================ +#pragma once + +#if defined(__AVR__) +#include +#define TXT(s) PSTR(s) +inline char lerTxt(const char* p) { return (char)pgm_read_byte(p); } +#else +#define TXT(s) (s) +inline char lerTxt(const char* p) { return *p; } +#endif +``` + +Crie `arduino/data_flow_inventory/src/logica/protocolo.h` com exatamente este conteúdo: + +```cpp +// ============================================================ +// DATA FLOW INVENTORY — Protocolo serial Uno ↔ ESP32 (lógica pura) +// ------------------------------------------------------------ +// Interpreta comandos (CMD:PECA:X, CMD:RESET) e formata as mensagens +// JSON publicadas pelo Uno. C++ puro, sem Arduino.h: testado no PC +// (test/firmware_uno). O formato de cada mensagem é idêntico ao do +// firmware v2.x (ArduinoJson): mesma ordem de campos, sem espaços. +// Campos novos são sempre OPCIONAIS (só saem quando informados). +// Textos fixos passados às funções formatar* (estado, tipo, fase, versão, +// reset) devem ser TXT(...) — ver texto_flash.h. +// ============================================================ +#pragma once +#include +#include + +// ---- Comandos ----------------------------------------------------------- +enum ComandoTipo : uint8_t { + CMD_NENHUM = 0, // linha vazia + CMD_PECA, // CMD:PECA:A|B|C (peça válida em *peca) + CMD_PECA_INVALIDA, // CMD:PECA: com letra/forma inválida + CMD_RESET, // CMD:RESET + CMD_DESCONHECIDO // qualquer outra linha +}; + +// Interpreta uma linha (sem '\n'). Ignora espaços nas pontas. +// Em CMD_PECA, *peca recebe 'A', 'B' ou 'C'; nos demais casos, 0. +ComandoTipo interpretarComando(const char* linha, char* peca); + +// Acumula bytes da serial até '\n'. '\r' é ignorado. Linha maior que +// a capacidade é DESCARTADA inteira (até o próximo '\n'). +struct LeitorLinha { + static const uint8_t CAPACIDADE = 32; + char buf[CAPACIDADE + 1]; + uint8_t n; + bool descartando; +}; +void leitorIniciar(LeitorLinha& l); +// Devolve true quando uma linha completa está em l.buf (terminada em 0). +bool leitorAlimentar(LeitorLinha& l, char c); + +// ---- Mensagens -------------------------------------------------------------- +// Todas escrevem em buf (capacidade cap), terminam em 0 e devolvem o +// tamanho escrito (sem o 0), ou 0 se não couber. +// Valores "opcionais": passe -1 (inteiros) ou nullptr (texto) para omitir. +size_t formatarStatus(char* buf, size_t cap, const char* estado, + int pecaSolicitada, uint32_t uptimeS, long ramLivre); +size_t formatarEstoque(char* buf, size_t cap, const int estoque[3]); +size_t formatarSensores(char* buf, size_t cap, const bool topo[3], + const bool juncao[3]); +size_t formatarEsteiras(char* buf, size_t cap, const bool secundaria[3]); +size_t formatarPedido(char* buf, size_t cap, char peca); +size_t formatarEntrega(char* buf, size_t cap, char peca, const int estoque[3]); +// peca = 0 omite o campo; fase = nullptr omite; tMs/bordas = -1 omitem. +size_t formatarErro(char* buf, size_t cap, const char* tipo, char peca, + const char* fase, long tMs, long bordas); +// reset = nullptr omite; lcd = -1 omite (0/1 → false/true); ramLivre = -1 omite. +size_t formatarInicio(char* buf, size_t cap, const char* versao, + const char* reset, int lcd, long ramLivre); +``` + +Crie `arduino/data_flow_inventory/src/logica/protocolo.cpp` com exatamente este conteúdo: + +```cpp +#include "protocolo.h" + +#include + +#include "texto_flash.h" + +// ---- Comandos ----------------------------------------------------------- + +static bool ehEspaco(char c) { return c == ' ' || c == '\t' || c == '\r' || c == '\n'; } + +// ram[0..n) é igual ao texto TXT (inteiro)? +static bool igualTxt(const char* ram, size_t n, const char* txt) { + size_t i = 0; + for (; i < n; i++) { + char c = lerTxt(txt + i); + if (c == 0 || c != ram[i]) return false; + } + return lerTxt(txt + i) == 0; +} + +ComandoTipo interpretarComando(const char* linha, char* peca) { + *peca = 0; + const char* ini = linha; + while (*ini && ehEspaco(*ini)) ini++; + size_t n = strlen(ini); + while (n > 0 && ehEspaco(ini[n - 1])) n--; + if (n == 0) return CMD_NENHUM; + + const size_t TAM_PREFIXO = 9; // "CMD:PECA:" + if (n >= TAM_PREFIXO && igualTxt(ini, TAM_PREFIXO, TXT("CMD:PECA:"))) { + if (n == TAM_PREFIXO + 1) { + char c = ini[TAM_PREFIXO]; + if (c == 'A' || c == 'B' || c == 'C') { + *peca = c; + return CMD_PECA; + } + } + return CMD_PECA_INVALIDA; + } + if (igualTxt(ini, n, TXT("CMD:RESET"))) return CMD_RESET; + return CMD_DESCONHECIDO; +} + +void leitorIniciar(LeitorLinha& l) { + l.n = 0; + l.buf[0] = 0; + l.descartando = false; +} + +bool leitorAlimentar(LeitorLinha& l, char c) { + if (c == '\r') return false; + if (c == '\n') { + if (l.descartando) { + leitorIniciar(l); + return false; + } + l.buf[l.n] = 0; + l.n = 0; + return true; + } + if (l.descartando) return false; + if (l.n >= LeitorLinha::CAPACIDADE) { + l.descartando = true; + l.n = 0; + l.buf[0] = 0; + return false; + } + l.buf[l.n++] = c; + return false; +} + +// ---- Escritor de JSON (sem printf: menos flash no AVR) --------------------- + +namespace { +struct Escritor { + char* b; + size_t cap; + size_t n; + bool ok; +}; + +void iniciar(Escritor& e, char* b, size_t cap) { + e.b = b; + e.cap = cap; + e.n = 0; + e.ok = cap > 0; + if (e.ok) b[0] = 0; +} + +void car(Escritor& e, char c) { + if (!e.ok) return; + if (e.n + 1 >= e.cap) { e.ok = false; return; } + e.b[e.n++] = c; + e.b[e.n] = 0; +} + +// Texto fixo (TXT: flash no AVR). +void txt(Escritor& e, const char* s) { + for (char c = lerTxt(s); c != 0; c = lerTxt(++s)) car(e, c); +} + +void num(Escritor& e, long v) { + char tmp[12]; + int i = 0; + bool neg = v < 0; + unsigned long u = neg ? (unsigned long)(-(v + 1)) + 1UL : (unsigned long)v; + do { tmp[i++] = (char)('0' + (u % 10)); u /= 10; } while (u > 0); + if (neg) car(e, '-'); + while (i > 0) car(e, tmp[--i]); +} + +void numU(Escritor& e, uint32_t v) { + char tmp[11]; + int i = 0; + do { tmp[i++] = (char)('0' + (v % 10)); v /= 10; } while (v > 0); + while (i > 0) car(e, tmp[--i]); +} + +// ,"chave": (k é TXT) +void chave(Escritor& e, const char* k, bool primeira = false) { + if (!primeira) car(e, ','); + car(e, '"'); + txt(e, k); + car(e, '"'); + car(e, ':'); +} + +void campoTexto(Escritor& e, const char* k, const char* v, bool primeira = false) { + chave(e, k, primeira); + car(e, '"'); + txt(e, v); + car(e, '"'); +} + +void campoLetra(Escritor& e, const char* k, char v) { + chave(e, k); + car(e, '"'); + car(e, v); + car(e, '"'); +} + +void campoNum(Escritor& e, const char* k, long v) { + chave(e, k); + num(e, v); +} + +size_t fim(Escritor& e) { + car(e, '}'); + if (!e.ok) { + if (e.cap > 0) e.b[0] = 0; + return 0; + } + return e.n; +} + +void inicioMsg(Escritor& e, char* buf, size_t cap, const char* type) { + iniciar(e, buf, cap); + car(e, '{'); + campoTexto(e, TXT("type"), type, true); +} +} // namespace + +// ---- Mensagens -------------------------------------------------------------- + +size_t formatarStatus(char* buf, size_t cap, const char* estado, + int pecaSolicitada, uint32_t uptimeS, long ramLivre) { + Escritor e; + inicioMsg(e, buf, cap, TXT("status")); + campoTexto(e, TXT("estado"), estado); + campoNum(e, TXT("pecaSolicitada"), pecaSolicitada); + chave(e, TXT("uptime")); + numU(e, uptimeS); + if (ramLivre >= 0) campoNum(e, TXT("ram_livre"), ramLivre); + return fim(e); +} + +size_t formatarEstoque(char* buf, size_t cap, const int estoque[3]) { + Escritor e; + inicioMsg(e, buf, cap, TXT("estoque")); + campoNum(e, TXT("pecaA"), estoque[0]); + campoNum(e, TXT("pecaB"), estoque[1]); + campoNum(e, TXT("pecaC"), estoque[2]); + return fim(e); +} + +size_t formatarSensores(char* buf, size_t cap, const bool topo[3], + const bool juncao[3]) { + Escritor e; + inicioMsg(e, buf, cap, TXT("sensores")); + chave(e, TXT("topo")); + car(e, '{'); + chave(e, TXT("A"), true); num(e, topo[0] ? 1 : 0); + chave(e, TXT("B")); num(e, topo[1] ? 1 : 0); + chave(e, TXT("C")); num(e, topo[2] ? 1 : 0); + car(e, '}'); + chave(e, TXT("juncao")); + car(e, '{'); + chave(e, TXT("J1"), true); num(e, juncao[0] ? 1 : 0); + chave(e, TXT("J2")); num(e, juncao[1] ? 1 : 0); + chave(e, TXT("J3")); num(e, juncao[2] ? 1 : 0); + car(e, '}'); + return fim(e); +} + +size_t formatarEsteiras(char* buf, size_t cap, const bool secundaria[3]) { + Escritor e; + inicioMsg(e, buf, cap, TXT("esteiras")); + campoNum(e, TXT("principal"), 1); // sempre ligada (direto na fonte) + campoNum(e, TXT("secA"), secundaria[0] ? 1 : 0); + campoNum(e, TXT("secB"), secundaria[1] ? 1 : 0); + campoNum(e, TXT("secC"), secundaria[2] ? 1 : 0); + return fim(e); +} + +size_t formatarPedido(char* buf, size_t cap, char peca) { + Escritor e; + inicioMsg(e, buf, cap, TXT("evento")); + campoTexto(e, TXT("evento"), TXT("pedido")); + campoLetra(e, TXT("peca"), peca); + return fim(e); +} + +size_t formatarEntrega(char* buf, size_t cap, char peca, const int estoque[3]) { + Escritor e; + inicioMsg(e, buf, cap, TXT("evento")); + campoTexto(e, TXT("evento"), TXT("entrega")); + campoLetra(e, TXT("peca"), peca); + campoNum(e, TXT("estoqueA"), estoque[0]); + campoNum(e, TXT("estoqueB"), estoque[1]); + campoNum(e, TXT("estoqueC"), estoque[2]); + return fim(e); +} + +size_t formatarErro(char* buf, size_t cap, const char* tipo, char peca, + const char* fase, long tMs, long bordas) { + Escritor e; + inicioMsg(e, buf, cap, TXT("evento")); + campoTexto(e, TXT("evento"), TXT("erro")); + campoTexto(e, TXT("tipo"), tipo); + if (peca != 0) campoLetra(e, TXT("peca"), peca); + if (fase != nullptr) campoTexto(e, TXT("fase"), fase); + if (tMs >= 0) campoNum(e, TXT("t_ms"), tMs); + if (bordas >= 0) campoNum(e, TXT("bordas_juncao"), bordas); + return fim(e); +} + +size_t formatarInicio(char* buf, size_t cap, const char* versao, + const char* reset, int lcd, long ramLivre) { + Escritor e; + inicioMsg(e, buf, cap, TXT("evento")); + campoTexto(e, TXT("evento"), TXT("inicio")); + campoTexto(e, TXT("msg"), TXT("Sistema iniciado")); + campoTexto(e, TXT("versao"), versao); + campoTexto(e, TXT("driver"), TXT("IRF520")); + if (reset != nullptr) campoTexto(e, TXT("reset"), reset); + if (lcd >= 0) { + chave(e, TXT("lcd")); + txt(e, lcd ? TXT("true") : TXT("false")); + } + if (ramLivre >= 0) campoNum(e, TXT("ram_livre"), ramLivre); + return fim(e); +} +``` + +- [ ] **Passo 5: Rodar e ver passar** + +Rode (na raiz do repositório, Git Bash): + +```bash +MSYS_NO_PATHCONV=1 docker run --rm -v "$(cygpath -w "$PWD"):/w" -w /w gcc:14 make -C test/firmware_uno test +``` + +Esperado: `17 testes, ... 0 falhas` (os testes `*_igual_v2` provam que o formato não mudou). + +- [ ] **Passo 6: Job de CI** + +Em `.github/workflows/lint-and-security.yaml`, acrescente este job ao final da seção `jobs:` (mesma indentação dos outros jobs; não altere os existentes): + +```yaml + firmware-logica: + runs-on: ubuntu-latest + steps: + - name: Checkout código + uses: actions/checkout@v4 + + - name: Testes da lógica do firmware do Uno (g++ no PC) + run: make -C test/firmware_uno test +``` + +- [ ] **Passo 7: Commit** + +```bash +git add test/firmware_uno arduino/data_flow_inventory/src/logica .gitignore .github/workflows/lint-and-security.yaml +git commit -m "test(firmware): protocolo serial em logica pura com contrato do formato v2.x + +Co-Authored-By: Claude Opus 5.5 " +``` + +--- + +### Tarefa 2: Filtro de entrada e causa do reset + +**Arquivos:** +- Criar: `arduino/data_flow_inventory/src/logica/filtro_entrada.h/.cpp`, `causa_reset.h/.cpp` +- Teste: `test/firmware_uno/teste_filtro_reset.cpp` + +**Interfaces:** +- Consome: `TXT()` (Tarefa 1). +- Produz: `struct FiltroEntrada { bool estavel; bool bruto; uint32_t mudouEm; uint16_t janelaMs; }`, `enum Borda`, `filtroIniciar(f, valor, agora, janelaMs)`, `Borda filtroAtualizar(f, bruto, agora)`; constantes `MCUSR_PORF/EXTRF/BORF/WDRF` e `const char* causaReset(uint8_t mcusr, bool assinaturaValida, bool marcaWatchdog)` (devolve TXT). + +- [ ] **Passo 1: Escrever os testes (falham)** + +Crie `test/firmware_uno/teste_filtro_reset.cpp` com exatamente este conteúdo: + +```cpp +// Filtro de entrada (debounce) e causa do reset. +#include "causa_reset.h" +#include "filtro_entrada.h" +#include "teste.h" + +// ---- Filtro ------------------------------------------------------------------ + +TESTE(filtro_ignora_pulso_curto) { + FiltroEntrada f; + filtroIniciar(f, false, 0, 20); + VERIFICA(filtroAtualizar(f, true, 100) == BORDA_NENHUMA); + VERIFICA(filtroAtualizar(f, true, 110) == BORDA_NENHUMA); // 10 ms + VERIFICA(filtroAtualizar(f, false, 115) == BORDA_NENHUMA); // voltou + VERIFICA(filtroAtualizar(f, false, 200) == BORDA_NENHUMA); + VERIFICA(!f.estavel); +} + +TESTE(filtro_aceita_nivel_estavel_e_gera_bordas) { + FiltroEntrada f; + filtroIniciar(f, false, 0, 20); + filtroAtualizar(f, true, 100); + VERIFICA(filtroAtualizar(f, true, 120) == BORDA_OCUPOU); + VERIFICA(f.estavel); + filtroAtualizar(f, false, 300); + VERIFICA(filtroAtualizar(f, false, 330) == BORDA_LIBEROU); +} + +TESTE(filtro_funciona_no_estouro_do_millis) { + FiltroEntrada f; + const uint32_t quase = 0xFFFFFFF0UL; + filtroIniciar(f, false, quase, 20); + filtroAtualizar(f, true, quase + 5); + VERIFICA(filtroAtualizar(f, true, quase + 30) == BORDA_OCUPOU); // passou de 0 +} + +// ---- Causa do reset ------------------------------------------------------------ + +TESTE(causa_reset) { + VERIFICA_TEXTO(causaReset(MCUSR_WDRF, false, false), "watchdog"); + VERIFICA_TEXTO(causaReset(0, true, true), "watchdog"); + VERIFICA_TEXTO(causaReset(MCUSR_BORF, true, false), "brownout"); + VERIFICA_TEXTO(causaReset(MCUSR_PORF, true, false), "energia"); + VERIFICA_TEXTO(causaReset(0, false, false), "energia"); + VERIFICA_TEXTO(causaReset(0, true, false), "reinicio"); + VERIFICA_TEXTO(causaReset(MCUSR_EXTRF, true, false), "reinicio"); + VERIFICA_TEXTO(causaReset(0, false, true), "energia"); // marca sem assinatura não vale +} +``` + +- [ ] **Passo 2: Rodar e ver falhar** + +Rode (na raiz do repositório, Git Bash): + +```bash +MSYS_NO_PATHCONV=1 docker run --rm -v "$(cygpath -w "$PWD"):/w" -w /w gcc:14 make -C test/firmware_uno test +``` + +Esperado: erro de compilação (`causa_reset.h`/`filtro_entrada.h` não existem). + +- [ ] **Passo 3: Implementar** + +Crie `arduino/data_flow_inventory/src/logica/filtro_entrada.h` com exatamente este conteúdo: + +```cpp +// ============================================================ +// DATA FLOW INVENTORY — Filtro de entrada digital (lógica pura) +// ------------------------------------------------------------ +// Debounce por tempo: a leitura bruta só vira "estável" depois de +// permanecer igual por janelaMs. Usado nos sensores de topo e para o +// nível atual das junções. C++ puro (testado no PC). +// ============================================================ +#pragma once +#include + +struct FiltroEntrada { + bool estavel; // valor filtrado (true = sensor ocupado) + bool bruto; // última leitura bruta + uint32_t mudouEm; // instante da última mudança da leitura bruta + uint16_t janelaMs; // tempo mínimo de estabilidade +}; + +enum Borda : int8_t { BORDA_NENHUMA = 0, BORDA_OCUPOU = 1, BORDA_LIBEROU = -1 }; + +void filtroIniciar(FiltroEntrada& f, bool valor, uint32_t agora, uint16_t janelaMs); + +// Atualiza com a leitura bruta atual e devolve a borda do valor ESTÁVEL +// (se houve). Robusto ao estouro de millis() (aritmética sem sinal). +Borda filtroAtualizar(FiltroEntrada& f, bool bruto, uint32_t agora); +``` + +Crie `arduino/data_flow_inventory/src/logica/filtro_entrada.cpp` com exatamente este conteúdo: + +```cpp +#include "filtro_entrada.h" + +void filtroIniciar(FiltroEntrada& f, bool valor, uint32_t agora, uint16_t janelaMs) { + f.estavel = valor; + f.bruto = valor; + f.mudouEm = agora; + f.janelaMs = janelaMs; +} + +Borda filtroAtualizar(FiltroEntrada& f, bool bruto, uint32_t agora) { + if (bruto != f.bruto) { + f.bruto = bruto; + f.mudouEm = agora; + return BORDA_NENHUMA; + } + if (bruto != f.estavel && (uint32_t)(agora - f.mudouEm) >= f.janelaMs) { + f.estavel = bruto; + return bruto ? BORDA_OCUPOU : BORDA_LIBEROU; + } + return BORDA_NENHUMA; +} +``` + +Crie `arduino/data_flow_inventory/src/logica/causa_reset.h` com exatamente este conteúdo: + +```cpp +// ============================================================ +// DATA FLOW INVENTORY — Causa do último reset (lógica pura) +// ------------------------------------------------------------ +// O Optiboot do Uno costuma zerar o MCUSR antes do sketch; por isso a +// causa combina: o MCUSR (quando vier preenchido), uma assinatura em +// RAM não inicializada (.noinit, some quando a energia cai) e a marca +// gravada pela interrupção do watchdog. C++ puro (testado no PC). +// ============================================================ +#pragma once +#include + +// Bits do MCUSR do ATmega328P. +static const uint8_t MCUSR_PORF = 0x01; // energização +static const uint8_t MCUSR_EXTRF = 0x02; // pino de reset +static const uint8_t MCUSR_BORF = 0x04; // brownout +static const uint8_t MCUSR_WDRF = 0x08; // watchdog + +// Devolve TXT("watchdog"), TXT("brownout"), TXT("energia") ou TXT("reinicio"). +const char* causaReset(uint8_t mcusr, bool assinaturaValida, bool marcaWatchdog); +``` + +Crie `arduino/data_flow_inventory/src/logica/causa_reset.cpp` com exatamente este conteúdo: + +```cpp +#include "causa_reset.h" + +#include "texto_flash.h" + +const char* causaReset(uint8_t mcusr, bool assinaturaValida, bool marcaWatchdog) { + if ((mcusr & MCUSR_WDRF) || (assinaturaValida && marcaWatchdog)) return TXT("watchdog"); + if (mcusr & MCUSR_BORF) return TXT("brownout"); + if ((mcusr & MCUSR_PORF) || !assinaturaValida) return TXT("energia"); + return TXT("reinicio"); +} +``` + +- [ ] **Passo 4: Rodar e ver passar** + +Rode (na raiz do repositório, Git Bash): + +```bash +MSYS_NO_PATHCONV=1 docker run --rm -v "$(cygpath -w "$PWD"):/w" -w /w gcc:14 make -C test/firmware_uno test +``` + +Esperado: `21 testes, ... 0 falhas`. + +- [ ] **Passo 5: Commit** + +```bash +git add test/firmware_uno arduino/data_flow_inventory/src/logica +git commit -m "feat(firmware): filtro de entrada e causa do reset em logica pura + +Co-Authored-By: Claude Opus 5.5 " +``` + +--- + +### Tarefa 3: Supervisor de entrega por marcos + +**Arquivos:** +- Criar: `arduino/data_flow_inventory/src/logica/supervisor_entrega.h/.cpp` +- Teste: `test/firmware_uno/teste_supervisor.cpp` + +**Interfaces:** +- Produz: `struct ParamEsteira { uint8_t pwmRegime; uint16_t kickMs, prazoPartidaMs, timeoutMs, saidaMs, pulsoMinJuncaoMs; }` (nesta ordem — o `config.h` da Tarefa 6 usa inicialização por chaves nessa ordem); `struct LeituraJuncao { bool ocupada; uint32_t ocupouEm; uint16_t maiorPulsoMs; uint8_t bordas; }`; `enum FaseEntrega { FASE_PARTIDA, FASE_TRANSITO, FASE_SAIDA, FASE_FIM }`; `enum ResultadoSupervisor { SUP_SEGUE, SUP_CONFIRMOU, SUP_SAIDA_OK, SUP_FALHA_SEM_AVANCO, SUP_FALHA_TIMEOUT, SUP_FALHA_PRESA_SAIDA }`; `struct Supervisor { ParamEsteira p; FaseEntrega fase; uint32_t inicioMs; uint32_t confirmouEm; }`; `supervisorIniciar(s, p, agora)`; `ResultadoSupervisor supervisorAtualizar(s, agora, topoOcupado, const LeituraJuncao& j)`; `bool passagemValida(j, agora, pulsoMinMs)`. + +**Regras (spec §4.1):** prazos contados da partida do motor com aritmética sem sinal; uma passagem válida na junção confirma mesmo antes de o topo liberar; o caso P1 (junção nunca muda) termina em `SUP_FALHA_TIMEOUT`; o P2 (topo não libera) em `SUP_FALHA_SEM_AVANCO`. + +- [ ] **Passo 1: Escrever os testes (falham)** + +Crie `test/firmware_uno/teste_supervisor.cpp` com exatamente este conteúdo: + +```cpp +// Supervisor de entrega por marcos (M1 partida, M2 trânsito, M3 saída). +#include "supervisor_entrega.h" +#include "teste.h" + +// ---- Supervisor --------------------------------------------------------------- + +static ParamEsteira paramPadrao() { + ParamEsteira p; + p.pwmRegime = 200; + p.kickMs = 150; + p.prazoPartidaMs = 3000; + p.timeoutMs = 12500; + p.saidaMs = 3000; + p.pulsoMinJuncaoMs = 20; + return p; +} + +static LeituraJuncao livre() { + LeituraJuncao j; + j.ocupada = false; + j.ocupouEm = 0; + j.maiorPulsoMs = 0; + j.bordas = 0; + return j; +} + +TESTE(supervisor_m1_motor_sem_avanco) { + Supervisor s; + supervisorIniciar(s, paramPadrao(), 1000); + LeituraJuncao j = livre(); + VERIFICA(supervisorAtualizar(s, 3999, true, j) == SUP_SEGUE); + VERIFICA(supervisorAtualizar(s, 4000, true, j) == SUP_FALHA_SEM_AVANCO); +} + +TESTE(supervisor_m2_timeout_sem_bordas) { + Supervisor s; + supervisorIniciar(s, paramPadrao(), 0); + LeituraJuncao j = livre(); + VERIFICA(supervisorAtualizar(s, 1000, false, j) == SUP_SEGUE); // topo liberou + VERIFICA(s.fase == FASE_TRANSITO); + VERIFICA(supervisorAtualizar(s, 12499, false, j) == SUP_SEGUE); + VERIFICA(supervisorAtualizar(s, 12500, false, j) == SUP_FALHA_TIMEOUT); +} + +TESTE(supervisor_confirma_por_pulso_completo_capturado) { + Supervisor s; + supervisorIniciar(s, paramPadrao(), 0); + LeituraJuncao j = livre(); + supervisorAtualizar(s, 1000, false, j); + j.maiorPulsoMs = 25; // a interrupção viu um pulso de 25 ms entre dois ciclos + j.bordas = 2; + VERIFICA(supervisorAtualizar(s, 5000, false, j) == SUP_CONFIRMOU); + VERIFICA(s.fase == FASE_SAIDA); +} + +TESTE(supervisor_ignora_pulso_curto_de_ruido) { + Supervisor s; + supervisorIniciar(s, paramPadrao(), 0); + LeituraJuncao j = livre(); + supervisorAtualizar(s, 1000, false, j); + j.maiorPulsoMs = 5; + j.bordas = 2; + VERIFICA(supervisorAtualizar(s, 2000, false, j) == SUP_SEGUE); + j.ocupada = true; + j.ocupouEm = 2000; + VERIFICA(supervisorAtualizar(s, 2010, false, j) == SUP_SEGUE); // 10 ms + VERIFICA(supervisorAtualizar(s, 2020, false, j) == SUP_CONFIRMOU); // 20 ms +} + +TESTE(supervisor_m3_saida_ok_e_peca_presa) { + Supervisor s; + supervisorIniciar(s, paramPadrao(), 0); + LeituraJuncao j = livre(); + j.maiorPulsoMs = 30; + VERIFICA(supervisorAtualizar(s, 4000, false, j) == SUP_CONFIRMOU); + VERIFICA(supervisorAtualizar(s, 6999, false, j) == SUP_SEGUE); + VERIFICA(supervisorAtualizar(s, 7000, false, j) == SUP_SAIDA_OK); + + supervisorIniciar(s, paramPadrao(), 0); + LeituraJuncao k = livre(); + k.ocupada = true; + k.ocupouEm = 4000; + VERIFICA(supervisorAtualizar(s, 4030, false, k) == SUP_CONFIRMOU); + VERIFICA(supervisorAtualizar(s, 7030, false, k) == SUP_FALHA_PRESA_SAIDA); +} + +TESTE(supervisor_timeout_conta_desde_a_partida_no_estouro_do_millis) { + Supervisor s; + const uint32_t ini = 0xFFFFF000UL; + supervisorIniciar(s, paramPadrao(), ini); + LeituraJuncao j = livre(); + supervisorAtualizar(s, ini + 500, false, j); + VERIFICA(supervisorAtualizar(s, ini + 12499, false, j) == SUP_SEGUE); + VERIFICA(supervisorAtualizar(s, ini + 12500, false, j) == SUP_FALHA_TIMEOUT); +} +``` + +- [ ] **Passo 2: Rodar e ver falhar** + +Rode (na raiz do repositório, Git Bash): + +```bash +MSYS_NO_PATHCONV=1 docker run --rm -v "$(cygpath -w "$PWD"):/w" -w /w gcc:14 make -C test/firmware_uno test +``` + +Esperado: erro de compilação (`supervisor_entrega.h` não existe). + +- [ ] **Passo 3: Implementar** + +Crie `arduino/data_flow_inventory/src/logica/supervisor_entrega.h` com exatamente este conteúdo: + +```cpp +// ============================================================ +// DATA FLOW INVENTORY — Supervisor de entrega por marcos (lógica pura) +// ------------------------------------------------------------ +// M1 Partida : o sensor de topo fica livre em até prazoPartidaMs. +// M2 Trânsito: a junção registra uma passagem válida (pulso ≥ +// pulsoMinJuncaoMs) em até timeoutMs desde a partida. +// M3 Saída : depois da confirmação, o motor segue saidaMs; no fim a +// junção precisa estar livre. +// Todos os prazos contam com aritmética sem sinal (estouro de millis). +// C++ puro (testado no PC). +// ============================================================ +#pragma once +#include + +struct ParamEsteira { + uint8_t pwmRegime; // PWM de regime (0-255) + uint16_t kickMs; // duração do kick-start em PWM 255 + uint16_t prazoPartidaMs; // M1 + uint16_t timeoutMs; // M2 (desde a partida) + uint16_t saidaMs; // M3 + uint16_t pulsoMinJuncaoMs; // largura mínima de uma passagem válida +}; + +// Leitura de um sensor de junção, montada pela camada de hardware a +// partir das bordas capturadas por interrupção. +struct LeituraJuncao { + bool ocupada; // nível atual (true = peça sobre o sensor) + uint32_t ocupouEm; // instante em que ficou ocupada (vale se ocupada) + uint16_t maiorPulsoMs; // maior pulso COMPLETO desde o último zerar + uint8_t bordas; // mudanças de nível desde o último zerar +}; + +enum FaseEntrega : uint8_t { FASE_PARTIDA, FASE_TRANSITO, FASE_SAIDA, FASE_FIM }; + +enum ResultadoSupervisor : uint8_t { + SUP_SEGUE = 0, + SUP_CONFIRMOU, // passagem válida na junção (debitar agora) + SUP_SAIDA_OK, // fim da saída com a junção livre + SUP_FALHA_SEM_AVANCO, // M1 + SUP_FALHA_TIMEOUT, // M2 + SUP_FALHA_PRESA_SAIDA // M3 +}; + +struct Supervisor { + ParamEsteira p; + FaseEntrega fase; + uint32_t inicioMs; + uint32_t confirmouEm; +}; + +void supervisorIniciar(Supervisor& s, const ParamEsteira& p, uint32_t agora); +ResultadoSupervisor supervisorAtualizar(Supervisor& s, uint32_t agora, + bool topoOcupado, const LeituraJuncao& j); +// Passagem válida: pulso completo largo o bastante ou ocupada há tempo suficiente. +bool passagemValida(const LeituraJuncao& j, uint32_t agora, uint16_t pulsoMinMs); +``` + +Crie `arduino/data_flow_inventory/src/logica/supervisor_entrega.cpp` com exatamente este conteúdo: + +```cpp +#include "supervisor_entrega.h" + +void supervisorIniciar(Supervisor& s, const ParamEsteira& p, uint32_t agora) { + s.p = p; + s.fase = FASE_PARTIDA; + s.inicioMs = agora; + s.confirmouEm = agora; +} + +bool passagemValida(const LeituraJuncao& j, uint32_t agora, uint16_t pulsoMinMs) { + if (j.maiorPulsoMs >= pulsoMinMs) return true; + return j.ocupada && (uint32_t)(agora - j.ocupouEm) >= pulsoMinMs; +} + +ResultadoSupervisor supervisorAtualizar(Supervisor& s, uint32_t agora, + bool topoOcupado, const LeituraJuncao& j) { + const uint32_t decorrido = (uint32_t)(agora - s.inicioMs); + + if (s.fase == FASE_PARTIDA || s.fase == FASE_TRANSITO) { + // Uma passagem válida prova o avanço mesmo antes de o filtro do topo liberar. + if (passagemValida(j, agora, s.p.pulsoMinJuncaoMs)) { + s.fase = FASE_SAIDA; + s.confirmouEm = agora; + return SUP_CONFIRMOU; + } + if (s.fase == FASE_PARTIDA) { + if (!topoOcupado) { + s.fase = FASE_TRANSITO; + } else if (decorrido >= s.p.prazoPartidaMs) { + s.fase = FASE_FIM; + return SUP_FALHA_SEM_AVANCO; + } + } + if (s.fase == FASE_TRANSITO && decorrido >= s.p.timeoutMs) { + s.fase = FASE_FIM; + return SUP_FALHA_TIMEOUT; + } + return SUP_SEGUE; + } + + if (s.fase == FASE_SAIDA) { + if ((uint32_t)(agora - s.confirmouEm) >= s.p.saidaMs) { + s.fase = FASE_FIM; + // "Presa" = ainda ocupada e há tempo suficiente para não ser ruído. + bool presa = j.ocupada && (uint32_t)(agora - j.ocupouEm) >= s.p.pulsoMinJuncaoMs; + return presa ? SUP_FALHA_PRESA_SAIDA : SUP_SAIDA_OK; + } + return SUP_SEGUE; + } + return SUP_SEGUE; +} +``` + +- [ ] **Passo 4: Rodar e ver passar** + +Rode (na raiz do repositório, Git Bash): + +```bash +MSYS_NO_PATHCONV=1 docker run --rm -v "$(cygpath -w "$PWD"):/w" -w /w gcc:14 make -C test/firmware_uno test +``` + +Esperado: `27 testes, ... 0 falhas`. + +- [ ] **Passo 5: Commit** + +```bash +git add test/firmware_uno arduino/data_flow_inventory/src/logica +git commit -m "feat(firmware): supervisor de entrega por marcos (partida, transito, saida) + +Co-Authored-By: Claude Opus 5.5 " +``` + +--- + +### Tarefa 4: Máquina de estados + +**Arquivos:** +- Criar: `arduino/data_flow_inventory/src/logica/fsm.h/.cpp` +- Teste: `test/firmware_uno/teste_fsm.cpp` + +**Interfaces:** +- Consome: `protocolo.h` (Tarefa 1), `supervisor_entrega.h` (Tarefa 3), `TXT()`. +- Produz: `enum Estado`, `enum TipoErro` (ERR_*), `enum FaseErro` (FERR_*), `nomeEstado/nomeErro/nomeFaseErro` (TXT; `nomeFaseErro(FERR_NENHUMA)` = `nullptr`), `enum TelaLcd`, `enum TipoEvento`, `struct Evento`, `struct EntradasFsm`, `struct SaidasFsm` (até 4 eventos + `zerarJuncao`), `struct Fsm`, `fsmIniciar(f, params[3], estoqueInicial)`, `fsmPasso(f, in, out)`, `bool fsmMotorLigado(f, idx)`, `char letraPeca(uint8_t)`. + +**Regras (spec §4):** checagem prévia (estoque → topo → junção) vai para ERRO sem ligar motor; débito só em `SUP_CONFIRMOU`; em ERRO nenhum motor fica ligado; `ocupado` traz a peça pedida; RESET só vale em ERRO e republica estado e estoque; sequência de eventos igual à do v2.x (pedido → status ACIONANDO → esteiras ...). + +- [ ] **Passo 1: Escrever os testes (falham)** + +Crie `test/firmware_uno/teste_fsm.cpp` com exatamente este conteúdo: + +```cpp +// Cenários da FSM com tempo simulado (casos de bancada P1–P5 inclusos). +#include "fsm.h" +#include "teste.h" + +namespace { + +ParamEsteira param() { + ParamEsteira p; + p.pwmRegime = 200; + p.kickMs = 150; + p.prazoPartidaMs = 3000; + p.timeoutMs = 12500; + p.saidaMs = 3000; + p.pulsoMinJuncaoMs = 20; + return p; +} + +// Bancada simulada: guarda as entradas e registra o que a FSM publicou. +struct Bancada { + Fsm f; + EntradasFsm in; + SaidasFsm out; + char log[2048]; // eventos publicados, um por linha + int debitos; // vezes em que o estoque diminuiu + int confirmacoes; // vezes em que houve EV_ENTREGA + + Bancada() { + ParamEsteira p[3] = {param(), param(), param()}; + fsmIniciar(f, p, 15); + memset(&in, 0, sizeof in); + for (int i = 0; i < 3; i++) in.topo[i] = true; // uma peça em cada topo + log[0] = 0; + debitos = 0; + confirmacoes = 0; + } + + void anota(const char* s) { + strncat(log, s, sizeof log - strlen(log) - 1); + strncat(log, "\n", sizeof log - strlen(log) - 1); + } + + // Um ciclo do loop no instante t (ms). + void passo(uint32_t t, ComandoTipo cmd = CMD_NENHUM, char peca = 0) { + in.agora = t; + in.cmd = cmd; + in.cmdPeca = peca; + int antes = f.estoque[0] + f.estoque[1] + f.estoque[2]; + fsmPasso(f, in, out); + int depois = f.estoque[0] + f.estoque[1] + f.estoque[2]; + debitos += antes - depois; + for (uint8_t i = 0; i < out.nEventos; i++) { + const Evento& e = out.eventos[i]; + char linha[64]; + switch (e.tipo) { + case EV_PEDIDO: snprintf(linha, sizeof linha, "pedido %c", e.peca); break; + case EV_ENTREGA: snprintf(linha, sizeof linha, "entrega %c", e.peca); confirmacoes++; break; + case EV_ESTADO: snprintf(linha, sizeof linha, "estado %s", nomeEstado(f.estado)); break; + case EV_ESTOQUE: snprintf(linha, sizeof linha, "estoque %d %d %d", f.estoque[0], f.estoque[1], f.estoque[2]); break; + case EV_ESTEIRAS: snprintf(linha, sizeof linha, "esteiras %d%d%d", fsmMotorLigado(f, 0), fsmMotorLigado(f, 1), fsmMotorLigado(f, 2)); break; + default: + snprintf(linha, sizeof linha, "erro %s %c %s", nomeErro(e.erro), e.peca ? e.peca : '-', + nomeFaseErro(e.fase) ? nomeFaseErro(e.fase) : "-"); + break; + } + anota(linha); + } + if (out.zerarJuncao >= 0) { + LeituraJuncao& j = in.juncao[out.zerarJuncao]; + j.maiorPulsoMs = 0; + j.bordas = 0; + } + } + + // Avança de t0 a t1 em passos de 5 ms. + void ate(uint32_t t0, uint32_t t1) { + for (uint32_t t = t0; t <= t1; t += 5) passo(t); + } + + bool contem(const char* trecho) const { return strstr(log, trecho) != nullptr; } + bool algumMotor() const { return fsmMotorLigado(f, 0) || fsmMotorLigado(f, 1) || fsmMotorLigado(f, 2); } +}; + +} // namespace + +TESTE(entrega_normal_debita_na_confirmacao_e_volta_a_aguardar) { + for (int idx = 0; idx < 3; idx++) { + Bancada b; + const char letra = (char)('A' + idx); + b.passo(0, CMD_PECA, letra); // pedido + verificação + VERIFICA(b.f.estado == ACIONANDO_ESTEIRA); + b.passo(5); // liga a esteira + VERIFICA(b.f.estado == ENTREGANDO_PECA); + VERIFICA(fsmMotorLigado(b.f, (uint8_t)idx)); + b.in.topo[idx] = false; // a peça saiu do topo + b.ate(10, 4000); + VERIFICA(b.f.estoque[idx] == 15); // ainda não confirmou + b.in.juncao[idx].ocupada = true; // peça sobre a junção + b.in.juncao[idx].ocupouEm = 4005; + b.ate(4005, 4030); + VERIFICA(b.f.estoque[idx] == 14); // debitou na confirmação + VERIFICA(fsmMotorLigado(b.f, (uint8_t)idx)); // motor segue na saída + b.in.juncao[idx].ocupada = false; // a peça caiu na principal + b.ate(4035, 7025); + VERIFICA(!b.algumMotor()); // saída de 3 s terminou + VERIFICA(b.f.estado == ENTREGANDO_PECA && b.f.emPausa); + b.ate(7030, 8600); + VERIFICA(b.f.estado == AGUARDANDO_PEDIDO); + VERIFICA(b.debitos == 1 && b.confirmacoes == 1); + char esperado[48]; + snprintf(esperado, sizeof esperado, "pedido %c\nestado ACIONANDO_ESTEIRA", letra); + VERIFICA(b.contem(esperado)); + VERIFICA(b.contem("estado AGUARDANDO_PEDIDO")); + } +} + +TESTE(checagem_previa_vai_para_erro_sem_ligar_motor) { + { // estoque zerado + Bancada b; + b.f.estoque[0] = 0; + b.passo(0, CMD_PECA, 'A'); + VERIFICA(b.f.estado == ERRO && !b.algumMotor()); + VERIFICA(b.contem("erro sem_estoque A verificacao\nestado ERRO\nesteiras 000")); + } + { // contador > 0 mas sem peça no topo + Bancada b; + b.in.topo[1] = false; + b.passo(0, CMD_PECA, 'B'); + VERIFICA(b.f.estado == ERRO && !b.algumMotor()); + VERIFICA(b.contem("erro sem_peca_topo B verificacao")); + } + { // junção já ocupada antes de ligar (sensor obstruído ou peça esquecida) + Bancada b; + b.in.juncao[2].ocupada = true; + b.passo(0, CMD_PECA, 'C'); + VERIFICA(b.f.estado == ERRO && !b.algumMotor()); + VERIFICA(b.contem("erro juncao_obstruida C verificacao")); + } +} + +TESTE(caso_p2_esteira_c_travada_para_em_3s_sem_debito) { + Bancada b; + b.passo(0, CMD_PECA, 'C'); + b.passo(5); + b.ate(10, 3000); // topo continua ocupado: motor não avança + VERIFICA(b.f.estado == ENTREGANDO_PECA); + b.ate(3005, 3010); + VERIFICA(b.f.estado == ERRO); + VERIFICA(!b.algumMotor()); + VERIFICA(b.contem("erro motor_sem_avanco C partida")); + VERIFICA(b.debitos == 0); +} + +TESTE(caso_p1_juncao_b_nao_registra_e_termina_em_timeout_sem_debito) { + Bancada b; + b.passo(0, CMD_PECA, 'B'); + b.passo(5); + b.in.topo[1] = false; // saiu do topo... + b.ate(10, 12500); // ...mas J2 nunca mudou + VERIFICA(b.f.estado == ENTREGANDO_PECA); + b.ate(12505, 12510); + VERIFICA(b.f.estado == ERRO && !b.algumMotor()); + VERIFICA(b.contem("erro timeout B transito")); + VERIFICA(b.debitos == 0); +} + +TESTE(ruido_na_juncao_nao_debita) { + Bancada b; + b.passo(0, CMD_PECA, 'A'); + b.passo(5); + b.in.topo[0] = false; + b.ate(10, 2000); + b.in.juncao[0].maiorPulsoMs = 8; // pico de 8 ms capturado pela interrupção + b.in.juncao[0].bordas = 2; + b.ate(2005, 3000); + VERIFICA(b.debitos == 0); + VERIFICA(b.f.estado == ENTREGANDO_PECA); +} + +TESTE(peca_presa_na_saida_ja_debitou_e_vai_para_erro) { + Bancada b; + b.passo(0, CMD_PECA, 'A'); + b.passo(5); + b.in.topo[0] = false; + b.in.juncao[0].ocupada = true; + b.in.juncao[0].ocupouEm = 1000; + b.ate(1000, 1030); + VERIFICA(b.debitos == 1); + b.ate(1035, 4100); // junção continua ocupada + VERIFICA(b.f.estado == ERRO && !b.algumMotor()); + VERIFICA(b.contem("erro peca_presa_saida A saida")); + VERIFICA(b.debitos == 1); +} + +TESTE(comandos_rejeitados_nao_mudam_estado) { + Bancada b; + b.passo(0, CMD_PECA, 'A'); + b.passo(5); + b.passo(10, CMD_PECA, 'B'); // ocupado, com a peça PEDIDA + VERIFICA(b.contem("erro ocupado B -")); + b.passo(15, CMD_PECA_INVALIDA, 0); + VERIFICA(b.contem("erro peca_invalida - -")); + b.passo(20, CMD_DESCONHECIDO, 0); + VERIFICA(b.contem("erro comando_desconhecido - -")); + b.passo(25, CMD_RESET, 0); // RESET fora de ERRO: ignorado + VERIFICA(b.f.estado == ENTREGANDO_PECA); + VERIFICA(fsmMotorLigado(b.f, 0)); +} + +TESTE(reset_em_erro_volta_a_aguardar_e_republica) { + Bancada b; + b.f.estoque[0] = 0; + b.passo(0, CMD_PECA, 'A'); + VERIFICA(b.f.estado == ERRO); + b.log[0] = 0; + b.passo(5, CMD_RESET, 0); + VERIFICA(b.f.estado == AGUARDANDO_PEDIDO); + VERIFICA(b.contem("estado AGUARDANDO_PEDIDO\nestoque 0 15 15")); + b.passo(10, CMD_PECA, 'B'); // aceita novo pedido + VERIFICA(b.contem("pedido B")); +} + +TESTE(saida_segura_em_erro_mantem_motores_desligados) { + Bancada b; + b.passo(0, CMD_PECA, 'C'); + b.passo(5); + b.ate(10, 3010); // motor_sem_avanco + VERIFICA(b.f.estado == ERRO); + for (uint32_t t = 3015; t < 20000; t += 50) { + b.in.topo[2] = (t / 50) % 2; // sensores mudando não religam nada + b.in.juncao[2].ocupada = (t / 100) % 2; + b.passo(t); + VERIFICA(!b.algumMotor()); + } +} + +TESTE(propriedade_nenhum_debito_sem_confirmacao) { + // Varre combinações de comportamento do topo e da junção e confere que + // o número de débitos é sempre igual ao de entregas publicadas. + for (int topoSai = 0; topoSai < 2; topoSai++) { + for (int pulso = 0; pulso < 40; pulso += 7) { + Bancada b; + b.passo(0, CMD_PECA, 'B'); + b.passo(5); + if (topoSai) b.in.topo[1] = false; + b.ate(10, 2000); + b.in.juncao[1].maiorPulsoMs = (uint16_t)pulso; + b.ate(2005, 16000); + VERIFICA(b.debitos == b.confirmacoes); + VERIFICA(b.debitos <= 1); + } + } +} +``` + +- [ ] **Passo 2: Rodar e ver falhar** + +Rode (na raiz do repositório, Git Bash): + +```bash +MSYS_NO_PATHCONV=1 docker run --rm -v "$(cygpath -w "$PWD"):/w" -w /w gcc:14 make -C test/firmware_uno test +``` + +Esperado: erro de compilação (`fsm.h` não existe). + +- [ ] **Passo 3: Implementar** + +Crie `arduino/data_flow_inventory/src/logica/fsm.h` com exatamente este conteúdo: + +```cpp +// ============================================================ +// DATA FLOW INVENTORY — Máquina de estados do Uno (lógica pura) +// ------------------------------------------------------------ +// 5 estados (fluxograma oficial) + supervisão da entrega por marcos. +// A FSM não toca em hardware: recebe as entradas de um ciclo e devolve +// os eventos a publicar. Motores e LCD são derivados do estado +// (fsmMotorLigado / tela): em ERRO, nenhum motor fica ligado. +// Invariante: o estoque só diminui em SUP_CONFIRMOU (passagem válida +// na junção da esteira pedida). +// C++ puro (testado no PC). +// ============================================================ +#pragma once +#include + +#include "protocolo.h" +#include "supervisor_entrega.h" + +enum Estado : uint8_t { + AGUARDANDO_PEDIDO = 0, + VERIFICANDO_ESTOQUE, + ACIONANDO_ESTEIRA, + ENTREGANDO_PECA, + ERRO +}; + +enum TipoErro : uint8_t { + ERR_NENHUM = 0, + ERR_SEM_ESTOQUE, // contador zerado → ERRO + ERR_SEM_PECA_TOPO, // contador > 0, topo vazio → ERRO + ERR_JUNCAO_OBSTRUIDA, // junção ocupada antes de ligar → ERRO + ERR_MOTOR_SEM_AVANCO, // M1 → ERRO + ERR_TIMEOUT, // M2 → ERRO + ERR_PECA_PRESA_SAIDA, // M3 → ERRO + ERR_OCUPADO, // rejeição (não muda o estado) + ERR_PECA_INVALIDA, // rejeição + ERR_COMANDO_DESCONHECIDO // rejeição +}; + +enum FaseErro : uint8_t { FERR_NENHUMA = 0, FERR_VERIFICACAO, FERR_PARTIDA, FERR_TRANSITO, FERR_SAIDA }; + +// Nomes publicados no JSON (TXT: flash no AVR). +const char* nomeEstado(Estado e); +const char* nomeErro(TipoErro e); +const char* nomeFaseErro(FaseErro f); // nullptr para FERR_NENHUMA + +enum TelaLcd : uint8_t { TELA_ESTOQUE = 0, TELA_ENTREGANDO, TELA_SAINDO, TELA_ENTREGUE, TELA_ERRO }; + +enum TipoEvento : uint8_t { EV_PEDIDO = 0, EV_ERRO, EV_ENTREGA, EV_ESTADO, EV_ESTOQUE, EV_ESTEIRAS }; + +struct Evento { + TipoEvento tipo; + char peca; // 'A'..'C' ou 0 + TipoErro erro; // EV_ERRO + FaseErro fase; // EV_ERRO + long tMs; // EV_ERRO: tempo desde a partida (ou -1) + long bordas; // EV_ERRO: bordas na junção (ou -1) +}; + +struct EntradasFsm { + uint32_t agora; + bool topo[3]; // sensores de topo FILTRADOS (true = peça presente) + LeituraJuncao juncao[3]; // junções (capturadas por interrupção) + ComandoTipo cmd; // comando recebido neste ciclo (CMD_NENHUM se nenhum) + char cmdPeca; // peça do CMD_PECA +}; + +struct SaidasFsm { + static const uint8_t MAX_EVENTOS = 4; + Evento eventos[MAX_EVENTOS]; + uint8_t nEventos; + int8_t zerarJuncao; // -1 = nada; 0..2 = zerar a captura dessa junção +}; + +struct Fsm { + Estado estado; + uint8_t peca; // 0 = nenhuma; 1..3 = A..C + int estoque[3]; + ParamEsteira params[3]; + Supervisor sup; + bool emPausa; // pausa pós-entrega (motor parado) + uint32_t pausaDesde; + TelaLcd tela; + TipoErro erroAtual; // último erro que levou a ERRO (para o LCD) +}; + +static const uint16_t PAUSA_POS_ENTREGA_MS = 1500; + +void fsmIniciar(Fsm& f, const ParamEsteira params[3], int estoqueInicial); +void fsmPasso(Fsm& f, const EntradasFsm& in, SaidasFsm& out); +// Esteira idx (0..2) deve estar ligada agora? +bool fsmMotorLigado(const Fsm& f, uint8_t idx); +char letraPeca(uint8_t peca); // 1..3 → 'A'..'C'; outro → 0 +``` + +Crie `arduino/data_flow_inventory/src/logica/fsm.cpp` com exatamente este conteúdo: + +```cpp +#include "fsm.h" + +#include "texto_flash.h" + +const char* nomeEstado(Estado e) { + switch (e) { + case AGUARDANDO_PEDIDO: return TXT("AGUARDANDO_PEDIDO"); + case VERIFICANDO_ESTOQUE: return TXT("VERIFICANDO_ESTOQUE"); + case ACIONANDO_ESTEIRA: return TXT("ACIONANDO_ESTEIRA"); + case ENTREGANDO_PECA: return TXT("ENTREGANDO_PECA"); + default: return TXT("ERRO"); + } +} + +const char* nomeErro(TipoErro e) { + switch (e) { + case ERR_SEM_ESTOQUE: return TXT("sem_estoque"); + case ERR_SEM_PECA_TOPO: return TXT("sem_peca_topo"); + case ERR_JUNCAO_OBSTRUIDA: return TXT("juncao_obstruida"); + case ERR_MOTOR_SEM_AVANCO: return TXT("motor_sem_avanco"); + case ERR_TIMEOUT: return TXT("timeout"); + case ERR_PECA_PRESA_SAIDA: return TXT("peca_presa_saida"); + case ERR_OCUPADO: return TXT("ocupado"); + case ERR_PECA_INVALIDA: return TXT("peca_invalida"); + case ERR_COMANDO_DESCONHECIDO: return TXT("comando_desconhecido"); + default: return TXT("desconhecido"); + } +} + +const char* nomeFaseErro(FaseErro f) { + switch (f) { + case FERR_VERIFICACAO: return TXT("verificacao"); + case FERR_PARTIDA: return TXT("partida"); + case FERR_TRANSITO: return TXT("transito"); + case FERR_SAIDA: return TXT("saida"); + default: return nullptr; + } +} + +char letraPeca(uint8_t peca) { + return (peca >= 1 && peca <= 3) ? (char)('A' + peca - 1) : 0; +} + +static void emitir(SaidasFsm& out, TipoEvento tipo, char peca = 0, + TipoErro erro = ERR_NENHUM, FaseErro fase = FERR_NENHUMA, + long tMs = -1, long bordas = -1) { + if (out.nEventos >= SaidasFsm::MAX_EVENTOS) return; + Evento& e = out.eventos[out.nEventos++]; + e.tipo = tipo; + e.peca = peca; + e.erro = erro; + e.fase = fase; + e.tMs = tMs; + e.bordas = bordas; +} + +// Vai para ERRO: motores param (fsmMotorLigado passa a ser falso), o erro é +// publicado com o diagnóstico e o dashboard recebe o novo estado e as esteiras. +static void falhar(Fsm& f, SaidasFsm& out, TipoErro erro, FaseErro fase, + long tMs, long bordas) { + f.estado = ERRO; + f.emPausa = false; + f.tela = TELA_ERRO; + f.erroAtual = erro; + emitir(out, EV_ERRO, letraPeca(f.peca), erro, fase, tMs, bordas); + emitir(out, EV_ESTADO); + emitir(out, EV_ESTEIRAS); +} + +void fsmIniciar(Fsm& f, const ParamEsteira params[3], int estoqueInicial) { + f.estado = AGUARDANDO_PEDIDO; + f.peca = 0; + for (uint8_t i = 0; i < 3; i++) { + f.estoque[i] = estoqueInicial; + f.params[i] = params[i]; + } + supervisorIniciar(f.sup, params[0], 0); + f.sup.fase = FASE_FIM; + f.emPausa = false; + f.pausaDesde = 0; + f.tela = TELA_ESTOQUE; + f.erroAtual = ERR_NENHUM; +} + +bool fsmMotorLigado(const Fsm& f, uint8_t idx) { + if (f.peca != idx + 1) return false; + if (f.estado != ENTREGANDO_PECA) return false; + if (f.emPausa) return false; + return f.sup.fase != FASE_FIM; +} + +static void tratarComando(Fsm& f, const EntradasFsm& in, SaidasFsm& out) { + switch (in.cmd) { + case CMD_PECA: + if (f.estado != AGUARDANDO_PEDIDO) { + emitir(out, EV_ERRO, in.cmdPeca, ERR_OCUPADO); + return; + } + f.peca = (uint8_t)(in.cmdPeca - 'A' + 1); + emitir(out, EV_PEDIDO, in.cmdPeca); + f.estado = VERIFICANDO_ESTOQUE; + return; + case CMD_PECA_INVALIDA: + emitir(out, EV_ERRO, 0, ERR_PECA_INVALIDA); + return; + case CMD_DESCONHECIDO: + emitir(out, EV_ERRO, 0, ERR_COMANDO_DESCONHECIDO); + return; + case CMD_RESET: + if (f.estado == ERRO) { + f.estado = AGUARDANDO_PEDIDO; + f.peca = 0; + f.emPausa = false; + f.sup.fase = FASE_FIM; + f.tela = TELA_ESTOQUE; + f.erroAtual = ERR_NENHUM; + emitir(out, EV_ESTADO); + emitir(out, EV_ESTOQUE); + } + // RESET fora de ERRO: ignorado de propósito (sistema já estável). + return; + default: + return; + } +} + +void fsmPasso(Fsm& f, const EntradasFsm& in, SaidasFsm& out) { + out.nEventos = 0; + out.zerarJuncao = -1; + + tratarComando(f, in, out); + const uint8_t idx = (f.peca >= 1 && f.peca <= 3) ? (uint8_t)(f.peca - 1) : 0; + + switch (f.estado) { + case VERIFICANDO_ESTOQUE: + // Checagem prévia: qualquer falha vai para ERRO SEM ligar o motor. + if (f.estoque[idx] <= 0) { + falhar(f, out, ERR_SEM_ESTOQUE, FERR_VERIFICACAO, -1, -1); + } else if (!in.topo[idx]) { + falhar(f, out, ERR_SEM_PECA_TOPO, FERR_VERIFICACAO, -1, -1); + } else if (in.juncao[idx].ocupada) { + falhar(f, out, ERR_JUNCAO_OBSTRUIDA, FERR_VERIFICACAO, -1, -1); + } else { + f.estado = ACIONANDO_ESTEIRA; + f.tela = TELA_ENTREGANDO; + emitir(out, EV_ESTADO); + } + break; + + case ACIONANDO_ESTEIRA: + out.zerarJuncao = (int8_t)idx; + supervisorIniciar(f.sup, f.params[idx], in.agora); + f.estado = ENTREGANDO_PECA; + emitir(out, EV_ESTEIRAS); + break; + + case ENTREGANDO_PECA: { + if (f.emPausa) { + if ((uint32_t)(in.agora - f.pausaDesde) >= PAUSA_POS_ENTREGA_MS) { + f.emPausa = false; + f.peca = 0; + f.estado = AGUARDANDO_PEDIDO; + f.tela = TELA_ESTOQUE; + emitir(out, EV_ESTADO); + emitir(out, EV_ESTEIRAS); + } + break; + } + const LeituraJuncao& j = in.juncao[idx]; + const long tMs = (long)(uint32_t)(in.agora - f.sup.inicioMs); + switch (supervisorAtualizar(f.sup, in.agora, in.topo[idx], j)) { + case SUP_CONFIRMOU: + f.estoque[idx]--; + f.tela = TELA_SAINDO; + emitir(out, EV_ENTREGA, letraPeca(f.peca)); + emitir(out, EV_ESTOQUE); + break; + case SUP_SAIDA_OK: + f.emPausa = true; + f.pausaDesde = in.agora; + f.tela = TELA_ENTREGUE; + emitir(out, EV_ESTEIRAS); + break; + case SUP_FALHA_SEM_AVANCO: + falhar(f, out, ERR_MOTOR_SEM_AVANCO, FERR_PARTIDA, tMs, j.bordas); + break; + case SUP_FALHA_TIMEOUT: + falhar(f, out, ERR_TIMEOUT, FERR_TRANSITO, tMs, j.bordas); + break; + case SUP_FALHA_PRESA_SAIDA: + falhar(f, out, ERR_PECA_PRESA_SAIDA, FERR_SAIDA, tMs, j.bordas); + break; + default: + break; + } + break; + } + + default: // AGUARDANDO_PEDIDO e ERRO: nada a fazer (motores desligados) + break; + } +} +``` + +- [ ] **Passo 4: Rodar e ver passar** + +Rode (na raiz do repositório, Git Bash): + +```bash +MSYS_NO_PATHCONV=1 docker run --rm -v "$(cygpath -w "$PWD"):/w" -w /w gcc:14 make -C test/firmware_uno test +``` + +Esperado: `37 testes, ... 0 falhas` (inclui os casos P1 e P2 e a propriedade "nenhum débito sem confirmação"). + +- [ ] **Passo 5: Commit** + +```bash +git add test/firmware_uno arduino/data_flow_inventory/src/logica +git commit -m "feat(firmware): maquina de estados com checagem previa e debito so na confirmacao + +Co-Authored-By: Claude Opus 5.5 " +``` + +--- + +### Tarefa 5: Textos do LCD + +**Arquivos:** +- Criar: `arduino/data_flow_inventory/src/logica/telas.h/.cpp` +- Teste: `test/firmware_uno/teste_telas.cpp` + +**Interfaces:** +- Consome: `fsm.h` (Tarefa 4), `TXT()`. +- Produz: `LCD_COLUNAS` (16) e `void textoTela(const Fsm& f, char* l1, char* l2)` (linhas com no máximo 16 caracteres, em RAM). + +- [ ] **Passo 1: Escrever os testes (falham)** + +Crie `test/firmware_uno/teste_telas.cpp` com exatamente este conteúdo: + +```cpp +// Textos do LCD (16 colunas). +#include "fsm.h" +#include "telas.h" +#include "teste.h" + +static ParamEsteira paramPadrao() { + ParamEsteira p; + p.pwmRegime = 200; + p.kickMs = 150; + p.prazoPartidaMs = 3000; + p.timeoutMs = 12500; + p.saidaMs = 3000; + p.pulsoMinJuncaoMs = 20; + return p; +} + +// ---- Telas --------------------------------------------------------------------- + +static Fsm fsmNova() { + Fsm f; + ParamEsteira p[3] = {paramPadrao(), paramPadrao(), paramPadrao()}; + fsmIniciar(f, p, 15); + return f; +} + +TESTE(telas_cabem_em_16_colunas) { + char l1[17], l2[17]; + Fsm f = fsmNova(); + textoTela(f, l1, l2); + VERIFICA_TEXTO(l1, "Estoque:"); + VERIFICA_TEXTO(l2, "A:15 B:15 C:15"); + + f.peca = 2; + f.tela = TELA_ENTREGANDO; + textoTela(f, l1, l2); + VERIFICA_TEXTO(l1, "Entregando B"); + + f.tela = TELA_ENTREGUE; + f.estoque[1] = 14; + textoTela(f, l1, l2); + VERIFICA_TEXTO(l2, "Entregue! B:14"); + + const TipoErro erros[] = {ERR_MOTOR_SEM_AVANCO, ERR_TIMEOUT, ERR_JUNCAO_OBSTRUIDA, ERR_SEM_PECA_TOPO, + ERR_SEM_ESTOQUE, ERR_PECA_PRESA_SAIDA, ERR_COMANDO_DESCONHECIDO}; + const char* esperado1[] = {"ERRO: Motor B", "ERRO: Timeout B", "ERRO: Juncao J2", "ERRO: Topo B", + "ERRO: Estoque B", "ERRO: Saida B", "ERRO"}; + const char* esperado2[] = {"Sem avanco", "Retire a peca", "Obstruida", "Sem peca", + "Zerado", "Peca presa", "comando_desconhe"}; + f.tela = TELA_ERRO; + for (int i = 0; i < 7; i++) { + f.erroAtual = erros[i]; + textoTela(f, l1, l2); + VERIFICA_TEXTO(l1, esperado1[i]); + VERIFICA_TEXTO(l2, esperado2[i]); + VERIFICA(strlen(l1) <= 16 && strlen(l2) <= 16); + } +} +``` + +- [ ] **Passo 2: Rodar e ver falhar** + +Rode (na raiz do repositório, Git Bash): + +```bash +MSYS_NO_PATHCONV=1 docker run --rm -v "$(cygpath -w "$PWD"):/w" -w /w gcc:14 make -C test/firmware_uno test +``` + +Esperado: erro de compilação (`telas.h` não existe). + +- [ ] **Passo 3: Implementar** + +Crie `arduino/data_flow_inventory/src/logica/telas.h` com exatamente este conteúdo: + +```cpp +// ============================================================ +// DATA FLOW INVENTORY — Textos do LCD 16x2 (lógica pura) +// ------------------------------------------------------------ +// Cada tela vira duas linhas de no máximo 16 caracteres. A camada de +// hardware só redesenha quando o texto muda. C++ puro (testado no PC). +// ============================================================ +#pragma once +#include "fsm.h" + +static const uint8_t LCD_COLUNAS = 16; + +// l1 e l2 precisam ter espaço para LCD_COLUNAS + 1 caracteres. +void textoTela(const Fsm& f, char* l1, char* l2); +``` + +Crie `arduino/data_flow_inventory/src/logica/telas.cpp` com exatamente este conteúdo: + +```cpp +#include "telas.h" + +#include + +#include "texto_flash.h" + +namespace { +// Acrescenta texto fixo (TXT) respeitando o limite de 16 colunas. +void anexaTxt(char* dst, const char* txt) { + uint8_t n = (uint8_t)strlen(dst); + for (char c = lerTxt(txt); c != 0 && n < LCD_COLUNAS; c = lerTxt(++txt)) dst[n++] = c; + dst[n] = 0; +} + +// Acrescenta texto em RAM respeitando o limite de 16 colunas. +void anexaRam(char* dst, const char* s) { + uint8_t n = (uint8_t)strlen(dst); + while (*s && n < LCD_COLUNAS) dst[n++] = *s++; + dst[n] = 0; +} + +void anexaNum(char* dst, int v) { + char tmp[7]; + uint8_t i = 0; + bool neg = v < 0; + unsigned u = neg ? (unsigned)(-v) : (unsigned)v; + do { tmp[i++] = (char)('0' + (u % 10)); u /= 10; } while (u > 0 && i < 6); + char s[8]; + uint8_t k = 0; + if (neg) s[k++] = '-'; + while (i > 0) s[k++] = tmp[--i]; + s[k] = 0; + anexaRam(dst, s); +} + +void anexaLetra(char* dst, char c) { + char s[2] = {c, 0}; + anexaRam(dst, s); +} + +void telaErro(const Fsm& f, char* l1, char* l2) { + const char p = letraPeca(f.peca); + switch (f.erroAtual) { + case ERR_MOTOR_SEM_AVANCO: + anexaTxt(l1, TXT("ERRO: Motor ")); anexaLetra(l1, p); anexaTxt(l2, TXT("Sem avanco")); + break; + case ERR_TIMEOUT: + anexaTxt(l1, TXT("ERRO: Timeout ")); anexaLetra(l1, p); anexaTxt(l2, TXT("Retire a peca")); + break; + case ERR_JUNCAO_OBSTRUIDA: + anexaTxt(l1, TXT("ERRO: Juncao J")); anexaNum(l1, f.peca); anexaTxt(l2, TXT("Obstruida")); + break; + case ERR_SEM_PECA_TOPO: + anexaTxt(l1, TXT("ERRO: Topo ")); anexaLetra(l1, p); anexaTxt(l2, TXT("Sem peca")); + break; + case ERR_SEM_ESTOQUE: + anexaTxt(l1, TXT("ERRO: Estoque ")); anexaLetra(l1, p); anexaTxt(l2, TXT("Zerado")); + break; + case ERR_PECA_PRESA_SAIDA: + anexaTxt(l1, TXT("ERRO: Saida ")); anexaLetra(l1, p); anexaTxt(l2, TXT("Peca presa")); + break; + default: + anexaTxt(l1, TXT("ERRO")); + anexaTxt(l2, nomeErro(f.erroAtual)); + break; + } +} +} // namespace + +void textoTela(const Fsm& f, char* l1, char* l2) { + l1[0] = 0; + l2[0] = 0; + const char p = letraPeca(f.peca); + switch (f.tela) { + case TELA_ENTREGANDO: + anexaTxt(l1, TXT("Entregando ")); anexaLetra(l1, p); + anexaTxt(l2, TXT("Aguarda juncao")); + break; + case TELA_SAINDO: + anexaTxt(l1, TXT("Saindo da")); + anexaTxt(l2, TXT("esteira...")); + break; + case TELA_ENTREGUE: + anexaTxt(l1, TXT("Entrega OK!")); + anexaTxt(l2, TXT("Entregue! ")); anexaLetra(l2, p); anexaTxt(l2, TXT(":")); + anexaNum(l2, (f.peca >= 1 && f.peca <= 3) ? f.estoque[f.peca - 1] : 0); + break; + case TELA_ERRO: + telaErro(f, l1, l2); + break; + default: // TELA_ESTOQUE + anexaTxt(l1, TXT("Estoque:")); + anexaTxt(l2, TXT("A:")); anexaNum(l2, f.estoque[0]); + anexaTxt(l2, TXT(" B:")); anexaNum(l2, f.estoque[1]); + anexaTxt(l2, TXT(" C:")); anexaNum(l2, f.estoque[2]); + break; + } +} +``` + +- [ ] **Passo 4: Rodar e ver passar** + +Rode (na raiz do repositório, Git Bash): + +```bash +MSYS_NO_PATHCONV=1 docker run --rm -v "$(cygpath -w "$PWD"):/w" -w /w gcc:14 make -C test/firmware_uno test +``` + +Esperado: `38 testes, 582 verificacoes, 0 falhas`. + +- [ ] **Passo 5: Commit** + +```bash +git add test/firmware_uno arduino/data_flow_inventory/src/logica +git commit -m "feat(firmware): textos do LCD 16x2 por situacao + +Co-Authored-By: Claude Opus 5.5 " +``` + +--- + +### Tarefa 6: Camada de hardware, troca do `.ino` e portão de RAM no CI + +**Arquivos:** +- Criar: `arduino/data_flow_inventory/config.h` +- Criar: `arduino/data_flow_inventory/src/hw/motores.h/.cpp`, `sensores.h/.cpp`, `comunicacao.h/.cpp`, `lcd.h/.cpp`, `diagnostico.h/.cpp` +- Modificar (substituir por inteiro): `arduino/data_flow_inventory/data_flow_inventory.ino` +- Modificar: `.github/workflows/lint-and-security.yaml` (passo "Compilar Arduino Uno (principal)") + +**Interfaces:** +- Consome: todos os módulos de `src/logica/` (Tarefas 1–5). +- Produz: firmware v3.0 completo. + +**Contexto:** esta é a tarefa que troca o firmware gravado. A ordem de mensagens no boot continua a do v2.x (estoque e depois `inicio`). Rollback: o firmware v2.x é o commit `fddde08` (`git checkout fddde08 -- arduino/data_flow_inventory`). O código comentado de botões e separador do v2.x não volta (fica no histórico). + +- [ ] **Passo 1: Configuração** + +Crie `arduino/data_flow_inventory/config.h` com exatamente este conteúdo: + +```cpp +// ============================================================ +// DATA FLOW INVENTORY — Configuração do firmware do Uno +// ------------------------------------------------------------ +// Pinos (iguais ao firmware v2.x e ao diagrama elétrico) e parâmetros +// por esteira. Para calibrar uma esteira, mude só a linha dela. +// ============================================================ +#pragma once +#include "src/logica/supervisor_entrega.h" + +#define VERSAO_FIRMWARE "3.0" + +// ---- Serial (deve ser IGUAL ao Serial2 do ESP32) -------------------------- +#define BAUD_SERIAL 9600 + +// ---- Motores DC (IRF520: 1 pino PWM por motor) ---------------------------- +#define MOTOR_A 9 // Timer1 +#define MOTOR_B 10 // Timer1 +#define MOTOR_C 11 // Timer2 + +// ---- Sensores TCRT5000 (LOW = peça detectada; INPUT_PULLUP) --------------- +#define SENSOR_TOPO_A A0 +#define SENSOR_TOPO_B A1 +#define SENSOR_TOPO_C A2 +#define SENSOR_JUNCAO_J1 A3 // PCINT11 (porta C) +#define SENSOR_JUNCAO_J2 2 // PCINT18 (porta D) +#define SENSOR_JUNCAO_J3 4 // PCINT20 (porta D) + +// ---- LCD I2C 16x2 (endereços procurados no boot) --------------------------- +#define LCD_ENDERECO_1 0x27 +#define LCD_ENDERECO_2 0x3F + +// ---- Sistema ----------------------------------------------------------------- +#define ESTOQUE_INICIAL 15 // peças de cada tipo ao ligar (não persiste) +#define INTERVALO_AMOSTRA_MS 5 // amostragem dos sensores +#define FILTRO_TOPO_MS 20 // estabilidade exigida no sensor de topo +#define INTERVALO_PUBLICACAO 1000 // ciclo das mensagens periódicas +#define PASSOS_PUBLICACAO 4 // status, estoque, sensores, esteiras +#define TELA_ABERTURA_MS 2000 // "Data Flow / Inventory v3.0" no boot + +// ---- Parâmetros por esteira (A, B, C) ---------------------------------------- +// pwmRegime : PWM depois do kick (bancada: mínimo ~150 para mover com peça) +// kickMs : partida em PWM 255 para vencer o atrito estático +// prazoPartidaMs : M1 — o topo precisa ficar livre neste prazo +// timeoutMs : M2 — a junção precisa confirmar a passagem neste prazo +// saidaMs : M3 — motor ligado depois da confirmação +// pulsoMinJuncaoMs : pulso mínimo na junção para valer como passagem +// pwm kick partida timeout saida pulso +#define PARAM_ESTEIRA_A { 200, 150, 3000, 12500, 3000, 20 } +#define PARAM_ESTEIRA_B { 200, 150, 3000, 12500, 3000, 20 } +#define PARAM_ESTEIRA_C { 200, 150, 3000, 12500, 3000, 20 } +``` + +- [ ] **Passo 2: Motores, sensores, serial, LCD e diagnóstico** + +Crie `arduino/data_flow_inventory/src/hw/motores.h` com exatamente este conteúdo: + +```cpp +// ============================================================ +// DATA FLOW INVENTORY — Motores das esteiras secundárias (hardware) +// ------------------------------------------------------------ +// IRF520: 1 pino PWM por motor. Partida com kick-start (PWM 255 por +// kickMs) e depois o PWM de regime da esteira. O estado desejado vem +// da FSM a cada ciclo; o que não está desejado fica em 0. +// ============================================================ +#pragma once +#include + +#include "../logica/supervisor_entrega.h" + +// Configura os 3 pinos como saída em 0. Chamar ANTES de qualquer outra +// inicialização (o gate do IRF520 não pode ficar flutuando). +void motoresIniciar(); + +// desejado[i] = esteira i deve estar ligada agora. +void motoresAplicar(const bool desejado[3], const ParamEsteira params[3], uint32_t agora); +``` + +Crie `arduino/data_flow_inventory/src/hw/motores.cpp` com exatamente este conteúdo: + +```cpp +#include "motores.h" + +#include + +#include "../../config.h" + +namespace { +const uint8_t PINOS[3] = {MOTOR_A, MOTOR_B, MOTOR_C}; + +struct EstadoMotor { + bool ligado; + bool emRegime; + uint32_t ligouEm; +}; +EstadoMotor motor[3]; +} // namespace + +void motoresIniciar() { + for (uint8_t i = 0; i < 3; i++) { + pinMode(PINOS[i], OUTPUT); + analogWrite(PINOS[i], 0); + motor[i].ligado = false; + motor[i].emRegime = false; + motor[i].ligouEm = 0; + } +} + +void motoresAplicar(const bool desejado[3], const ParamEsteira params[3], uint32_t agora) { + for (uint8_t i = 0; i < 3; i++) { + EstadoMotor& m = motor[i]; + if (!desejado[i]) { + if (m.ligado) { + analogWrite(PINOS[i], 0); + m.ligado = false; + m.emRegime = false; + } + continue; + } + if (!m.ligado) { + m.ligado = true; + m.ligouEm = agora; + if (params[i].kickMs > 0) { + analogWrite(PINOS[i], 255); // kick-start + m.emRegime = false; + } else { + analogWrite(PINOS[i], params[i].pwmRegime); + m.emRegime = true; + } + } else if (!m.emRegime && (uint32_t)(agora - m.ligouEm) >= params[i].kickMs) { + analogWrite(PINOS[i], params[i].pwmRegime); + m.emRegime = true; + } + } +} +``` + +Crie `arduino/data_flow_inventory/src/hw/sensores.h` com exatamente este conteúdo: + +```cpp +// ============================================================ +// DATA FLOW INVENTORY — Sensores TCRT5000 (hardware) +// ------------------------------------------------------------ +// Topo (A0–A2): amostrados a cada INTERVALO_AMOSTRA_MS e filtrados. +// Junção (J1=A3, J2=D2, J3=D4): mudanças capturadas por interrupção de +// mudança de pino (PCINT), para que um pulso curto não se perca quando +// o loop está ocupado (serial, LCD). LOW no pino = peça detectada. +// ============================================================ +#pragma once +#include + +#include "../logica/supervisor_entrega.h" + +void sensoresIniciar(uint32_t agora); +void sensoresAmostrar(uint32_t agora); + +bool sensorTopo(uint8_t i); // filtrado (true = peça presente) +bool sensorJuncaoNivel(uint8_t i); // nível atual da junção +LeituraJuncao sensorJuncao(uint8_t i); // cópia atômica da captura +void sensorJuncaoZerar(uint8_t i); // zera pulsos/bordas (início da entrega) +``` + +Crie `arduino/data_flow_inventory/src/hw/sensores.cpp` com exatamente este conteúdo: + +```cpp +#include "sensores.h" + +#include + +#include "../../config.h" +#include "../logica/filtro_entrada.h" + +namespace { +const uint8_t PINOS_TOPO[3] = {SENSOR_TOPO_A, SENSOR_TOPO_B, SENSOR_TOPO_C}; +FiltroEntrada topo[3]; + +struct CapturaJuncao { + volatile bool ocupada; + volatile uint32_t ocupouEm; + volatile uint16_t maiorPulsoMs; + volatile uint8_t bordas; +}; +CapturaJuncao juncao[3]; + +// Leitura direta das portas (rápida, dentro da ISR). LOW = ocupada. +inline bool lerJ1() { return (PINC & _BV(PC3)) == 0; } // A3 +inline bool lerJ2() { return (PIND & _BV(PD2)) == 0; } // D2 +inline bool lerJ3() { return (PIND & _BV(PD4)) == 0; } // D4 + +// Chamada só com interrupções desabilitadas (dentro da ISR). +inline void registrar(uint8_t i, bool ocupada, uint32_t agora) { + CapturaJuncao& c = juncao[i]; + if (ocupada == c.ocupada) return; + c.ocupada = ocupada; + if (c.bordas < 255) c.bordas = c.bordas + 1; + if (ocupada) { + c.ocupouEm = agora; + } else { + uint32_t largura = agora - c.ocupouEm; + uint16_t l = largura > 65535UL ? 65535 : (uint16_t)largura; + if (l > c.maiorPulsoMs) c.maiorPulsoMs = l; + } +} +} // namespace + +ISR(PCINT1_vect) { // porta C: A3 (J1) + registrar(0, lerJ1(), millis()); +} + +ISR(PCINT2_vect) { // porta D: D2 (J2) e D4 (J3) + const uint32_t agora = millis(); + registrar(1, lerJ2(), agora); + registrar(2, lerJ3(), agora); +} + +void sensoresIniciar(uint32_t agora) { + for (uint8_t i = 0; i < 3; i++) { + pinMode(PINOS_TOPO[i], INPUT_PULLUP); + filtroIniciar(topo[i], digitalRead(PINOS_TOPO[i]) == LOW, agora, FILTRO_TOPO_MS); + } + pinMode(SENSOR_JUNCAO_J1, INPUT_PULLUP); + pinMode(SENSOR_JUNCAO_J2, INPUT_PULLUP); + pinMode(SENSOR_JUNCAO_J3, INPUT_PULLUP); + + noInterrupts(); + const bool inicial[3] = {lerJ1(), lerJ2(), lerJ3()}; + for (uint8_t i = 0; i < 3; i++) { + juncao[i].ocupada = inicial[i]; + juncao[i].ocupouEm = agora; + juncao[i].maiorPulsoMs = 0; + juncao[i].bordas = 0; + } + PCMSK1 |= _BV(PCINT11); // A3 + PCMSK2 |= _BV(PCINT18) | _BV(PCINT20); // D2, D4 + PCIFR = _BV(PCIF1) | _BV(PCIF2); // descarta pendências antigas + PCICR |= _BV(PCIE1) | _BV(PCIE2); + interrupts(); +} + +void sensoresAmostrar(uint32_t agora) { + for (uint8_t i = 0; i < 3; i++) { + filtroAtualizar(topo[i], digitalRead(PINOS_TOPO[i]) == LOW, agora); + } +} + +bool sensorTopo(uint8_t i) { return topo[i].estavel; } + +bool sensorJuncaoNivel(uint8_t i) { return juncao[i].ocupada; } + +LeituraJuncao sensorJuncao(uint8_t i) { + LeituraJuncao l; + noInterrupts(); + l.ocupada = juncao[i].ocupada; + l.ocupouEm = juncao[i].ocupouEm; + l.maiorPulsoMs = juncao[i].maiorPulsoMs; + l.bordas = juncao[i].bordas; + interrupts(); + return l; +} + +void sensorJuncaoZerar(uint8_t i) { + noInterrupts(); + juncao[i].maiorPulsoMs = 0; + juncao[i].bordas = 0; + interrupts(); +} +``` + +Crie `arduino/data_flow_inventory/src/hw/comunicacao.h` com exatamente este conteúdo: + +```cpp +// ============================================================ +// DATA FLOW INVENTORY — Serial Uno ↔ ESP32 (hardware) +// ------------------------------------------------------------ +// Lê comandos sem bloquear e publica as mensagens JSON (formatadas por +// src/logica/protocolo, idênticas ao firmware v2.x + campos opcionais). +// Cada mensagem é uma linha terminada em "\r\n" (como Serial.println). +// ============================================================ +#pragma once +#include + +#include "../logica/fsm.h" + +void comunicacaoIniciar(); + +// Consome bytes disponíveis até completar UMA linha; devolve o comando +// (CMD_NENHUM se ainda não há linha completa). Bytes restantes ficam +// para o próximo ciclo. +ComandoTipo comunicacaoLer(char* peca); + +void publicarEvento(const Evento& e, const Fsm& f, uint32_t agora); +void publicarEstado(const Fsm& f, uint32_t agora); +void publicarEstoque(const Fsm& f); +void publicarSensores(); +void publicarEsteiras(const Fsm& f); +void publicarInicio(const char* versao, const char* reset, bool lcd, long ramLivre); +``` + +Crie `arduino/data_flow_inventory/src/hw/comunicacao.cpp` com exatamente este conteúdo: + +```cpp +#include "comunicacao.h" + +#include + +#include "../../config.h" +#include "../logica/protocolo.h" +#include "diagnostico.h" +#include "sensores.h" + +namespace { +LeitorLinha leitor; +char buf[160]; // buffer único de formatação (maior mensagem ~140 B) + +void enviar(size_t n) { + if (n == 0) return; // não coube: não envia linha quebrada + Serial.write(reinterpret_cast(buf), n); + Serial.write('\r'); + Serial.write('\n'); +} +} // namespace + +void comunicacaoIniciar() { leitorIniciar(leitor); } + +ComandoTipo comunicacaoLer(char* peca) { + *peca = 0; + while (Serial.available() > 0) { + if (leitorAlimentar(leitor, (char)Serial.read())) { + ComandoTipo c = interpretarComando(leitor.buf, peca); + if (c != CMD_NENHUM) return c; + } + } + return CMD_NENHUM; +} + +void publicarEstado(const Fsm& f, uint32_t agora) { + enviar(formatarStatus(buf, sizeof buf, nomeEstado(f.estado), f.peca, agora / 1000UL, ramLivre())); +} + +void publicarEstoque(const Fsm& f) { enviar(formatarEstoque(buf, sizeof buf, f.estoque)); } + +void publicarSensores() { + bool topo[3], jun[3]; + for (uint8_t i = 0; i < 3; i++) { + topo[i] = sensorTopo(i); + jun[i] = sensorJuncaoNivel(i); + } + enviar(formatarSensores(buf, sizeof buf, topo, jun)); +} + +void publicarEsteiras(const Fsm& f) { + bool sec[3]; + for (uint8_t i = 0; i < 3; i++) sec[i] = fsmMotorLigado(f, i); + enviar(formatarEsteiras(buf, sizeof buf, sec)); +} + +void publicarInicio(const char* versao, const char* reset, bool lcd, long ram) { + enviar(formatarInicio(buf, sizeof buf, versao, reset, lcd ? 1 : 0, ram)); +} + +void publicarEvento(const Evento& e, const Fsm& f, uint32_t agora) { + switch (e.tipo) { + case EV_PEDIDO: + enviar(formatarPedido(buf, sizeof buf, e.peca)); + break; + case EV_ENTREGA: + enviar(formatarEntrega(buf, sizeof buf, e.peca, f.estoque)); + break; + case EV_ERRO: + enviar(formatarErro(buf, sizeof buf, nomeErro(e.erro), e.peca, nomeFaseErro(e.fase), e.tMs, e.bordas)); + break; + case EV_ESTADO: + publicarEstado(f, agora); + break; + case EV_ESTOQUE: + publicarEstoque(f); + break; + case EV_ESTEIRAS: + publicarEsteiras(f); + break; + } +} +``` + +Crie `arduino/data_flow_inventory/src/hw/lcd.h` com exatamente este conteúdo: + +```cpp +// ============================================================ +// DATA FLOW INVENTORY — LCD I2C 16x2 (hardware) +// ------------------------------------------------------------ +// Procura o módulo em LCD_ENDERECO_1 e LCD_ENDERECO_2. Se nenhum +// responder, o sistema segue SEM LCD (nada trava). Redesenha só quando +// o texto muda, sobrescrevendo com espaços (sem lcd.clear(), sem piscar). +// ============================================================ +#pragma once +#include + +bool lcdIniciar(); // true se encontrou o LCD +void lcdAtualizar(const char* l1, const char* l2); // no-op sem LCD +``` + +Crie `arduino/data_flow_inventory/src/hw/lcd.cpp` com exatamente este conteúdo: + +```cpp +#include "lcd.h" + +#include +#include +#include +#include + +#include "../../config.h" + +namespace { +LiquidCrystal_I2C lcd1(LCD_ENDERECO_1, 16, 2); +LiquidCrystal_I2C lcd2(LCD_ENDERECO_2, 16, 2); +LiquidCrystal_I2C* ativo = nullptr; +char linha1[17] = ""; +char linha2[17] = ""; + +bool responde(uint8_t endereco) { + Wire.beginTransmission(endereco); + return Wire.endTransmission() == 0; +} + +void escreverLinha(uint8_t linha, const char* texto) { + ativo->setCursor(0, linha); + uint8_t i = 0; + for (; texto[i] && i < 16; i++) ativo->write(texto[i]); + for (; i < 16; i++) ativo->write(' '); +} +} // namespace + +bool lcdIniciar() { + Wire.begin(); + // Um barramento I2C preso (ruído do motor) não pode travar o Uno. + Wire.setWireTimeout(25000, true); + if (responde(LCD_ENDERECO_1)) ativo = &lcd1; + else if (responde(LCD_ENDERECO_2)) ativo = &lcd2; + if (ativo == nullptr) return false; + ativo->init(); + ativo->backlight(); + ativo->clear(); + linha1[0] = 0; + linha2[0] = 0; + return true; +} + +void lcdAtualizar(const char* l1, const char* l2) { + if (ativo == nullptr) return; + if (strncmp(l1, linha1, 16) != 0) { + escreverLinha(0, l1); + strncpy(linha1, l1, 16); + linha1[16] = 0; + } + if (strncmp(l2, linha2, 16) != 0) { + escreverLinha(1, l2); + strncpy(linha2, l2, 16); + linha2[16] = 0; + } +} +``` + +Crie `arduino/data_flow_inventory/src/hw/diagnostico.h` com exatamente este conteúdo: + +```cpp +// ============================================================ +// DATA FLOW INVENTORY — Diagnóstico: causa do reset, RAM, watchdog +// ------------------------------------------------------------ +// O watchdog (2 s, modo interrupção + reset) reinicia o Uno se o loop +// travar; a interrupção grava a marca "watchdog" em RAM .noinit antes +// do reset. O watchdog é desligado logo no boot (.init3), senão um +// reset por watchdog deixaria o Uno reiniciando em laço. +// ============================================================ +#pragma once +#include + +void diagnosticoIniciar(); // calcula a causa do reset e arma o watchdog +const char* diagnosticoCausaReset(); // "energia", "reinicio", "watchdog" ou "brownout" +long ramLivre(); // bytes entre o heap e a pilha +void watchdogAlimentar(); // chamar uma vez por ciclo do loop +``` + +Crie `arduino/data_flow_inventory/src/hw/diagnostico.cpp` com exatamente este conteúdo: + +```cpp +#include "diagnostico.h" + +#include +#include + +#include "../logica/causa_reset.h" + +namespace { +const uint32_t ASSINATURA = 0xDF1A3B5CUL; +const uint8_t MARCA_WATCHDOG = 0xA5; +const char* causa = "energia"; +} // namespace + +// Variáveis que sobrevivem a resets com energia (não são zeradas no boot). +uint8_t dfiMcusrNoBoot __attribute__((section(".noinit"))); +uint32_t dfiAssinatura __attribute__((section(".noinit"))); +volatile uint8_t dfiMarcaWatchdog __attribute__((section(".noinit"))); + +// Roda antes de main(): guarda o MCUSR e desliga o watchdog (padrão avr-libc). +extern "C" void dfiCapturarMcusr(void) __attribute__((naked, used, section(".init3"))); +extern "C" void dfiCapturarMcusr(void) { + dfiMcusrNoBoot = MCUSR; + MCUSR = 0; + wdt_disable(); +} + +ISR(WDT_vect) { + // O loop não alimentou o watchdog por 2 s: marca e deixa o próximo + // estouro reiniciar o Uno. + dfiMarcaWatchdog = MARCA_WATCHDOG; +} + +static void armarWatchdog() { + noInterrupts(); + wdt_reset(); + WDTCSR = _BV(WDCE) | _BV(WDE); + WDTCSR = _BV(WDIE) | _BV(WDE) | _BV(WDP2) | _BV(WDP1) | _BV(WDP0); // 2 s + interrupts(); +} + +void diagnosticoIniciar() { + const bool assinaturaValida = dfiAssinatura == ASSINATURA; + const bool marcaWd = dfiMarcaWatchdog == MARCA_WATCHDOG; + causa = causaReset(dfiMcusrNoBoot, assinaturaValida, marcaWd); + dfiAssinatura = ASSINATURA; + dfiMarcaWatchdog = 0; + armarWatchdog(); +} + +const char* diagnosticoCausaReset() { return causa; } + +extern int __heap_start; +extern int* __brkval; + +long ramLivre() { + int v; + return (long)((int)&v - (__brkval == 0 ? (int)&__heap_start : (int)__brkval)); +} + +void watchdogAlimentar() { + wdt_reset(); + if (dfiMarcaWatchdog == MARCA_WATCHDOG) { + // O loop voltou depois de um aviso do watchdog: limpa a marca e + // rearma o modo interrupção + reset. + dfiMarcaWatchdog = 0; + armarWatchdog(); + } +} +``` + +- [ ] **Passo 3: Substituir o `.ino`** + +Substitua **todo** o conteúdo de `arduino/data_flow_inventory/data_flow_inventory.ino` por: + +```cpp +// ============================================================ +// DATA FLOW INVENTORY — Sistema de Intralogística Automatizada +// SENAI São Caetano do Sul — Engenharia de Controle e Automação +// Autores: Henrique Moni, Matheus Nespolo, Murilo Tolardo, Vitor Marcolongo +// Ano: 2026 — Firmware do Arduino Uno v3.0 +// ============================================================ +// Máquina de estados com 5 etapas (fluxograma oficial): +// AGUARDANDO_PEDIDO → VERIFICANDO_ESTOQUE → ACIONANDO_ESTEIRA +// → ENTREGANDO_PECA → ERRO +// A entrega é supervisionada por marcos (src/logica/supervisor_entrega): +// M1 partida — o topo fica livre em 3 s (senão motor_sem_avanco) +// M2 trânsito — a junção confirma a passagem em 12,5 s (senão timeout) +// M3 saída — motor 3 s depois da confirmação; a junção precisa liberar +// O estoque só diminui na confirmação da junção da esteira pedida. +// +// Organização: +// config.h pinos e parâmetros por esteira +// src/logica/ regras puras (FSM, marcos, filtro, protocolo, telas), +// testadas no PC em test/firmware_uno +// src/hw/ motores, sensores, serial, LCD e diagnóstico +// +// Comunicação com o ESP32 (Serial 9600): mensagens JSON idênticas às do +// firmware v2.x, com campos opcionais novos. Comandos: CMD:PECA:X, CMD:RESET. +// Botões físicos e separador (motor de passo) seguem desabilitados; o código +// antigo deles está no histórico do Git (firmware v2.1). +// ============================================================ + +#include "config.h" +#include "src/hw/comunicacao.h" +#include "src/hw/diagnostico.h" +#include "src/hw/lcd.h" +#include "src/hw/motores.h" +#include "src/hw/sensores.h" +#include "src/logica/fsm.h" +#include "src/logica/telas.h" +#include "src/logica/texto_flash.h" + +static const ParamEsteira PARAMS[3] = {PARAM_ESTEIRA_A, PARAM_ESTEIRA_B, PARAM_ESTEIRA_C}; + +static Fsm fsm; +static bool lcdPresente = false; +static uint32_t inicioMs = 0; +static uint32_t ultimaAmostra = 0; +static uint32_t ultimaPublicacao = 0; +static uint8_t passoPublicacao = 0; + +void setup() { + motoresIniciar(); // 1º: gates do IRF520 em 0 antes de qualquer coisa + diagnosticoIniciar(); // causa do reset + watchdog de 2 s + Serial.begin(BAUD_SERIAL); + comunicacaoIniciar(); + + inicioMs = millis(); + sensoresIniciar(inicioMs); + lcdPresente = lcdIniciar(); + lcdAtualizar("Data Flow", "Inventory v" VERSAO_FIRMWARE); + + fsmIniciar(fsm, PARAMS, ESTOQUE_INICIAL); + + // Mesma ordem do firmware v2.x: estoque inicial e depois o evento de início. + publicarEstoque(fsm); + publicarInicio(TXT(VERSAO_FIRMWARE), diagnosticoCausaReset(), lcdPresente, ramLivre()); +} + +void loop() { + const uint32_t agora = millis(); + + if ((uint32_t)(agora - ultimaAmostra) >= INTERVALO_AMOSTRA_MS) { + ultimaAmostra = agora; + sensoresAmostrar(agora); + } + + // Entradas do ciclo → FSM. + EntradasFsm in; + in.agora = agora; + for (uint8_t i = 0; i < 3; i++) { + in.topo[i] = sensorTopo(i); + in.juncao[i] = sensorJuncao(i); + } + in.cmd = comunicacaoLer(&in.cmdPeca); + + SaidasFsm out; + fsmPasso(fsm, in, out); + if (out.zerarJuncao >= 0) sensorJuncaoZerar((uint8_t)out.zerarJuncao); + + // Motores derivados do estado: em ERRO nenhum fica ligado. + bool desejado[3]; + for (uint8_t i = 0; i < 3; i++) desejado[i] = fsmMotorLigado(fsm, i); + motoresAplicar(desejado, PARAMS, agora); + + for (uint8_t i = 0; i < out.nEventos; i++) publicarEvento(out.eventos[i], fsm, agora); + + // Publicação periódica escalonada: uma mensagem a cada 250 ms, cada uma + // 1x por segundo (evita prender o loop na serial de 9600 baud). + if ((uint32_t)(agora - ultimaPublicacao) >= INTERVALO_PUBLICACAO / PASSOS_PUBLICACAO) { + ultimaPublicacao = agora; + switch (passoPublicacao) { + case 0: publicarEstado(fsm, agora); break; + case 1: publicarEstoque(fsm); break; + case 2: publicarSensores(); break; + default: publicarEsteiras(fsm); break; + } + passoPublicacao = (uint8_t)((passoPublicacao + 1) % PASSOS_PUBLICACAO); + } + + // LCD: depois da tela de abertura, mostra a tela da FSM (redesenha só se mudou). + if (lcdPresente && (uint32_t)(agora - inicioMs) >= TELA_ABERTURA_MS) { + char l1[LCD_COLUNAS + 1], l2[LCD_COLUNAS + 1]; + textoTela(fsm, l1, l2); + lcdAtualizar(l1, l2); + } + + watchdogAlimentar(); +} +``` + +- [ ] **Passo 4: Compilar e conferir RAM e avisos** + +```bash +CLI="/c/Users/matheusn/AppData/Local/Programs/Arduino IDE/resources/app/lib/backend/resources/arduino-cli.exe" +"$CLI" compile --fqbn arduino:avr:uno --warnings all --clean arduino/data_flow_inventory 2>&1 | tee "$TEMP/uno.log" | grep -E "Sketch uses|Global variables" +grep -iE "warning|error" "$TEMP/uno.log" | grep -vE "LiquidCrystal|cores[\\/]arduino" || echo "sem avisos no código do projeto" +``` + +Esperado: compila; `Global variables use ~943 bytes (46%)` (limite: 65%); "sem avisos no código do projeto". Rode também os testes da lógica (devem continuar `38 testes, ... 0 falhas`). + +- [ ] **Passo 5: Portão de RAM no CI** + +No job `arduino-compile` de `.github/workflows/lint-and-security.yaml`, substitua o passo: + +```yaml + - name: Compilar Arduino Uno (principal) + run: arduino-cli compile --fqbn arduino:avr:uno --warnings all arduino/data_flow_inventory/data_flow_inventory.ino +``` + +por: + +```yaml + - name: Compilar Arduino Uno (principal) e conferir RAM (máx. 65%) + run: | + set -o pipefail + arduino-cli compile --fqbn arduino:avr:uno --warnings all arduino/data_flow_inventory/data_flow_inventory.ino 2>&1 | tee uno.log + ram=$(grep -oP 'Global variables use \d+ bytes \(\K\d+' uno.log) + echo "RAM global do Uno: ${ram}%" + test "$ram" -le 65 +``` + +- [ ] **Passo 6: Commit** + +```bash +git add arduino/data_flow_inventory .github/workflows/lint-and-security.yaml +git commit -m "feat(firmware): Uno v3.0 modular com marcos, watchdog e diagnostico + +Co-Authored-By: Claude Opus 5.5 " +``` + +--- + +### Tarefa 7: Documentação e roteiro de bancada + +**Arquivos:** +- Modificar: `docs/ARCHITECTURE.md` (seção 3), `README.md` (tabela da FSM), `docs/CHANGELOG.md`, `docs/CI-CD.md` +- Criar: `docs/testes/roteiros/firmware_uno_v3_validacao.md` + +- [ ] **Passo 1: `docs/ARCHITECTURE.md`** + +Na seção `## 3. Máquina de Estados (Arduino)`, substitua o trecho que começa em `**5 estados:**` e termina na linha antes de `**Diagramas:**` (inclui as listas "Erros que levam a ERRO" e "Rejeições") por: + +````markdown +**5 estados** (firmware v3.0 — código em `arduino/data_flow_inventory/src/logica/fsm.cpp`): +1. **AGUARDANDO_PEDIDO** → recebe comando +2. **VERIFICANDO_ESTOQUE** → checagem prévia, sem ligar o motor: contador > 0, peça no sensor do topo, junção livre +3. **ACIONANDO_ESTEIRA** → liga o motor da esteira escolhida (kick-start em PWM 255 por 150 ms, depois o PWM de regime da esteira) +4. **ENTREGANDO_PECA** → supervisão por marcos (abaixo); o estoque é debitado na confirmação da junção +5. **ERRO** → motores desligados a cada ciclo; só sai com `CMD:RESET` + +**Supervisão da entrega por marcos** (prazos por esteira em `arduino/data_flow_inventory/config.h`): + +| Marco | Prova | Prazo padrão | Falha → erro | Estoque | +|---|---|---|---|---| +| M1 Partida | sensor de topo fica livre | 3 s após ligar | `motor_sem_avanco` (motor parado na hora) | não debita | +| M2 Trânsito | junção registra uma passagem (pulso ≥ 20 ms, capturado por interrupção) | 12,5 s após ligar | `timeout` (retirar a peça da esteira) | não debita | +| Confirmação | — | — | — | **debita** e publica a entrega | +| M3 Saída | motor segue 3 s; no fim a junção precisa estar livre | 3 s | `peca_presa_saida` | já debitado | + +```mermaid +flowchart LR + P[Pedido] --> V{Checagem prévia} + V -- falha --> E[ERRO] + V -- ok --> M1{M1: topo livre em 3 s?} + M1 -- não --> E + M1 -- sim --> M2{M2: junção em 12,5 s?} + M2 -- não --> E + M2 -- sim --> D[Debita estoque e publica entrega] + D --> M3{M3: junção livre após 3 s?} + M3 -- não --> E + M3 -- sim --> A[AGUARDANDO_PEDIDO] + E -- CMD:RESET --> A +``` + +**Erros que levam a ERRO (exigem CMD:RESET):** +- `sem_estoque` — contador da peça zerado +- `sem_peca_topo` — contador > 0, mas sem peça no sensor do topo +- `juncao_obstruida` — sensor de junção já ocupado antes de ligar o motor +- `motor_sem_avanco` — M1: o topo não liberou no prazo (motor travado, fraco ou peça presa no início) +- `timeout` — M2: a peça saiu do topo e não chegou à junção no prazo +- `peca_presa_saida` — M3: a junção continuou ocupada no fim da saída + +**Rejeições (não mudam o estado):** +- `peca_invalida` — formato diferente de `CMD:PECA:` + A, B ou C +- `ocupado` — pedido com a FSM fora de AGUARDANDO_PEDIDO (o campo `peca` é a peça pedida) +- `comando_desconhecido` — linha não reconhecida + +**Campos opcionais (v3.0)** — as mensagens da v2.x continuam idênticas; os campos abaixo só são acrescentados no fim: +- `evento: inicio` → `reset` (`energia`, `reinicio`, `watchdog` ou `brownout`), `lcd` (`true`/`false`), `ram_livre` +- `type: status` → `ram_livre` +- `evento: erro` → `fase` (`verificacao`, `partida`, `transito`, `saida`), `t_ms` (desde a partida do motor), `bordas_juncao` + +**Robustez:** watchdog de 2 s (o Uno reinicia sozinho se o loop travar); o LCD é procurado em 0x27 e 0x3F e, se não responder, o sistema segue sem ele; o estoque não é persistido (volta a 15 no boot) e o campo `reset` do `inicio` diz por que o Uno reiniciou. Abrir a Serial Monitor reinicia o Uno (auto-reset da USB), o que aparece como `reset: reinicio`. +```` + +- [ ] **Passo 2: `README.md`** + +Na tabela de estados (seção Funcionamento), substitua as três linhas: + +```markdown +| 3 | **ACIONANDO_ESTEIRA** | Liga motor da esteira secundária correspondente | +| 4 | **ENTREGANDO_PECA** | Monitora o sensor da junção (timeout de 12,5 s); só debita o estoque após a confirmação e mantém o motor 3 s depois dela | +| 5 | **ERRO** | Sinaliza falha no LCD, aguarda reset manual | +``` + +por: + +```markdown +| 3 | **ACIONANDO_ESTEIRA** | Liga o motor da esteira escolhida (kick-start e depois o PWM de regime) | +| 4 | **ENTREGANDO_PECA** | Supervisão por marcos: topo livre em 3 s, junção em 12,5 s (débito na confirmação) e 3 s de saída | +| 5 | **ERRO** | Motores desligados; LCD e dashboard mostram onde falhou (ver `docs/ARCHITECTURE.md` §3); aguarda reset | +``` + +- [ ] **Passo 3: `docs/CHANGELOG.md`** + +Logo abaixo de `## [Não publicado]` → `### Alterado`, insira como **primeiro** item: + +```markdown +- **Firmware do Uno v3.0 — supervisão por marcos e robustez (03/10/2026)** + - Firmware reestruturado em módulos: `src/logica/` (C++ puro: protocolo, filtro, supervisor, FSM, telas, causa do reset) testado no PC (`test/firmware_uno`, 38 testes, job `firmware-logica` no CI) e `src/hw/` (motores, sensores, serial, LCD, diagnóstico) + - **Compatibilidade:** mensagens JSON idênticas às da v2.x (testes de contrato byte a byte); ESP32 e servidor não mudaram. Novos campos opcionais (`reset`, `lcd`, `ram_livre`, `fase`, `t_ms`, `bordas_juncao`) e novos erros (`sem_peca_topo`, `juncao_obstruida`, `motor_sem_avanco`, `peca_presa_saida`) + - **Entrega por marcos:** checagem prévia sem ligar o motor; M1 topo livre em 3 s (motor travado para na hora); M2 junção em 12,5 s; débito na confirmação; M3 junção livre após 3 s de saída. Junções capturadas por interrupção, com pulso mínimo de 20 ms + - **Robustez:** kick-start dos motores, watchdog de 2 s, causa do reset no `inicio`, LCD procurado em 0x27/0x3F e opcional, textos na flash (RAM global de 55% para 46%), sem ArduinoJson no Uno + - **Rollback:** firmware v2.x no commit `fddde08`. **Validação em bancada pendente:** `docs/testes/roteiros/firmware_uno_v3_validacao.md` +``` + +- [ ] **Passo 4: `docs/CI-CD.md`** + +Na seção `#### 4. Arduino Compilation (arduino-cli)`, troque a linha `- **Falha em:** Erros de compilação` por `- **Falha em:** Erros de compilação ou RAM global do Uno acima de 65%`. Depois da linha `- Para repetir o job na sua máquina, veja [Compilar o firmware localmente](#compilar-o-firmware-localmente-sem-hardware)`, acrescente: + +```markdown + +#### 5. Lógica do firmware do Uno (`firmware-logica`) +- **O quê:** compila `arduino/data_flow_inventory/src/logica/` com g++ e roda `test/firmware_uno` (FSM, marcos, filtro, protocolo, telas e causa do reset, com tempo simulado) +- **Contrato:** os testes `*_igual_v2` garantem que as mensagens JSON continuam idênticas às da v2.x +- **Local:** `docker run --rm -v ":/w" -w /w gcc:14 make -C test/firmware_uno test` (ou `make -C test/firmware_uno test` com g++ instalado) +``` + +- [ ] **Passo 5: Roteiro de bancada** + +Crie `docs/testes/roteiros/firmware_uno_v3_validacao.md` com exatamente este conteúdo: + +```markdown +# Roteiro — Validação em bancada do firmware do Uno v3.0 + +> Objetivo: confirmar na bancada o que os testes automatizados não alcançam (sensores, motores, alimentação). Execute na ordem; anote o resultado de cada passo. Firmware: `arduino/data_flow_inventory/` (v3.0). Desconecte o ESP32 dos pinos 0/1 durante o upload no Uno. + +Ferramentas: Serial Monitor (9600 baud) **ou** `test/mqtt_probe` (com o ESP32 conectado) e o dashboard. + +> Abrir a Serial Monitor reinicia o Uno (auto-reset da USB) e o estoque volta a 15. Isso é esperado e aparece como `"reset":"reinicio"`. + +## 1. Boot + +1. Ligue o Uno. +2. **Esperado:** a linha `{"type":"evento","evento":"inicio",...,"versao":"3.0",...,"reset":"energia","lcd":true|false,"ram_livre":N}`. +3. Anote `lcd` (se `false`, o LCD não respondeu em 0x27 nem 0x3F: confira 5 V, GND, SDA=A4, SCL=A5) e `ram_livre`. + +## 2. Sensores (antes de qualquer entrega) + +1. Com as três esteiras **vazias**, leia a mensagem `"type":"sensores"`: esperado `"juncao":{"J1":0,"J2":0,"J3":0}`. +2. Passe uma peça sobre cada sensor de junção, um por vez: o valor correspondente deve ir a 1. +3. Repita para os sensores de topo (`"topo"`), com e sem peça. +4. **Se algum sensor ficar sempre em 0 ou sempre em 1**, ajuste o potenciômetro do módulo TCRT5000 (ou a fiação) antes de seguir. Este passo resolve a causa provável da "entrega da B sem débito". + +## 3. Uma entrega por esteira (A, B, C) + +1. Coloque uma peça no topo da esteira e peça pelo dashboard. +2. **Esperado:** `pedido` → status `ACIONANDO_ESTEIRA` → esteira liga → `entrega` com o estoque daquela peça diminuído em 1 → após ~3 s a esteira para → status `AGUARDANDO_PEDIDO`. +3. Confira o débito no dashboard e no LCD. + +## 4. Motor travado (esteira C) + +1. Segure a esteira C com a mão (ou trave a peça no topo) e peça uma peça C. +2. **Esperado:** em ~3 s, `erro` com `"tipo":"motor_sem_avanco","fase":"partida"`, motor parado, estado `ERRO`, **sem reset do Uno** (nenhum novo `inicio`). +3. Envie Reiniciar no dashboard: volta a `AGUARDANDO_PEDIDO`. + +## 5. Timeout no trânsito + +1. Peça uma peça e, depois que ela sair do topo, retire-a da esteira antes da junção. +2. **Esperado:** em 12,5 s desde a partida, `erro` com `"tipo":"timeout","fase":"transito"`, motor parado, estoque **intacto**. + +## 6. Peça presa na saída + +1. Peça uma peça e, quando ela chegar à junção, segure-a sobre o sensor. +2. **Esperado:** `entrega` (débito feito) e, 3 s depois, `erro` com `"tipo":"peca_presa_saida","fase":"saida"`. + +## 7. Junção obstruída + +1. Deixe uma peça parada sobre o sensor de junção e peça uma peça daquela esteira. +2. **Esperado:** `erro` com `"tipo":"juncao_obstruida","fase":"verificacao"`, **sem ligar o motor**. + +## 8. Resistência + +1. Faça 10 entregas seguidas (alternando A, B e C). +2. **Esperado:** nenhum `inicio` novo no meio (sem resets) e `ram_livre` do status estável. + +## Registro + +| Passo | Resultado | Observações | +|---|---|---| +| 1 Boot | | | +| 2 Sensores | | | +| 3 Entregas A/B/C | | | +| 4 Motor travado | | | +| 5 Timeout | | | +| 6 Peça presa | | | +| 7 Junção obstruída | | | +| 8 Resistência | | | +``` + +- [ ] **Passo 6: Verificar links e commit** + +Confira que os links relativos novos existem (`docs/ARCHITECTURE.md` cita `arduino/data_flow_inventory/config.h` e `src/logica/fsm.cpp` só como texto; o roteiro não tem links). Depois: + +```bash +git add docs README.md +git commit -m "docs(firmware): marcos, novos erros, campos opcionais, CI e roteiro de bancada da v3.0 + +Co-Authored-By: Claude Opus 5.5 " +``` + +--- + +### Tarefa 8: Verificação final (nada quebrou) + +Sem arquivos novos. Rode e registre a saída de cada item: + +- [ ] **Passo 1: Lógica do firmware** + +Rode (na raiz do repositório, Git Bash): + +```bash +MSYS_NO_PATHCONV=1 docker run --rm -v "$(cygpath -w "$PWD"):/w" -w /w gcc:14 make -C test/firmware_uno test +``` + +Esperado: `38 testes, 582 verificacoes, 0 falhas`. + +- [ ] **Passo 2: Compilação das placas que o CI compila** + +```bash +CLI="/c/Users/matheusn/AppData/Local/Programs/Arduino IDE/resources/app/lib/backend/resources/arduino-cli.exe" +"$CLI" compile --fqbn arduino:avr:uno --warnings all arduino/data_flow_inventory | grep -E "Sketch uses|Global variables" +"$CLI" compile --fqbn esp32:esp32:esp32 esp32/gateway_mqtt/gateway_mqtt.ino | grep -E "Sketch uses" +"$CLI" compile --fqbn arduino:avr:uno test/esteira_peca_b/arduino_esteiras_ab/arduino_esteiras_ab.ino | grep -E "Sketch uses" +``` + +Esperado: as três compilam; RAM do Uno principal ≤ 65%. (O ESP32 compila só se existir `esp32/gateway_mqtt/secrets.h` local; no CI ele é copiado do `.example`.) + +- [ ] **Passo 3: Fronteiras (ESP32, servidor, frontend e simulador intocados)** + +```bash +git diff --name-only fddde08 -- esp32 server frontend simulator || true +``` + +Esperado: nenhuma linha. + +- [ ] **Passo 4: Suítes existentes como rede de segurança** + +```bash +node --test test/frontend/*.test.mjs 2>&1 | grep -E "^# (pass|fail)" +(cd test/server_metrics && npm ci >/dev/null 2>&1; npm test 2>&1 | grep -E "^# (tests|pass|fail)") +``` + +Esperado: 0 falhas. + +- [ ] **Passo 5: Árvore limpa** + +```bash +git status --short +``` + +Esperado: vazio (o binário `test/firmware_uno/testes` está no `.gitignore`). From 0e5019268c2627ce2b2d21b6968b3d03497d9076 Mon Sep 17 00:00:00 2001 From: Matheus Nespolo Date: Sat, 3 Oct 2026 18:31:19 -0300 Subject: [PATCH 03/21] test(firmware): protocolo serial em logica pura com contrato do formato v2.x Co-Authored-By: Claude Opus 5.5 --- .github/workflows/lint-and-security.yaml | 9 + .gitignore | 3 + .../src/logica/protocolo.cpp | 262 ++++++++++++++++++ .../src/logica/protocolo.h | 58 ++++ .../src/logica/texto_flash.h | 19 ++ test/firmware_uno/Makefile | 18 ++ test/firmware_uno/principal.cpp | 14 + test/firmware_uno/teste.h | 45 +++ test/firmware_uno/teste_protocolo.cpp | 133 +++++++++ 9 files changed, 561 insertions(+) create mode 100644 arduino/data_flow_inventory/src/logica/protocolo.cpp create mode 100644 arduino/data_flow_inventory/src/logica/protocolo.h create mode 100644 arduino/data_flow_inventory/src/logica/texto_flash.h create mode 100644 test/firmware_uno/Makefile create mode 100644 test/firmware_uno/principal.cpp create mode 100644 test/firmware_uno/teste.h create mode 100644 test/firmware_uno/teste_protocolo.cpp diff --git a/.github/workflows/lint-and-security.yaml b/.github/workflows/lint-and-security.yaml index 5fb798b..410a582 100644 --- a/.github/workflows/lint-and-security.yaml +++ b/.github/workflows/lint-and-security.yaml @@ -192,3 +192,12 @@ jobs: - name: Testes das métricas e da infraestrutura de observabilidade (sem Docker) run: cd test/server_metrics && npm test + + firmware-logica: + runs-on: ubuntu-latest + steps: + - name: Checkout código + uses: actions/checkout@v4 + + - name: Testes da lógica do firmware do Uno (g++ no PC) + run: make -C test/firmware_uno test diff --git a/.gitignore b/.gitignore index f9bb8ff..3ab3a95 100644 --- a/.gitignore +++ b/.gitignore @@ -47,3 +47,6 @@ esp32/test_mqtt_cloud/secrets.h.example test/frontend_smoke/test-results/ test/frontend_smoke/playwright-report/ test/frontend_smoke/capturas/ + +# Binário dos testes da lógica do firmware (test/firmware_uno) +test/firmware_uno/testes diff --git a/arduino/data_flow_inventory/src/logica/protocolo.cpp b/arduino/data_flow_inventory/src/logica/protocolo.cpp new file mode 100644 index 0000000..704f0ad --- /dev/null +++ b/arduino/data_flow_inventory/src/logica/protocolo.cpp @@ -0,0 +1,262 @@ +#include "protocolo.h" + +#include + +#include "texto_flash.h" + +// ---- Comandos ----------------------------------------------------------- + +static bool ehEspaco(char c) { return c == ' ' || c == '\t' || c == '\r' || c == '\n'; } + +// ram[0..n) é igual ao texto TXT (inteiro)? +static bool igualTxt(const char* ram, size_t n, const char* txt) { + size_t i = 0; + for (; i < n; i++) { + char c = lerTxt(txt + i); + if (c == 0 || c != ram[i]) return false; + } + return lerTxt(txt + i) == 0; +} + +ComandoTipo interpretarComando(const char* linha, char* peca) { + *peca = 0; + const char* ini = linha; + while (*ini && ehEspaco(*ini)) ini++; + size_t n = strlen(ini); + while (n > 0 && ehEspaco(ini[n - 1])) n--; + if (n == 0) return CMD_NENHUM; + + const size_t TAM_PREFIXO = 9; // "CMD:PECA:" + if (n >= TAM_PREFIXO && igualTxt(ini, TAM_PREFIXO, TXT("CMD:PECA:"))) { + if (n == TAM_PREFIXO + 1) { + char c = ini[TAM_PREFIXO]; + if (c == 'A' || c == 'B' || c == 'C') { + *peca = c; + return CMD_PECA; + } + } + return CMD_PECA_INVALIDA; + } + if (igualTxt(ini, n, TXT("CMD:RESET"))) return CMD_RESET; + return CMD_DESCONHECIDO; +} + +void leitorIniciar(LeitorLinha& l) { + l.n = 0; + l.buf[0] = 0; + l.descartando = false; +} + +bool leitorAlimentar(LeitorLinha& l, char c) { + if (c == '\r') return false; + if (c == '\n') { + if (l.descartando) { + leitorIniciar(l); + return false; + } + l.buf[l.n] = 0; + l.n = 0; + return true; + } + if (l.descartando) return false; + if (l.n >= LeitorLinha::CAPACIDADE) { + l.descartando = true; + l.n = 0; + l.buf[0] = 0; + return false; + } + l.buf[l.n++] = c; + return false; +} + +// ---- Escritor de JSON (sem printf: menos flash no AVR) --------------------- + +namespace { +struct Escritor { + char* b; + size_t cap; + size_t n; + bool ok; +}; + +void iniciar(Escritor& e, char* b, size_t cap) { + e.b = b; + e.cap = cap; + e.n = 0; + e.ok = cap > 0; + if (e.ok) b[0] = 0; +} + +void car(Escritor& e, char c) { + if (!e.ok) return; + if (e.n + 1 >= e.cap) { e.ok = false; return; } + e.b[e.n++] = c; + e.b[e.n] = 0; +} + +// Texto fixo (TXT: flash no AVR). +void txt(Escritor& e, const char* s) { + for (char c = lerTxt(s); c != 0; c = lerTxt(++s)) car(e, c); +} + +void num(Escritor& e, long v) { + char tmp[12]; + int i = 0; + bool neg = v < 0; + unsigned long u = neg ? (unsigned long)(-(v + 1)) + 1UL : (unsigned long)v; + do { tmp[i++] = (char)('0' + (u % 10)); u /= 10; } while (u > 0); + if (neg) car(e, '-'); + while (i > 0) car(e, tmp[--i]); +} + +void numU(Escritor& e, uint32_t v) { + char tmp[11]; + int i = 0; + do { tmp[i++] = (char)('0' + (v % 10)); v /= 10; } while (v > 0); + while (i > 0) car(e, tmp[--i]); +} + +// ,"chave": (k é TXT) +void chave(Escritor& e, const char* k, bool primeira = false) { + if (!primeira) car(e, ','); + car(e, '"'); + txt(e, k); + car(e, '"'); + car(e, ':'); +} + +void campoTexto(Escritor& e, const char* k, const char* v, bool primeira = false) { + chave(e, k, primeira); + car(e, '"'); + txt(e, v); + car(e, '"'); +} + +void campoLetra(Escritor& e, const char* k, char v) { + chave(e, k); + car(e, '"'); + car(e, v); + car(e, '"'); +} + +void campoNum(Escritor& e, const char* k, long v) { + chave(e, k); + num(e, v); +} + +size_t fim(Escritor& e) { + car(e, '}'); + if (!e.ok) { + if (e.cap > 0) e.b[0] = 0; + return 0; + } + return e.n; +} + +void inicioMsg(Escritor& e, char* buf, size_t cap, const char* type) { + iniciar(e, buf, cap); + car(e, '{'); + campoTexto(e, TXT("type"), type, true); +} +} // namespace + +// ---- Mensagens -------------------------------------------------------------- + +size_t formatarStatus(char* buf, size_t cap, const char* estado, + int pecaSolicitada, uint32_t uptimeS, long ramLivre) { + Escritor e; + inicioMsg(e, buf, cap, TXT("status")); + campoTexto(e, TXT("estado"), estado); + campoNum(e, TXT("pecaSolicitada"), pecaSolicitada); + chave(e, TXT("uptime")); + numU(e, uptimeS); + if (ramLivre >= 0) campoNum(e, TXT("ram_livre"), ramLivre); + return fim(e); +} + +size_t formatarEstoque(char* buf, size_t cap, const int estoque[3]) { + Escritor e; + inicioMsg(e, buf, cap, TXT("estoque")); + campoNum(e, TXT("pecaA"), estoque[0]); + campoNum(e, TXT("pecaB"), estoque[1]); + campoNum(e, TXT("pecaC"), estoque[2]); + return fim(e); +} + +size_t formatarSensores(char* buf, size_t cap, const bool topo[3], + const bool juncao[3]) { + Escritor e; + inicioMsg(e, buf, cap, TXT("sensores")); + chave(e, TXT("topo")); + car(e, '{'); + chave(e, TXT("A"), true); num(e, topo[0] ? 1 : 0); + chave(e, TXT("B")); num(e, topo[1] ? 1 : 0); + chave(e, TXT("C")); num(e, topo[2] ? 1 : 0); + car(e, '}'); + chave(e, TXT("juncao")); + car(e, '{'); + chave(e, TXT("J1"), true); num(e, juncao[0] ? 1 : 0); + chave(e, TXT("J2")); num(e, juncao[1] ? 1 : 0); + chave(e, TXT("J3")); num(e, juncao[2] ? 1 : 0); + car(e, '}'); + return fim(e); +} + +size_t formatarEsteiras(char* buf, size_t cap, const bool secundaria[3]) { + Escritor e; + inicioMsg(e, buf, cap, TXT("esteiras")); + campoNum(e, TXT("principal"), 1); // sempre ligada (direto na fonte) + campoNum(e, TXT("secA"), secundaria[0] ? 1 : 0); + campoNum(e, TXT("secB"), secundaria[1] ? 1 : 0); + campoNum(e, TXT("secC"), secundaria[2] ? 1 : 0); + return fim(e); +} + +size_t formatarPedido(char* buf, size_t cap, char peca) { + Escritor e; + inicioMsg(e, buf, cap, TXT("evento")); + campoTexto(e, TXT("evento"), TXT("pedido")); + campoLetra(e, TXT("peca"), peca); + return fim(e); +} + +size_t formatarEntrega(char* buf, size_t cap, char peca, const int estoque[3]) { + Escritor e; + inicioMsg(e, buf, cap, TXT("evento")); + campoTexto(e, TXT("evento"), TXT("entrega")); + campoLetra(e, TXT("peca"), peca); + campoNum(e, TXT("estoqueA"), estoque[0]); + campoNum(e, TXT("estoqueB"), estoque[1]); + campoNum(e, TXT("estoqueC"), estoque[2]); + return fim(e); +} + +size_t formatarErro(char* buf, size_t cap, const char* tipo, char peca, + const char* fase, long tMs, long bordas) { + Escritor e; + inicioMsg(e, buf, cap, TXT("evento")); + campoTexto(e, TXT("evento"), TXT("erro")); + campoTexto(e, TXT("tipo"), tipo); + if (peca != 0) campoLetra(e, TXT("peca"), peca); + if (fase != nullptr) campoTexto(e, TXT("fase"), fase); + if (tMs >= 0) campoNum(e, TXT("t_ms"), tMs); + if (bordas >= 0) campoNum(e, TXT("bordas_juncao"), bordas); + return fim(e); +} + +size_t formatarInicio(char* buf, size_t cap, const char* versao, + const char* reset, int lcd, long ramLivre) { + Escritor e; + inicioMsg(e, buf, cap, TXT("evento")); + campoTexto(e, TXT("evento"), TXT("inicio")); + campoTexto(e, TXT("msg"), TXT("Sistema iniciado")); + campoTexto(e, TXT("versao"), versao); + campoTexto(e, TXT("driver"), TXT("IRF520")); + if (reset != nullptr) campoTexto(e, TXT("reset"), reset); + if (lcd >= 0) { + chave(e, TXT("lcd")); + txt(e, lcd ? TXT("true") : TXT("false")); + } + if (ramLivre >= 0) campoNum(e, TXT("ram_livre"), ramLivre); + return fim(e); +} diff --git a/arduino/data_flow_inventory/src/logica/protocolo.h b/arduino/data_flow_inventory/src/logica/protocolo.h new file mode 100644 index 0000000..ca08e77 --- /dev/null +++ b/arduino/data_flow_inventory/src/logica/protocolo.h @@ -0,0 +1,58 @@ +// ============================================================ +// DATA FLOW INVENTORY — Protocolo serial Uno ↔ ESP32 (lógica pura) +// ------------------------------------------------------------ +// Interpreta comandos (CMD:PECA:X, CMD:RESET) e formata as mensagens +// JSON publicadas pelo Uno. C++ puro, sem Arduino.h: testado no PC +// (test/firmware_uno). O formato de cada mensagem é idêntico ao do +// firmware v2.x (ArduinoJson): mesma ordem de campos, sem espaços. +// Campos novos são sempre OPCIONAIS (só saem quando informados). +// Textos fixos passados às funções formatar* (estado, tipo, fase, versão, +// reset) devem ser TXT(...) — ver texto_flash.h. +// ============================================================ +#pragma once +#include +#include + +// ---- Comandos ----------------------------------------------------------- +enum ComandoTipo : uint8_t { + CMD_NENHUM = 0, // linha vazia + CMD_PECA, // CMD:PECA:A|B|C (peça válida em *peca) + CMD_PECA_INVALIDA, // CMD:PECA: com letra/forma inválida + CMD_RESET, // CMD:RESET + CMD_DESCONHECIDO // qualquer outra linha +}; + +// Interpreta uma linha (sem '\n'). Ignora espaços nas pontas. +// Em CMD_PECA, *peca recebe 'A', 'B' ou 'C'; nos demais casos, 0. +ComandoTipo interpretarComando(const char* linha, char* peca); + +// Acumula bytes da serial até '\n'. '\r' é ignorado. Linha maior que +// a capacidade é DESCARTADA inteira (até o próximo '\n'). +struct LeitorLinha { + static const uint8_t CAPACIDADE = 32; + char buf[CAPACIDADE + 1]; + uint8_t n; + bool descartando; +}; +void leitorIniciar(LeitorLinha& l); +// Devolve true quando uma linha completa está em l.buf (terminada em 0). +bool leitorAlimentar(LeitorLinha& l, char c); + +// ---- Mensagens -------------------------------------------------------------- +// Todas escrevem em buf (capacidade cap), terminam em 0 e devolvem o +// tamanho escrito (sem o 0), ou 0 se não couber. +// Valores "opcionais": passe -1 (inteiros) ou nullptr (texto) para omitir. +size_t formatarStatus(char* buf, size_t cap, const char* estado, + int pecaSolicitada, uint32_t uptimeS, long ramLivre); +size_t formatarEstoque(char* buf, size_t cap, const int estoque[3]); +size_t formatarSensores(char* buf, size_t cap, const bool topo[3], + const bool juncao[3]); +size_t formatarEsteiras(char* buf, size_t cap, const bool secundaria[3]); +size_t formatarPedido(char* buf, size_t cap, char peca); +size_t formatarEntrega(char* buf, size_t cap, char peca, const int estoque[3]); +// peca = 0 omite o campo; fase = nullptr omite; tMs/bordas = -1 omitem. +size_t formatarErro(char* buf, size_t cap, const char* tipo, char peca, + const char* fase, long tMs, long bordas); +// reset = nullptr omite; lcd = -1 omite (0/1 → false/true); ramLivre = -1 omite. +size_t formatarInicio(char* buf, size_t cap, const char* versao, + const char* reset, int lcd, long ramLivre); diff --git a/arduino/data_flow_inventory/src/logica/texto_flash.h b/arduino/data_flow_inventory/src/logica/texto_flash.h new file mode 100644 index 0000000..7252326 --- /dev/null +++ b/arduino/data_flow_inventory/src/logica/texto_flash.h @@ -0,0 +1,19 @@ +// ============================================================ +// DATA FLOW INVENTORY — Textos fixos na memória flash +// ------------------------------------------------------------ +// No AVR, um literal "..." ocupa RAM (o Uno só tem 2 KB). TXT("...") +// grava o texto na flash (PROGMEM) e lerTxt() lê um caractere dele. +// No PC (testes) os dois viram o comportamento normal de C++. +// Convenção: todo `const char*` de texto FIXO nas APIs de src/logica +// é um TXT (flash no AVR). Textos montados em RAM (buffers) não são. +// ============================================================ +#pragma once + +#if defined(__AVR__) +#include +#define TXT(s) PSTR(s) +inline char lerTxt(const char* p) { return (char)pgm_read_byte(p); } +#else +#define TXT(s) (s) +inline char lerTxt(const char* p) { return *p; } +#endif diff --git a/test/firmware_uno/Makefile b/test/firmware_uno/Makefile new file mode 100644 index 0000000..c3c888a --- /dev/null +++ b/test/firmware_uno/Makefile @@ -0,0 +1,18 @@ +# Testes da lógica pura do firmware do Uno (compila no PC com g++). +# Uso: make -C test/firmware_uno test +LOGICA := ../../arduino/data_flow_inventory/src/logica +CXX ?= g++ +CXXFLAGS := -std=c++11 -Wall -Wextra -Werror -O1 -I$(LOGICA) + +FONTES_LOGICA := $(wildcard $(LOGICA)/*.cpp) +FONTES_TESTE := principal.cpp $(wildcard teste_*.cpp) + +.PHONY: test clean +test: testes + ./testes + +testes: $(FONTES_LOGICA) $(FONTES_TESTE) teste.h $(wildcard $(LOGICA)/*.h) + $(CXX) $(CXXFLAGS) -o $@ $(FONTES_TESTE) $(FONTES_LOGICA) + +clean: + rm -f testes diff --git a/test/firmware_uno/principal.cpp b/test/firmware_uno/principal.cpp new file mode 100644 index 0000000..2ba0874 --- /dev/null +++ b/test/firmware_uno/principal.cpp @@ -0,0 +1,14 @@ +// Executor dos testes do firmware: roda todos os TESTE(...) registrados. +#include "teste.h" + +int main() { + int nTestes = 0; + for (teste::Registro* r = teste::lista(); r; r = r->prox) { + int antes = teste::falhas(); + r->f(); + nTestes++; + printf("%s %s\n", teste::falhas() == antes ? "ok " : "FALHOU", r->nome); + } + printf("\n%d testes, %d verificacoes, %d falhas\n", nTestes, teste::total(), teste::falhas()); + return teste::falhas() == 0 ? 0 : 1; +} diff --git a/test/firmware_uno/teste.h b/test/firmware_uno/teste.h new file mode 100644 index 0000000..f7df7e8 --- /dev/null +++ b/test/firmware_uno/teste.h @@ -0,0 +1,45 @@ +// Mini-framework de testes do firmware (sem dependências externas). +#pragma once +#include +#include + +namespace teste { +inline int& falhas() { static int n = 0; return n; } +inline int& total() { static int n = 0; return n; } +typedef void (*FuncaoTeste)(); +struct Registro { const char* nome; FuncaoTeste f; Registro* prox; }; +inline Registro*& lista() { static Registro* r = nullptr; return r; } +struct Registrar { + Registro r; + Registrar(const char* nome, FuncaoTeste f) { + r.nome = nome; r.f = f; r.prox = nullptr; + Registro** p = &lista(); + while (*p) p = &(*p)->prox; // mantém a ordem de declaração + *p = &r; + } +}; +} // namespace teste + +#define TESTE(nome) \ + static void nome(); \ + static teste::Registrar registro_##nome(#nome, nome); \ + static void nome() + +#define VERIFICA(cond) \ + do { \ + teste::total()++; \ + if (!(cond)) { \ + teste::falhas()++; \ + printf(" FALHA %s:%d: %s\n", __FILE__, __LINE__, #cond); \ + } \ + } while (0) + +#define VERIFICA_TEXTO(a, b) \ + do { \ + teste::total()++; \ + if (strcmp((a), (b)) != 0) { \ + teste::falhas()++; \ + printf(" FALHA %s:%d:\n obtido: %s\n esperado: %s\n", \ + __FILE__, __LINE__, (a), (b)); \ + } \ + } while (0) diff --git a/test/firmware_uno/teste_protocolo.cpp b/test/firmware_uno/teste_protocolo.cpp new file mode 100644 index 0000000..e19c287 --- /dev/null +++ b/test/firmware_uno/teste_protocolo.cpp @@ -0,0 +1,133 @@ +// Contrato do protocolo serial. As strings "esperado" reproduzem byte a +// byte o que o firmware v2.x (ArduinoJson) publicava: se um destes testes +// quebrar, o ESP32/servidor/dashboard podem deixar de entender o Uno. +#include "protocolo.h" +#include "teste.h" + +static char buf[160]; // mesmo tamanho do buffer do firmware (src/hw/comunicacao.cpp) + +// ---- Mensagens existentes (formato v2.x, sem campos novos) ---------------- + +TESTE(status_igual_v2) { + formatarStatus(buf, sizeof buf, "AGUARDANDO_PEDIDO", 0, 42, -1); + VERIFICA_TEXTO(buf, "{\"type\":\"status\",\"estado\":\"AGUARDANDO_PEDIDO\",\"pecaSolicitada\":0,\"uptime\":42}"); +} + +TESTE(estoque_igual_v2) { + const int est[3] = {15, 14, 0}; + formatarEstoque(buf, sizeof buf, est); + VERIFICA_TEXTO(buf, "{\"type\":\"estoque\",\"pecaA\":15,\"pecaB\":14,\"pecaC\":0}"); +} + +TESTE(sensores_igual_v2) { + const bool topo[3] = {true, false, true}; + const bool jun[3] = {false, true, false}; + formatarSensores(buf, sizeof buf, topo, jun); + VERIFICA_TEXTO(buf, "{\"type\":\"sensores\",\"topo\":{\"A\":1,\"B\":0,\"C\":1},\"juncao\":{\"J1\":0,\"J2\":1,\"J3\":0}}"); +} + +TESTE(esteiras_igual_v2) { + const bool sec[3] = {false, true, false}; + formatarEsteiras(buf, sizeof buf, sec); + VERIFICA_TEXTO(buf, "{\"type\":\"esteiras\",\"principal\":1,\"secA\":0,\"secB\":1,\"secC\":0}"); +} + +TESTE(pedido_igual_v2) { + formatarPedido(buf, sizeof buf, 'B'); + VERIFICA_TEXTO(buf, "{\"type\":\"evento\",\"evento\":\"pedido\",\"peca\":\"B\"}"); +} + +TESTE(entrega_igual_v2) { + const int est[3] = {15, 14, 15}; + formatarEntrega(buf, sizeof buf, 'B', est); + VERIFICA_TEXTO(buf, "{\"type\":\"evento\",\"evento\":\"entrega\",\"peca\":\"B\",\"estoqueA\":15,\"estoqueB\":14,\"estoqueC\":15}"); +} + +TESTE(erro_igual_v2_com_e_sem_peca) { + formatarErro(buf, sizeof buf, "ocupado", 'C', nullptr, -1, -1); + VERIFICA_TEXTO(buf, "{\"type\":\"evento\",\"evento\":\"erro\",\"tipo\":\"ocupado\",\"peca\":\"C\"}"); + formatarErro(buf, sizeof buf, "peca_invalida", 0, nullptr, -1, -1); + VERIFICA_TEXTO(buf, "{\"type\":\"evento\",\"evento\":\"erro\",\"tipo\":\"peca_invalida\"}"); +} + +TESTE(inicio_igual_v2_sem_campos_novos) { + formatarInicio(buf, sizeof buf, "2.1", nullptr, -1, -1); + VERIFICA_TEXTO(buf, "{\"type\":\"evento\",\"evento\":\"inicio\",\"msg\":\"Sistema iniciado\",\"versao\":\"2.1\",\"driver\":\"IRF520\"}"); +} + +// ---- Campos novos: sempre DEPOIS dos atuais e só quando informados --------- + +TESTE(status_com_ram_livre) { + formatarStatus(buf, sizeof buf, "ERRO", 2, 4294967295UL, 812); + VERIFICA_TEXTO(buf, "{\"type\":\"status\",\"estado\":\"ERRO\",\"pecaSolicitada\":2,\"uptime\":4294967295,\"ram_livre\":812}"); +} + +TESTE(erro_com_diagnostico) { + formatarErro(buf, sizeof buf, "timeout", 'B', "transito", 12503, 0); + VERIFICA_TEXTO(buf, "{\"type\":\"evento\",\"evento\":\"erro\",\"tipo\":\"timeout\",\"peca\":\"B\",\"fase\":\"transito\",\"t_ms\":12503,\"bordas_juncao\":0}"); +} + +TESTE(inicio_com_campos_novos) { + formatarInicio(buf, sizeof buf, "3.0", "watchdog", 0, 640); + VERIFICA_TEXTO(buf, "{\"type\":\"evento\",\"evento\":\"inicio\",\"msg\":\"Sistema iniciado\",\"versao\":\"3.0\",\"driver\":\"IRF520\",\"reset\":\"watchdog\",\"lcd\":false,\"ram_livre\":640}"); +} + +TESTE(maiores_mensagens_cabem_no_buffer_de_160_bytes) { + const bool t[3] = {true, true, true}; + VERIFICA(formatarSensores(buf, sizeof buf, t, t) > 0); + VERIFICA(formatarErro(buf, sizeof buf, "peca_presa_saida", 'C', "transito", 2147483647L, 255) > 0); + VERIFICA(formatarInicio(buf, sizeof buf, "3.0", "reinicio", 1, 2048) > 0); + VERIFICA(strlen(buf) < 160); +} + +TESTE(buffer_pequeno_devolve_zero_e_string_vazia) { + char pequeno[10]; + VERIFICA(formatarPedido(pequeno, sizeof pequeno, 'A') == 0); + VERIFICA_TEXTO(pequeno, ""); +} + +// ---- Comandos ------------------------------------------------------------------- + +TESTE(comandos_validos) { + char p; + VERIFICA(interpretarComando("CMD:PECA:A", &p) == CMD_PECA && p == 'A'); + VERIFICA(interpretarComando("CMD:PECA:C", &p) == CMD_PECA && p == 'C'); + VERIFICA(interpretarComando(" CMD:PECA:B \r", &p) == CMD_PECA && p == 'B'); + VERIFICA(interpretarComando("CMD:RESET", &p) == CMD_RESET && p == 0); +} + +TESTE(comandos_invalidos) { + char p; + VERIFICA(interpretarComando("", &p) == CMD_NENHUM); + VERIFICA(interpretarComando(" ", &p) == CMD_NENHUM); + VERIFICA(interpretarComando("CMD:PECA:", &p) == CMD_PECA_INVALIDA); + VERIFICA(interpretarComando("CMD:PECA:Z", &p) == CMD_PECA_INVALIDA && p == 0); + VERIFICA(interpretarComando("CMD:PECA:AB", &p) == CMD_PECA_INVALIDA); + VERIFICA(interpretarComando("CMD:PECA:a", &p) == CMD_PECA_INVALIDA); + VERIFICA(interpretarComando("CMD:RESETX", &p) == CMD_DESCONHECIDO); + VERIFICA(interpretarComando("{\"type\":\"x\"}", &p) == CMD_DESCONHECIDO); +} + +TESTE(leitor_monta_linhas_e_ignora_cr) { + LeitorLinha l; + leitorIniciar(l); + const char* entrada = "CMD:PE"; + for (const char* c = entrada; *c; c++) VERIFICA(!leitorAlimentar(l, *c)); + const char* resto = "CA:A\r\n"; + bool pronto = false; + for (const char* c = resto; *c; c++) pronto = leitorAlimentar(l, *c); + VERIFICA(pronto); + VERIFICA_TEXTO(l.buf, "CMD:PECA:A"); +} + +TESTE(leitor_descarta_linha_longa_inteira) { + LeitorLinha l; + leitorIniciar(l); + for (int i = 0; i < 50; i++) VERIFICA(!leitorAlimentar(l, 'x')); + VERIFICA(!leitorAlimentar(l, '\n')); // a linha longa some inteira + const char* prox = "CMD:RESET\n"; + bool pronto = false; + for (const char* c = prox; *c; c++) pronto = leitorAlimentar(l, *c); + VERIFICA(pronto); + VERIFICA_TEXTO(l.buf, "CMD:RESET"); +} From 5e91ff32e02230ab2499c820eae5039dd84dd7f5 Mon Sep 17 00:00:00 2001 From: Matheus Nespolo Date: Sat, 3 Oct 2026 18:38:07 -0300 Subject: [PATCH 04/21] feat(firmware): filtro de entrada e causa do reset em logica pura Co-Authored-By: Claude Opus 5.5 --- .../src/logica/causa_reset.cpp | 10 ++++ .../src/logica/causa_reset.h | 19 ++++++++ .../src/logica/filtro_entrada.cpp | 21 +++++++++ .../src/logica/filtro_entrada.h | 24 ++++++++++ test/firmware_uno/teste_filtro_reset.cpp | 47 +++++++++++++++++++ 5 files changed, 121 insertions(+) create mode 100644 arduino/data_flow_inventory/src/logica/causa_reset.cpp create mode 100644 arduino/data_flow_inventory/src/logica/causa_reset.h create mode 100644 arduino/data_flow_inventory/src/logica/filtro_entrada.cpp create mode 100644 arduino/data_flow_inventory/src/logica/filtro_entrada.h create mode 100644 test/firmware_uno/teste_filtro_reset.cpp diff --git a/arduino/data_flow_inventory/src/logica/causa_reset.cpp b/arduino/data_flow_inventory/src/logica/causa_reset.cpp new file mode 100644 index 0000000..259dcd4 --- /dev/null +++ b/arduino/data_flow_inventory/src/logica/causa_reset.cpp @@ -0,0 +1,10 @@ +#include "causa_reset.h" + +#include "texto_flash.h" + +const char* causaReset(uint8_t mcusr, bool assinaturaValida, bool marcaWatchdog) { + if ((mcusr & MCUSR_WDRF) || (assinaturaValida && marcaWatchdog)) return TXT("watchdog"); + if (mcusr & MCUSR_BORF) return TXT("brownout"); + if ((mcusr & MCUSR_PORF) || !assinaturaValida) return TXT("energia"); + return TXT("reinicio"); +} diff --git a/arduino/data_flow_inventory/src/logica/causa_reset.h b/arduino/data_flow_inventory/src/logica/causa_reset.h new file mode 100644 index 0000000..0b5dd7f --- /dev/null +++ b/arduino/data_flow_inventory/src/logica/causa_reset.h @@ -0,0 +1,19 @@ +// ============================================================ +// DATA FLOW INVENTORY — Causa do último reset (lógica pura) +// ------------------------------------------------------------ +// O Optiboot do Uno costuma zerar o MCUSR antes do sketch; por isso a +// causa combina: o MCUSR (quando vier preenchido), uma assinatura em +// RAM não inicializada (.noinit, some quando a energia cai) e a marca +// gravada pela interrupção do watchdog. C++ puro (testado no PC). +// ============================================================ +#pragma once +#include + +// Bits do MCUSR do ATmega328P. +static const uint8_t MCUSR_PORF = 0x01; // energização +static const uint8_t MCUSR_EXTRF = 0x02; // pino de reset +static const uint8_t MCUSR_BORF = 0x04; // brownout +static const uint8_t MCUSR_WDRF = 0x08; // watchdog + +// Devolve TXT("watchdog"), TXT("brownout"), TXT("energia") ou TXT("reinicio"). +const char* causaReset(uint8_t mcusr, bool assinaturaValida, bool marcaWatchdog); diff --git a/arduino/data_flow_inventory/src/logica/filtro_entrada.cpp b/arduino/data_flow_inventory/src/logica/filtro_entrada.cpp new file mode 100644 index 0000000..0299cb8 --- /dev/null +++ b/arduino/data_flow_inventory/src/logica/filtro_entrada.cpp @@ -0,0 +1,21 @@ +#include "filtro_entrada.h" + +void filtroIniciar(FiltroEntrada& f, bool valor, uint32_t agora, uint16_t janelaMs) { + f.estavel = valor; + f.bruto = valor; + f.mudouEm = agora; + f.janelaMs = janelaMs; +} + +Borda filtroAtualizar(FiltroEntrada& f, bool bruto, uint32_t agora) { + if (bruto != f.bruto) { + f.bruto = bruto; + f.mudouEm = agora; + return BORDA_NENHUMA; + } + if (bruto != f.estavel && (uint32_t)(agora - f.mudouEm) >= f.janelaMs) { + f.estavel = bruto; + return bruto ? BORDA_OCUPOU : BORDA_LIBEROU; + } + return BORDA_NENHUMA; +} diff --git a/arduino/data_flow_inventory/src/logica/filtro_entrada.h b/arduino/data_flow_inventory/src/logica/filtro_entrada.h new file mode 100644 index 0000000..4141d69 --- /dev/null +++ b/arduino/data_flow_inventory/src/logica/filtro_entrada.h @@ -0,0 +1,24 @@ +// ============================================================ +// DATA FLOW INVENTORY — Filtro de entrada digital (lógica pura) +// ------------------------------------------------------------ +// Debounce por tempo: a leitura bruta só vira "estável" depois de +// permanecer igual por janelaMs. Usado nos sensores de topo e para o +// nível atual das junções. C++ puro (testado no PC). +// ============================================================ +#pragma once +#include + +struct FiltroEntrada { + bool estavel; // valor filtrado (true = sensor ocupado) + bool bruto; // última leitura bruta + uint32_t mudouEm; // instante da última mudança da leitura bruta + uint16_t janelaMs; // tempo mínimo de estabilidade +}; + +enum Borda : int8_t { BORDA_NENHUMA = 0, BORDA_OCUPOU = 1, BORDA_LIBEROU = -1 }; + +void filtroIniciar(FiltroEntrada& f, bool valor, uint32_t agora, uint16_t janelaMs); + +// Atualiza com a leitura bruta atual e devolve a borda do valor ESTÁVEL +// (se houve). Robusto ao estouro de millis() (aritmética sem sinal). +Borda filtroAtualizar(FiltroEntrada& f, bool bruto, uint32_t agora); diff --git a/test/firmware_uno/teste_filtro_reset.cpp b/test/firmware_uno/teste_filtro_reset.cpp new file mode 100644 index 0000000..01eab9f --- /dev/null +++ b/test/firmware_uno/teste_filtro_reset.cpp @@ -0,0 +1,47 @@ +// Filtro de entrada (debounce) e causa do reset. +#include "causa_reset.h" +#include "filtro_entrada.h" +#include "teste.h" + +// ---- Filtro ------------------------------------------------------------------ + +TESTE(filtro_ignora_pulso_curto) { + FiltroEntrada f; + filtroIniciar(f, false, 0, 20); + VERIFICA(filtroAtualizar(f, true, 100) == BORDA_NENHUMA); + VERIFICA(filtroAtualizar(f, true, 110) == BORDA_NENHUMA); // 10 ms + VERIFICA(filtroAtualizar(f, false, 115) == BORDA_NENHUMA); // voltou + VERIFICA(filtroAtualizar(f, false, 200) == BORDA_NENHUMA); + VERIFICA(!f.estavel); +} + +TESTE(filtro_aceita_nivel_estavel_e_gera_bordas) { + FiltroEntrada f; + filtroIniciar(f, false, 0, 20); + filtroAtualizar(f, true, 100); + VERIFICA(filtroAtualizar(f, true, 120) == BORDA_OCUPOU); + VERIFICA(f.estavel); + filtroAtualizar(f, false, 300); + VERIFICA(filtroAtualizar(f, false, 330) == BORDA_LIBEROU); +} + +TESTE(filtro_funciona_no_estouro_do_millis) { + FiltroEntrada f; + const uint32_t quase = 0xFFFFFFF0UL; + filtroIniciar(f, false, quase, 20); + filtroAtualizar(f, true, quase + 5); + VERIFICA(filtroAtualizar(f, true, quase + 30) == BORDA_OCUPOU); // passou de 0 +} + +// ---- Causa do reset ------------------------------------------------------------ + +TESTE(causa_reset) { + VERIFICA_TEXTO(causaReset(MCUSR_WDRF, false, false), "watchdog"); + VERIFICA_TEXTO(causaReset(0, true, true), "watchdog"); + VERIFICA_TEXTO(causaReset(MCUSR_BORF, true, false), "brownout"); + VERIFICA_TEXTO(causaReset(MCUSR_PORF, true, false), "energia"); + VERIFICA_TEXTO(causaReset(0, false, false), "energia"); + VERIFICA_TEXTO(causaReset(0, true, false), "reinicio"); + VERIFICA_TEXTO(causaReset(MCUSR_EXTRF, true, false), "reinicio"); + VERIFICA_TEXTO(causaReset(0, false, true), "energia"); // marca sem assinatura não vale +} From d521a9182a43b3ee173492cbaba0c7acb0baf95b Mon Sep 17 00:00:00 2001 From: Matheus Nespolo Date: Sat, 3 Oct 2026 18:49:18 -0300 Subject: [PATCH 05/21] feat(firmware): supervisor de entrega por marcos (partida, transito, saida) Co-Authored-By: Claude Opus 5.5 --- .../src/logica/supervisor_entrega.cpp | 51 ++++++++++ .../src/logica/supervisor_entrega.h | 55 +++++++++++ test/firmware_uno/teste_supervisor.cpp | 95 +++++++++++++++++++ 3 files changed, 201 insertions(+) create mode 100644 arduino/data_flow_inventory/src/logica/supervisor_entrega.cpp create mode 100644 arduino/data_flow_inventory/src/logica/supervisor_entrega.h create mode 100644 test/firmware_uno/teste_supervisor.cpp diff --git a/arduino/data_flow_inventory/src/logica/supervisor_entrega.cpp b/arduino/data_flow_inventory/src/logica/supervisor_entrega.cpp new file mode 100644 index 0000000..8778960 --- /dev/null +++ b/arduino/data_flow_inventory/src/logica/supervisor_entrega.cpp @@ -0,0 +1,51 @@ +#include "supervisor_entrega.h" + +void supervisorIniciar(Supervisor& s, const ParamEsteira& p, uint32_t agora) { + s.p = p; + s.fase = FASE_PARTIDA; + s.inicioMs = agora; + s.confirmouEm = agora; +} + +bool passagemValida(const LeituraJuncao& j, uint32_t agora, uint16_t pulsoMinMs) { + if (j.maiorPulsoMs >= pulsoMinMs) return true; + return j.ocupada && (uint32_t)(agora - j.ocupouEm) >= pulsoMinMs; +} + +ResultadoSupervisor supervisorAtualizar(Supervisor& s, uint32_t agora, + bool topoOcupado, const LeituraJuncao& j) { + const uint32_t decorrido = (uint32_t)(agora - s.inicioMs); + + if (s.fase == FASE_PARTIDA || s.fase == FASE_TRANSITO) { + // Uma passagem válida prova o avanço mesmo antes de o filtro do topo liberar. + if (passagemValida(j, agora, s.p.pulsoMinJuncaoMs)) { + s.fase = FASE_SAIDA; + s.confirmouEm = agora; + return SUP_CONFIRMOU; + } + if (s.fase == FASE_PARTIDA) { + if (!topoOcupado) { + s.fase = FASE_TRANSITO; + } else if (decorrido >= s.p.prazoPartidaMs) { + s.fase = FASE_FIM; + return SUP_FALHA_SEM_AVANCO; + } + } + if (s.fase == FASE_TRANSITO && decorrido >= s.p.timeoutMs) { + s.fase = FASE_FIM; + return SUP_FALHA_TIMEOUT; + } + return SUP_SEGUE; + } + + if (s.fase == FASE_SAIDA) { + if ((uint32_t)(agora - s.confirmouEm) >= s.p.saidaMs) { + s.fase = FASE_FIM; + // "Presa" = ainda ocupada e há tempo suficiente para não ser ruído. + bool presa = j.ocupada && (uint32_t)(agora - j.ocupouEm) >= s.p.pulsoMinJuncaoMs; + return presa ? SUP_FALHA_PRESA_SAIDA : SUP_SAIDA_OK; + } + return SUP_SEGUE; + } + return SUP_SEGUE; +} diff --git a/arduino/data_flow_inventory/src/logica/supervisor_entrega.h b/arduino/data_flow_inventory/src/logica/supervisor_entrega.h new file mode 100644 index 0000000..d255fd5 --- /dev/null +++ b/arduino/data_flow_inventory/src/logica/supervisor_entrega.h @@ -0,0 +1,55 @@ +// ============================================================ +// DATA FLOW INVENTORY — Supervisor de entrega por marcos (lógica pura) +// ------------------------------------------------------------ +// M1 Partida : o sensor de topo fica livre em até prazoPartidaMs. +// M2 Trânsito: a junção registra uma passagem válida (pulso ≥ +// pulsoMinJuncaoMs) em até timeoutMs desde a partida. +// M3 Saída : depois da confirmação, o motor segue saidaMs; no fim a +// junção precisa estar livre. +// Todos os prazos contam com aritmética sem sinal (estouro de millis). +// C++ puro (testado no PC). +// ============================================================ +#pragma once +#include + +struct ParamEsteira { + uint8_t pwmRegime; // PWM de regime (0-255) + uint16_t kickMs; // duração do kick-start em PWM 255 + uint16_t prazoPartidaMs; // M1 + uint16_t timeoutMs; // M2 (desde a partida) + uint16_t saidaMs; // M3 + uint16_t pulsoMinJuncaoMs; // largura mínima de uma passagem válida +}; + +// Leitura de um sensor de junção, montada pela camada de hardware a +// partir das bordas capturadas por interrupção. +struct LeituraJuncao { + bool ocupada; // nível atual (true = peça sobre o sensor) + uint32_t ocupouEm; // instante em que ficou ocupada (vale se ocupada) + uint16_t maiorPulsoMs; // maior pulso COMPLETO desde o último zerar + uint8_t bordas; // mudanças de nível desde o último zerar +}; + +enum FaseEntrega : uint8_t { FASE_PARTIDA, FASE_TRANSITO, FASE_SAIDA, FASE_FIM }; + +enum ResultadoSupervisor : uint8_t { + SUP_SEGUE = 0, + SUP_CONFIRMOU, // passagem válida na junção (debitar agora) + SUP_SAIDA_OK, // fim da saída com a junção livre + SUP_FALHA_SEM_AVANCO, // M1 + SUP_FALHA_TIMEOUT, // M2 + SUP_FALHA_PRESA_SAIDA // M3 +}; + +struct Supervisor { + ParamEsteira p; + FaseEntrega fase; + uint32_t inicioMs; + uint32_t confirmouEm; +}; + +void supervisorIniciar(Supervisor& s, const ParamEsteira& p, uint32_t agora); +ResultadoSupervisor supervisorAtualizar(Supervisor& s, uint32_t agora, + bool topoOcupado, const LeituraJuncao& j); +// Passagem válida: pulso completo largo o bastante ou ocupada há tempo suficiente. +bool passagemValida(const LeituraJuncao& j, uint32_t agora, uint16_t pulsoMinMs); diff --git a/test/firmware_uno/teste_supervisor.cpp b/test/firmware_uno/teste_supervisor.cpp new file mode 100644 index 0000000..91388cc --- /dev/null +++ b/test/firmware_uno/teste_supervisor.cpp @@ -0,0 +1,95 @@ +// Supervisor de entrega por marcos (M1 partida, M2 trânsito, M3 saída). +#include "supervisor_entrega.h" +#include "teste.h" + +// ---- Supervisor --------------------------------------------------------------- + +static ParamEsteira paramPadrao() { + ParamEsteira p; + p.pwmRegime = 200; + p.kickMs = 150; + p.prazoPartidaMs = 3000; + p.timeoutMs = 12500; + p.saidaMs = 3000; + p.pulsoMinJuncaoMs = 20; + return p; +} + +static LeituraJuncao livre() { + LeituraJuncao j; + j.ocupada = false; + j.ocupouEm = 0; + j.maiorPulsoMs = 0; + j.bordas = 0; + return j; +} + +TESTE(supervisor_m1_motor_sem_avanco) { + Supervisor s; + supervisorIniciar(s, paramPadrao(), 1000); + LeituraJuncao j = livre(); + VERIFICA(supervisorAtualizar(s, 3999, true, j) == SUP_SEGUE); + VERIFICA(supervisorAtualizar(s, 4000, true, j) == SUP_FALHA_SEM_AVANCO); +} + +TESTE(supervisor_m2_timeout_sem_bordas) { + Supervisor s; + supervisorIniciar(s, paramPadrao(), 0); + LeituraJuncao j = livre(); + VERIFICA(supervisorAtualizar(s, 1000, false, j) == SUP_SEGUE); // topo liberou + VERIFICA(s.fase == FASE_TRANSITO); + VERIFICA(supervisorAtualizar(s, 12499, false, j) == SUP_SEGUE); + VERIFICA(supervisorAtualizar(s, 12500, false, j) == SUP_FALHA_TIMEOUT); +} + +TESTE(supervisor_confirma_por_pulso_completo_capturado) { + Supervisor s; + supervisorIniciar(s, paramPadrao(), 0); + LeituraJuncao j = livre(); + supervisorAtualizar(s, 1000, false, j); + j.maiorPulsoMs = 25; // a interrupção viu um pulso de 25 ms entre dois ciclos + j.bordas = 2; + VERIFICA(supervisorAtualizar(s, 5000, false, j) == SUP_CONFIRMOU); + VERIFICA(s.fase == FASE_SAIDA); +} + +TESTE(supervisor_ignora_pulso_curto_de_ruido) { + Supervisor s; + supervisorIniciar(s, paramPadrao(), 0); + LeituraJuncao j = livre(); + supervisorAtualizar(s, 1000, false, j); + j.maiorPulsoMs = 5; + j.bordas = 2; + VERIFICA(supervisorAtualizar(s, 2000, false, j) == SUP_SEGUE); + j.ocupada = true; + j.ocupouEm = 2000; + VERIFICA(supervisorAtualizar(s, 2010, false, j) == SUP_SEGUE); // 10 ms + VERIFICA(supervisorAtualizar(s, 2020, false, j) == SUP_CONFIRMOU); // 20 ms +} + +TESTE(supervisor_m3_saida_ok_e_peca_presa) { + Supervisor s; + supervisorIniciar(s, paramPadrao(), 0); + LeituraJuncao j = livre(); + j.maiorPulsoMs = 30; + VERIFICA(supervisorAtualizar(s, 4000, false, j) == SUP_CONFIRMOU); + VERIFICA(supervisorAtualizar(s, 6999, false, j) == SUP_SEGUE); + VERIFICA(supervisorAtualizar(s, 7000, false, j) == SUP_SAIDA_OK); + + supervisorIniciar(s, paramPadrao(), 0); + LeituraJuncao k = livre(); + k.ocupada = true; + k.ocupouEm = 4000; + VERIFICA(supervisorAtualizar(s, 4030, false, k) == SUP_CONFIRMOU); + VERIFICA(supervisorAtualizar(s, 7030, false, k) == SUP_FALHA_PRESA_SAIDA); +} + +TESTE(supervisor_timeout_conta_desde_a_partida_no_estouro_do_millis) { + Supervisor s; + const uint32_t ini = 0xFFFFF000UL; + supervisorIniciar(s, paramPadrao(), ini); + LeituraJuncao j = livre(); + supervisorAtualizar(s, ini + 500, false, j); + VERIFICA(supervisorAtualizar(s, ini + 12499, false, j) == SUP_SEGUE); + VERIFICA(supervisorAtualizar(s, ini + 12500, false, j) == SUP_FALHA_TIMEOUT); +} From a8cf76beda2c2949c376087504d7419ecdcdd043 Mon Sep 17 00:00:00 2001 From: Matheus Nespolo Date: Sat, 3 Oct 2026 19:04:49 -0300 Subject: [PATCH 06/21] fix(firmware): ocupouEm marcado apos a leitura de millis nao confirma passagem Co-Authored-By: Claude Opus 5.5 --- .../src/logica/supervisor_entrega.cpp | 14 ++++++++++++-- test/firmware_uno/teste_supervisor.cpp | 13 +++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/arduino/data_flow_inventory/src/logica/supervisor_entrega.cpp b/arduino/data_flow_inventory/src/logica/supervisor_entrega.cpp index 8778960..6d89c3f 100644 --- a/arduino/data_flow_inventory/src/logica/supervisor_entrega.cpp +++ b/arduino/data_flow_inventory/src/logica/supervisor_entrega.cpp @@ -7,9 +7,19 @@ void supervisorIniciar(Supervisor& s, const ParamEsteira& p, uint32_t agora) { s.confirmouEm = agora; } +// Tempo que a junção está ocupada, ou 0 se não está ocupada ou se ocupouEm +// foi marcado no futuro (pela interrupção entre a leitura de millis() e a +// cópia da captura da junção). +static uint32_t ocupadaHa(const LeituraJuncao& j, uint32_t agora) { + if (!j.ocupada) return 0; + int32_t decorrido = (int32_t)(agora - j.ocupouEm); + if (decorrido < 0) return 0; // ocupouEm está no futuro + return (uint32_t)decorrido; +} + bool passagemValida(const LeituraJuncao& j, uint32_t agora, uint16_t pulsoMinMs) { if (j.maiorPulsoMs >= pulsoMinMs) return true; - return j.ocupada && (uint32_t)(agora - j.ocupouEm) >= pulsoMinMs; + return ocupadaHa(j, agora) >= pulsoMinMs; } ResultadoSupervisor supervisorAtualizar(Supervisor& s, uint32_t agora, @@ -42,7 +52,7 @@ ResultadoSupervisor supervisorAtualizar(Supervisor& s, uint32_t agora, if ((uint32_t)(agora - s.confirmouEm) >= s.p.saidaMs) { s.fase = FASE_FIM; // "Presa" = ainda ocupada e há tempo suficiente para não ser ruído. - bool presa = j.ocupada && (uint32_t)(agora - j.ocupouEm) >= s.p.pulsoMinJuncaoMs; + bool presa = ocupadaHa(j, agora) >= s.p.pulsoMinJuncaoMs; return presa ? SUP_FALHA_PRESA_SAIDA : SUP_SAIDA_OK; } return SUP_SEGUE; diff --git a/test/firmware_uno/teste_supervisor.cpp b/test/firmware_uno/teste_supervisor.cpp index 91388cc..4584cb9 100644 --- a/test/firmware_uno/teste_supervisor.cpp +++ b/test/firmware_uno/teste_supervisor.cpp @@ -93,3 +93,16 @@ TESTE(supervisor_timeout_conta_desde_a_partida_no_estouro_do_millis) { VERIFICA(supervisorAtualizar(s, ini + 12499, false, j) == SUP_SEGUE); VERIFICA(supervisorAtualizar(s, ini + 12500, false, j) == SUP_FALHA_TIMEOUT); } + +TESTE(supervisor_ocupou_depois_da_leitura_do_millis_nao_confirma) { + // A interrupção marcou ocupouEm 1 ms DEPOIS do agora lido pelo loop. + Supervisor s; + supervisorIniciar(s, paramPadrao(), 0); + LeituraJuncao j = livre(); + supervisorAtualizar(s, 1000, false, j); + j.ocupada = true; + j.ocupouEm = 2001; + VERIFICA(supervisorAtualizar(s, 2000, false, j) == SUP_SEGUE); + VERIFICA(!passagemValida(j, 2000, 20)); + VERIFICA(supervisorAtualizar(s, 2021, false, j) == SUP_CONFIRMOU); // 20 ms reais depois +} From 8c015eaa86744033f95d9dac44171a52d8011e42 Mon Sep 17 00:00:00 2001 From: Matheus Nespolo Date: Sat, 3 Oct 2026 19:10:09 -0300 Subject: [PATCH 07/21] feat(firmware): maquina de estados com checagem previa e debito so na confirmacao Co-Authored-By: Claude Opus 5.5 --- .../data_flow_inventory/src/logica/fsm.cpp | 202 ++++++++++++++ arduino/data_flow_inventory/src/logica/fsm.h | 92 +++++++ test/firmware_uno/teste_fsm.cpp | 252 ++++++++++++++++++ 3 files changed, 546 insertions(+) create mode 100644 arduino/data_flow_inventory/src/logica/fsm.cpp create mode 100644 arduino/data_flow_inventory/src/logica/fsm.h create mode 100644 test/firmware_uno/teste_fsm.cpp diff --git a/arduino/data_flow_inventory/src/logica/fsm.cpp b/arduino/data_flow_inventory/src/logica/fsm.cpp new file mode 100644 index 0000000..600e9f2 --- /dev/null +++ b/arduino/data_flow_inventory/src/logica/fsm.cpp @@ -0,0 +1,202 @@ +#include "fsm.h" + +#include "texto_flash.h" + +const char* nomeEstado(Estado e) { + switch (e) { + case AGUARDANDO_PEDIDO: return TXT("AGUARDANDO_PEDIDO"); + case VERIFICANDO_ESTOQUE: return TXT("VERIFICANDO_ESTOQUE"); + case ACIONANDO_ESTEIRA: return TXT("ACIONANDO_ESTEIRA"); + case ENTREGANDO_PECA: return TXT("ENTREGANDO_PECA"); + default: return TXT("ERRO"); + } +} + +const char* nomeErro(TipoErro e) { + switch (e) { + case ERR_SEM_ESTOQUE: return TXT("sem_estoque"); + case ERR_SEM_PECA_TOPO: return TXT("sem_peca_topo"); + case ERR_JUNCAO_OBSTRUIDA: return TXT("juncao_obstruida"); + case ERR_MOTOR_SEM_AVANCO: return TXT("motor_sem_avanco"); + case ERR_TIMEOUT: return TXT("timeout"); + case ERR_PECA_PRESA_SAIDA: return TXT("peca_presa_saida"); + case ERR_OCUPADO: return TXT("ocupado"); + case ERR_PECA_INVALIDA: return TXT("peca_invalida"); + case ERR_COMANDO_DESCONHECIDO: return TXT("comando_desconhecido"); + default: return TXT("desconhecido"); + } +} + +const char* nomeFaseErro(FaseErro f) { + switch (f) { + case FERR_VERIFICACAO: return TXT("verificacao"); + case FERR_PARTIDA: return TXT("partida"); + case FERR_TRANSITO: return TXT("transito"); + case FERR_SAIDA: return TXT("saida"); + default: return nullptr; + } +} + +char letraPeca(uint8_t peca) { + return (peca >= 1 && peca <= 3) ? (char)('A' + peca - 1) : 0; +} + +static void emitir(SaidasFsm& out, TipoEvento tipo, char peca = 0, + TipoErro erro = ERR_NENHUM, FaseErro fase = FERR_NENHUMA, + long tMs = -1, long bordas = -1) { + if (out.nEventos >= SaidasFsm::MAX_EVENTOS) return; + Evento& e = out.eventos[out.nEventos++]; + e.tipo = tipo; + e.peca = peca; + e.erro = erro; + e.fase = fase; + e.tMs = tMs; + e.bordas = bordas; +} + +// Vai para ERRO: motores param (fsmMotorLigado passa a ser falso), o erro é +// publicado com o diagnóstico e o dashboard recebe o novo estado e as esteiras. +static void falhar(Fsm& f, SaidasFsm& out, TipoErro erro, FaseErro fase, + long tMs, long bordas) { + f.estado = ERRO; + f.emPausa = false; + f.tela = TELA_ERRO; + f.erroAtual = erro; + emitir(out, EV_ERRO, letraPeca(f.peca), erro, fase, tMs, bordas); + emitir(out, EV_ESTADO); + emitir(out, EV_ESTEIRAS); +} + +void fsmIniciar(Fsm& f, const ParamEsteira params[3], int estoqueInicial) { + f.estado = AGUARDANDO_PEDIDO; + f.peca = 0; + for (uint8_t i = 0; i < 3; i++) { + f.estoque[i] = estoqueInicial; + f.params[i] = params[i]; + } + supervisorIniciar(f.sup, params[0], 0); + f.sup.fase = FASE_FIM; + f.emPausa = false; + f.pausaDesde = 0; + f.tela = TELA_ESTOQUE; + f.erroAtual = ERR_NENHUM; +} + +bool fsmMotorLigado(const Fsm& f, uint8_t idx) { + if (f.peca != idx + 1) return false; + if (f.estado != ENTREGANDO_PECA) return false; + if (f.emPausa) return false; + return f.sup.fase != FASE_FIM; +} + +static void tratarComando(Fsm& f, const EntradasFsm& in, SaidasFsm& out) { + switch (in.cmd) { + case CMD_PECA: + if (f.estado != AGUARDANDO_PEDIDO) { + emitir(out, EV_ERRO, in.cmdPeca, ERR_OCUPADO); + return; + } + f.peca = (uint8_t)(in.cmdPeca - 'A' + 1); + emitir(out, EV_PEDIDO, in.cmdPeca); + f.estado = VERIFICANDO_ESTOQUE; + return; + case CMD_PECA_INVALIDA: + emitir(out, EV_ERRO, 0, ERR_PECA_INVALIDA); + return; + case CMD_DESCONHECIDO: + emitir(out, EV_ERRO, 0, ERR_COMANDO_DESCONHECIDO); + return; + case CMD_RESET: + if (f.estado == ERRO) { + f.estado = AGUARDANDO_PEDIDO; + f.peca = 0; + f.emPausa = false; + f.sup.fase = FASE_FIM; + f.tela = TELA_ESTOQUE; + f.erroAtual = ERR_NENHUM; + emitir(out, EV_ESTADO); + emitir(out, EV_ESTOQUE); + } + // RESET fora de ERRO: ignorado de propósito (sistema já estável). + return; + default: + return; + } +} + +void fsmPasso(Fsm& f, const EntradasFsm& in, SaidasFsm& out) { + out.nEventos = 0; + out.zerarJuncao = -1; + + tratarComando(f, in, out); + const uint8_t idx = (f.peca >= 1 && f.peca <= 3) ? (uint8_t)(f.peca - 1) : 0; + + switch (f.estado) { + case VERIFICANDO_ESTOQUE: + // Checagem prévia: qualquer falha vai para ERRO SEM ligar o motor. + if (f.estoque[idx] <= 0) { + falhar(f, out, ERR_SEM_ESTOQUE, FERR_VERIFICACAO, -1, -1); + } else if (!in.topo[idx]) { + falhar(f, out, ERR_SEM_PECA_TOPO, FERR_VERIFICACAO, -1, -1); + } else if (in.juncao[idx].ocupada) { + falhar(f, out, ERR_JUNCAO_OBSTRUIDA, FERR_VERIFICACAO, -1, -1); + } else { + f.estado = ACIONANDO_ESTEIRA; + f.tela = TELA_ENTREGANDO; + emitir(out, EV_ESTADO); + } + break; + + case ACIONANDO_ESTEIRA: + out.zerarJuncao = (int8_t)idx; + supervisorIniciar(f.sup, f.params[idx], in.agora); + f.estado = ENTREGANDO_PECA; + emitir(out, EV_ESTEIRAS); + break; + + case ENTREGANDO_PECA: { + if (f.emPausa) { + if ((uint32_t)(in.agora - f.pausaDesde) >= PAUSA_POS_ENTREGA_MS) { + f.emPausa = false; + f.peca = 0; + f.estado = AGUARDANDO_PEDIDO; + f.tela = TELA_ESTOQUE; + emitir(out, EV_ESTADO); + emitir(out, EV_ESTEIRAS); + } + break; + } + const LeituraJuncao& j = in.juncao[idx]; + const long tMs = (long)(uint32_t)(in.agora - f.sup.inicioMs); + switch (supervisorAtualizar(f.sup, in.agora, in.topo[idx], j)) { + case SUP_CONFIRMOU: + f.estoque[idx]--; + f.tela = TELA_SAINDO; + emitir(out, EV_ENTREGA, letraPeca(f.peca)); + emitir(out, EV_ESTOQUE); + break; + case SUP_SAIDA_OK: + f.emPausa = true; + f.pausaDesde = in.agora; + f.tela = TELA_ENTREGUE; + emitir(out, EV_ESTEIRAS); + break; + case SUP_FALHA_SEM_AVANCO: + falhar(f, out, ERR_MOTOR_SEM_AVANCO, FERR_PARTIDA, tMs, j.bordas); + break; + case SUP_FALHA_TIMEOUT: + falhar(f, out, ERR_TIMEOUT, FERR_TRANSITO, tMs, j.bordas); + break; + case SUP_FALHA_PRESA_SAIDA: + falhar(f, out, ERR_PECA_PRESA_SAIDA, FERR_SAIDA, tMs, j.bordas); + break; + default: + break; + } + break; + } + + default: // AGUARDANDO_PEDIDO e ERRO: nada a fazer (motores desligados) + break; + } +} diff --git a/arduino/data_flow_inventory/src/logica/fsm.h b/arduino/data_flow_inventory/src/logica/fsm.h new file mode 100644 index 0000000..f99e6cf --- /dev/null +++ b/arduino/data_flow_inventory/src/logica/fsm.h @@ -0,0 +1,92 @@ +// ============================================================ +// DATA FLOW INVENTORY — Máquina de estados do Uno (lógica pura) +// ------------------------------------------------------------ +// 5 estados (fluxograma oficial) + supervisão da entrega por marcos. +// A FSM não toca em hardware: recebe as entradas de um ciclo e devolve +// os eventos a publicar. Motores e LCD são derivados do estado +// (fsmMotorLigado / tela): em ERRO, nenhum motor fica ligado. +// Invariante: o estoque só diminui em SUP_CONFIRMOU (passagem válida +// na junção da esteira pedida). +// C++ puro (testado no PC). +// ============================================================ +#pragma once +#include + +#include "protocolo.h" +#include "supervisor_entrega.h" + +enum Estado : uint8_t { + AGUARDANDO_PEDIDO = 0, + VERIFICANDO_ESTOQUE, + ACIONANDO_ESTEIRA, + ENTREGANDO_PECA, + ERRO +}; + +enum TipoErro : uint8_t { + ERR_NENHUM = 0, + ERR_SEM_ESTOQUE, // contador zerado → ERRO + ERR_SEM_PECA_TOPO, // contador > 0, topo vazio → ERRO + ERR_JUNCAO_OBSTRUIDA, // junção ocupada antes de ligar → ERRO + ERR_MOTOR_SEM_AVANCO, // M1 → ERRO + ERR_TIMEOUT, // M2 → ERRO + ERR_PECA_PRESA_SAIDA, // M3 → ERRO + ERR_OCUPADO, // rejeição (não muda o estado) + ERR_PECA_INVALIDA, // rejeição + ERR_COMANDO_DESCONHECIDO // rejeição +}; + +enum FaseErro : uint8_t { FERR_NENHUMA = 0, FERR_VERIFICACAO, FERR_PARTIDA, FERR_TRANSITO, FERR_SAIDA }; + +// Nomes publicados no JSON (TXT: flash no AVR). +const char* nomeEstado(Estado e); +const char* nomeErro(TipoErro e); +const char* nomeFaseErro(FaseErro f); // nullptr para FERR_NENHUMA + +enum TelaLcd : uint8_t { TELA_ESTOQUE = 0, TELA_ENTREGANDO, TELA_SAINDO, TELA_ENTREGUE, TELA_ERRO }; + +enum TipoEvento : uint8_t { EV_PEDIDO = 0, EV_ERRO, EV_ENTREGA, EV_ESTADO, EV_ESTOQUE, EV_ESTEIRAS }; + +struct Evento { + TipoEvento tipo; + char peca; // 'A'..'C' ou 0 + TipoErro erro; // EV_ERRO + FaseErro fase; // EV_ERRO + long tMs; // EV_ERRO: tempo desde a partida (ou -1) + long bordas; // EV_ERRO: bordas na junção (ou -1) +}; + +struct EntradasFsm { + uint32_t agora; + bool topo[3]; // sensores de topo FILTRADOS (true = peça presente) + LeituraJuncao juncao[3]; // junções (capturadas por interrupção) + ComandoTipo cmd; // comando recebido neste ciclo (CMD_NENHUM se nenhum) + char cmdPeca; // peça do CMD_PECA +}; + +struct SaidasFsm { + static const uint8_t MAX_EVENTOS = 4; + Evento eventos[MAX_EVENTOS]; + uint8_t nEventos; + int8_t zerarJuncao; // -1 = nada; 0..2 = zerar a captura dessa junção +}; + +struct Fsm { + Estado estado; + uint8_t peca; // 0 = nenhuma; 1..3 = A..C + int estoque[3]; + ParamEsteira params[3]; + Supervisor sup; + bool emPausa; // pausa pós-entrega (motor parado) + uint32_t pausaDesde; + TelaLcd tela; + TipoErro erroAtual; // último erro que levou a ERRO (para o LCD) +}; + +static const uint16_t PAUSA_POS_ENTREGA_MS = 1500; + +void fsmIniciar(Fsm& f, const ParamEsteira params[3], int estoqueInicial); +void fsmPasso(Fsm& f, const EntradasFsm& in, SaidasFsm& out); +// Esteira idx (0..2) deve estar ligada agora? +bool fsmMotorLigado(const Fsm& f, uint8_t idx); +char letraPeca(uint8_t peca); // 1..3 → 'A'..'C'; outro → 0 diff --git a/test/firmware_uno/teste_fsm.cpp b/test/firmware_uno/teste_fsm.cpp new file mode 100644 index 0000000..7f980db --- /dev/null +++ b/test/firmware_uno/teste_fsm.cpp @@ -0,0 +1,252 @@ +// Cenários da FSM com tempo simulado (casos de bancada P1–P5 inclusos). +#include "fsm.h" +#include "teste.h" + +namespace { + +ParamEsteira param() { + ParamEsteira p; + p.pwmRegime = 200; + p.kickMs = 150; + p.prazoPartidaMs = 3000; + p.timeoutMs = 12500; + p.saidaMs = 3000; + p.pulsoMinJuncaoMs = 20; + return p; +} + +// Bancada simulada: guarda as entradas e registra o que a FSM publicou. +struct Bancada { + Fsm f; + EntradasFsm in; + SaidasFsm out; + char log[2048]; // eventos publicados, um por linha + int debitos; // vezes em que o estoque diminuiu + int confirmacoes; // vezes em que houve EV_ENTREGA + + Bancada() { + ParamEsteira p[3] = {param(), param(), param()}; + fsmIniciar(f, p, 15); + memset(&in, 0, sizeof in); + for (int i = 0; i < 3; i++) in.topo[i] = true; // uma peça em cada topo + log[0] = 0; + debitos = 0; + confirmacoes = 0; + } + + void anota(const char* s) { + strncat(log, s, sizeof log - strlen(log) - 1); + strncat(log, "\n", sizeof log - strlen(log) - 1); + } + + // Um ciclo do loop no instante t (ms). + void passo(uint32_t t, ComandoTipo cmd = CMD_NENHUM, char peca = 0) { + in.agora = t; + in.cmd = cmd; + in.cmdPeca = peca; + int antes = f.estoque[0] + f.estoque[1] + f.estoque[2]; + fsmPasso(f, in, out); + int depois = f.estoque[0] + f.estoque[1] + f.estoque[2]; + debitos += antes - depois; + for (uint8_t i = 0; i < out.nEventos; i++) { + const Evento& e = out.eventos[i]; + char linha[64]; + switch (e.tipo) { + case EV_PEDIDO: snprintf(linha, sizeof linha, "pedido %c", e.peca); break; + case EV_ENTREGA: snprintf(linha, sizeof linha, "entrega %c", e.peca); confirmacoes++; break; + case EV_ESTADO: snprintf(linha, sizeof linha, "estado %s", nomeEstado(f.estado)); break; + case EV_ESTOQUE: snprintf(linha, sizeof linha, "estoque %d %d %d", f.estoque[0], f.estoque[1], f.estoque[2]); break; + case EV_ESTEIRAS: snprintf(linha, sizeof linha, "esteiras %d%d%d", fsmMotorLigado(f, 0), fsmMotorLigado(f, 1), fsmMotorLigado(f, 2)); break; + default: + snprintf(linha, sizeof linha, "erro %s %c %s", nomeErro(e.erro), e.peca ? e.peca : '-', + nomeFaseErro(e.fase) ? nomeFaseErro(e.fase) : "-"); + break; + } + anota(linha); + } + if (out.zerarJuncao >= 0) { + LeituraJuncao& j = in.juncao[out.zerarJuncao]; + j.maiorPulsoMs = 0; + j.bordas = 0; + } + } + + // Avança de t0 a t1 em passos de 5 ms. + void ate(uint32_t t0, uint32_t t1) { + for (uint32_t t = t0; t <= t1; t += 5) passo(t); + } + + bool contem(const char* trecho) const { return strstr(log, trecho) != nullptr; } + bool algumMotor() const { return fsmMotorLigado(f, 0) || fsmMotorLigado(f, 1) || fsmMotorLigado(f, 2); } +}; + +} // namespace + +TESTE(entrega_normal_debita_na_confirmacao_e_volta_a_aguardar) { + for (int idx = 0; idx < 3; idx++) { + Bancada b; + const char letra = (char)('A' + idx); + b.passo(0, CMD_PECA, letra); // pedido + verificação + VERIFICA(b.f.estado == ACIONANDO_ESTEIRA); + b.passo(5); // liga a esteira + VERIFICA(b.f.estado == ENTREGANDO_PECA); + VERIFICA(fsmMotorLigado(b.f, (uint8_t)idx)); + b.in.topo[idx] = false; // a peça saiu do topo + b.ate(10, 4000); + VERIFICA(b.f.estoque[idx] == 15); // ainda não confirmou + b.in.juncao[idx].ocupada = true; // peça sobre a junção + b.in.juncao[idx].ocupouEm = 4005; + b.ate(4005, 4030); + VERIFICA(b.f.estoque[idx] == 14); // debitou na confirmação + VERIFICA(fsmMotorLigado(b.f, (uint8_t)idx)); // motor segue na saída + b.in.juncao[idx].ocupada = false; // a peça caiu na principal + b.ate(4035, 7025); + VERIFICA(!b.algumMotor()); // saída de 3 s terminou + VERIFICA(b.f.estado == ENTREGANDO_PECA && b.f.emPausa); + b.ate(7030, 8600); + VERIFICA(b.f.estado == AGUARDANDO_PEDIDO); + VERIFICA(b.debitos == 1 && b.confirmacoes == 1); + char esperado[48]; + snprintf(esperado, sizeof esperado, "pedido %c\nestado ACIONANDO_ESTEIRA", letra); + VERIFICA(b.contem(esperado)); + VERIFICA(b.contem("estado AGUARDANDO_PEDIDO")); + } +} + +TESTE(checagem_previa_vai_para_erro_sem_ligar_motor) { + { // estoque zerado + Bancada b; + b.f.estoque[0] = 0; + b.passo(0, CMD_PECA, 'A'); + VERIFICA(b.f.estado == ERRO && !b.algumMotor()); + VERIFICA(b.contem("erro sem_estoque A verificacao\nestado ERRO\nesteiras 000")); + } + { // contador > 0 mas sem peça no topo + Bancada b; + b.in.topo[1] = false; + b.passo(0, CMD_PECA, 'B'); + VERIFICA(b.f.estado == ERRO && !b.algumMotor()); + VERIFICA(b.contem("erro sem_peca_topo B verificacao")); + } + { // junção já ocupada antes de ligar (sensor obstruído ou peça esquecida) + Bancada b; + b.in.juncao[2].ocupada = true; + b.passo(0, CMD_PECA, 'C'); + VERIFICA(b.f.estado == ERRO && !b.algumMotor()); + VERIFICA(b.contem("erro juncao_obstruida C verificacao")); + } +} + +TESTE(caso_p2_esteira_c_travada_para_em_3s_sem_debito) { + Bancada b; + b.passo(0, CMD_PECA, 'C'); + b.passo(5); + b.ate(10, 3000); // topo continua ocupado: motor não avança + VERIFICA(b.f.estado == ENTREGANDO_PECA); + b.ate(3005, 3010); + VERIFICA(b.f.estado == ERRO); + VERIFICA(!b.algumMotor()); + VERIFICA(b.contem("erro motor_sem_avanco C partida")); + VERIFICA(b.debitos == 0); +} + +TESTE(caso_p1_juncao_b_nao_registra_e_termina_em_timeout_sem_debito) { + Bancada b; + b.passo(0, CMD_PECA, 'B'); + b.passo(5); + b.in.topo[1] = false; // saiu do topo... + b.ate(10, 12500); // ...mas J2 nunca mudou + VERIFICA(b.f.estado == ENTREGANDO_PECA); + b.ate(12505, 12510); + VERIFICA(b.f.estado == ERRO && !b.algumMotor()); + VERIFICA(b.contem("erro timeout B transito")); + VERIFICA(b.debitos == 0); +} + +TESTE(ruido_na_juncao_nao_debita) { + Bancada b; + b.passo(0, CMD_PECA, 'A'); + b.passo(5); + b.in.topo[0] = false; + b.ate(10, 2000); + b.in.juncao[0].maiorPulsoMs = 8; // pico de 8 ms capturado pela interrupção + b.in.juncao[0].bordas = 2; + b.ate(2005, 3000); + VERIFICA(b.debitos == 0); + VERIFICA(b.f.estado == ENTREGANDO_PECA); +} + +TESTE(peca_presa_na_saida_ja_debitou_e_vai_para_erro) { + Bancada b; + b.passo(0, CMD_PECA, 'A'); + b.passo(5); + b.in.topo[0] = false; + b.in.juncao[0].ocupada = true; + b.in.juncao[0].ocupouEm = 1000; + b.ate(1000, 1030); + VERIFICA(b.debitos == 1); + b.ate(1035, 4100); // junção continua ocupada + VERIFICA(b.f.estado == ERRO && !b.algumMotor()); + VERIFICA(b.contem("erro peca_presa_saida A saida")); + VERIFICA(b.debitos == 1); +} + +TESTE(comandos_rejeitados_nao_mudam_estado) { + Bancada b; + b.passo(0, CMD_PECA, 'A'); + b.passo(5); + b.passo(10, CMD_PECA, 'B'); // ocupado, com a peça PEDIDA + VERIFICA(b.contem("erro ocupado B -")); + b.passo(15, CMD_PECA_INVALIDA, 0); + VERIFICA(b.contem("erro peca_invalida - -")); + b.passo(20, CMD_DESCONHECIDO, 0); + VERIFICA(b.contem("erro comando_desconhecido - -")); + b.passo(25, CMD_RESET, 0); // RESET fora de ERRO: ignorado + VERIFICA(b.f.estado == ENTREGANDO_PECA); + VERIFICA(fsmMotorLigado(b.f, 0)); +} + +TESTE(reset_em_erro_volta_a_aguardar_e_republica) { + Bancada b; + b.f.estoque[0] = 0; + b.passo(0, CMD_PECA, 'A'); + VERIFICA(b.f.estado == ERRO); + b.log[0] = 0; + b.passo(5, CMD_RESET, 0); + VERIFICA(b.f.estado == AGUARDANDO_PEDIDO); + VERIFICA(b.contem("estado AGUARDANDO_PEDIDO\nestoque 0 15 15")); + b.passo(10, CMD_PECA, 'B'); // aceita novo pedido + VERIFICA(b.contem("pedido B")); +} + +TESTE(saida_segura_em_erro_mantem_motores_desligados) { + Bancada b; + b.passo(0, CMD_PECA, 'C'); + b.passo(5); + b.ate(10, 3010); // motor_sem_avanco + VERIFICA(b.f.estado == ERRO); + for (uint32_t t = 3015; t < 20000; t += 50) { + b.in.topo[2] = (t / 50) % 2; // sensores mudando não religam nada + b.in.juncao[2].ocupada = (t / 100) % 2; + b.passo(t); + VERIFICA(!b.algumMotor()); + } +} + +TESTE(propriedade_nenhum_debito_sem_confirmacao) { + // Varre combinações de comportamento do topo e da junção e confere que + // o número de débitos é sempre igual ao de entregas publicadas. + for (int topoSai = 0; topoSai < 2; topoSai++) { + for (int pulso = 0; pulso < 40; pulso += 7) { + Bancada b; + b.passo(0, CMD_PECA, 'B'); + b.passo(5); + if (topoSai) b.in.topo[1] = false; + b.ate(10, 2000); + b.in.juncao[1].maiorPulsoMs = (uint16_t)pulso; + b.ate(2005, 16000); + VERIFICA(b.debitos == b.confirmacoes); + VERIFICA(b.debitos <= 1); + } + } +} From 749fdc602940ab865c9c04ffd8f8de4dcdda67bb Mon Sep 17 00:00:00 2001 From: Matheus Nespolo Date: Sat, 3 Oct 2026 19:18:35 -0300 Subject: [PATCH 08/21] fix(firmware): propriedade de debito por varredura, juncao ocupada antes da partida nao confirma Co-Authored-By: Claude Opus 5.5 --- arduino/data_flow_inventory/src/logica/fsm.h | 2 +- .../src/logica/supervisor_entrega.cpp | 6 ++- test/firmware_uno/teste_fsm.cpp | 47 +++++++++++++------ test/firmware_uno/teste_supervisor.cpp | 11 +++++ 4 files changed, 50 insertions(+), 16 deletions(-) diff --git a/arduino/data_flow_inventory/src/logica/fsm.h b/arduino/data_flow_inventory/src/logica/fsm.h index f99e6cf..4831e99 100644 --- a/arduino/data_flow_inventory/src/logica/fsm.h +++ b/arduino/data_flow_inventory/src/logica/fsm.h @@ -65,7 +65,7 @@ struct EntradasFsm { }; struct SaidasFsm { - static const uint8_t MAX_EVENTOS = 4; + static const uint8_t MAX_EVENTOS = 6; // pior caso atual: 4 (pedido/ocupado + falha) Evento eventos[MAX_EVENTOS]; uint8_t nEventos; int8_t zerarJuncao; // -1 = nada; 0..2 = zerar a captura dessa junção diff --git a/arduino/data_flow_inventory/src/logica/supervisor_entrega.cpp b/arduino/data_flow_inventory/src/logica/supervisor_entrega.cpp index 6d89c3f..b77d5c2 100644 --- a/arduino/data_flow_inventory/src/logica/supervisor_entrega.cpp +++ b/arduino/data_flow_inventory/src/logica/supervisor_entrega.cpp @@ -28,7 +28,11 @@ ResultadoSupervisor supervisorAtualizar(Supervisor& s, uint32_t agora, if (s.fase == FASE_PARTIDA || s.fase == FASE_TRANSITO) { // Uma passagem válida prova o avanço mesmo antes de o filtro do topo liberar. - if (passagemValida(j, agora, s.p.pulsoMinJuncaoMs)) { + // Por nível, só vale a ocupação que começou DEPOIS da partida: uma junção + // ocupada na janela entre a checagem prévia e a partida não é passagem. + LeituraJuncao jv = j; + if (jv.ocupada && (int32_t)(jv.ocupouEm - s.inicioMs) < 0) jv.ocupada = false; + if (passagemValida(jv, agora, s.p.pulsoMinJuncaoMs)) { s.fase = FASE_SAIDA; s.confirmouEm = agora; return SUP_CONFIRMOU; diff --git a/test/firmware_uno/teste_fsm.cpp b/test/firmware_uno/teste_fsm.cpp index 7f980db..db0b62a 100644 --- a/test/firmware_uno/teste_fsm.cpp +++ b/test/firmware_uno/teste_fsm.cpp @@ -233,20 +233,39 @@ TESTE(saida_segura_em_erro_mantem_motores_desligados) { } } -TESTE(propriedade_nenhum_debito_sem_confirmacao) { - // Varre combinações de comportamento do topo e da junção e confere que - // o número de débitos é sempre igual ao de entregas publicadas. - for (int topoSai = 0; topoSai < 2; topoSai++) { - for (int pulso = 0; pulso < 40; pulso += 7) { - Bancada b; - b.passo(0, CMD_PECA, 'B'); - b.passo(5); - if (topoSai) b.in.topo[1] = false; - b.ate(10, 2000); - b.in.juncao[1].maiorPulsoMs = (uint16_t)pulso; - b.ate(2005, 16000); - VERIFICA(b.debitos == b.confirmacoes); - VERIFICA(b.debitos <= 1); +TESTE(propriedade_debito_so_com_passagem_valida_na_juncao_pedida) { + // Varre peça pedida × junção que recebe o pulso × largura × instante e + // compara com o esperado calculado à parte: só debita se o pulso veio na + // junção da esteira PEDIDA, durou >= 20 ms e chegou antes do timeout. + const uint16_t larguras[] = {5, 19, 20, 35}; + const uint32_t instantes[] = {2000, 13000}; + for (int pedida = 0; pedida < 3; pedida++) { + for (int jun = 0; jun < 3; jun++) { + for (int l = 0; l < 4; l++) { + for (int k = 0; k < 2; k++) { + Bancada b; + const uint32_t t0 = instantes[k]; + const uint16_t larg = larguras[l]; + b.passo(0, CMD_PECA, (char)('A' + pedida)); + b.passo(5); + b.in.topo[pedida] = false; + b.ate(10, t0 - 5); + LeituraJuncao& j = b.in.juncao[jun]; + for (uint32_t t = t0; t <= t0 + 40; t++) { // passo de 1 ms em volta do pulso + if (t == t0) { j.ocupada = true; j.ocupouEm = t0; j.bordas++; } + if (t == t0 + larg) { + j.ocupada = false; + if (larg > j.maiorPulsoMs) j.maiorPulsoMs = larg; + j.bordas++; + } + b.passo(t); + } + b.ate(t0 + 45, 20000); + const int esperado = (jun == pedida && larg >= 20 && t0 < 12500) ? 1 : 0; + VERIFICA(b.debitos == esperado); + VERIFICA(b.debitos == b.confirmacoes); + } + } } } } diff --git a/test/firmware_uno/teste_supervisor.cpp b/test/firmware_uno/teste_supervisor.cpp index 4584cb9..50f9011 100644 --- a/test/firmware_uno/teste_supervisor.cpp +++ b/test/firmware_uno/teste_supervisor.cpp @@ -106,3 +106,14 @@ TESTE(supervisor_ocupou_depois_da_leitura_do_millis_nao_confirma) { VERIFICA(!passagemValida(j, 2000, 20)); VERIFICA(supervisorAtualizar(s, 2021, false, j) == SUP_CONFIRMOU); // 20 ms reais depois } + +TESTE(supervisor_juncao_ocupada_antes_da_partida_nao_confirma) { + // Ocupou entre a checagem prévia e a partida do motor: não é passagem. + Supervisor s; + supervisorIniciar(s, paramPadrao(), 1000); + LeituraJuncao j = livre(); + j.ocupada = true; + j.ocupouEm = 995; + VERIFICA(supervisorAtualizar(s, 1100, false, j) == SUP_SEGUE); + VERIFICA(supervisorAtualizar(s, 13500, false, j) == SUP_FALHA_TIMEOUT); +} From 15922e764d3bd161ec59c4df5cf7bcb776bb4b59 Mon Sep 17 00:00:00 2001 From: Matheus Nespolo Date: Sat, 3 Oct 2026 19:21:31 -0300 Subject: [PATCH 09/21] feat(firmware): textos do LCD 16x2 por situacao Co-Authored-By: Claude Opus 5.5 --- .../data_flow_inventory/src/logica/telas.cpp | 98 +++++++++++++++++++ .../data_flow_inventory/src/logica/telas.h | 13 +++ test/firmware_uno/teste_telas.cpp | 57 +++++++++++ 3 files changed, 168 insertions(+) create mode 100644 arduino/data_flow_inventory/src/logica/telas.cpp create mode 100644 arduino/data_flow_inventory/src/logica/telas.h create mode 100644 test/firmware_uno/teste_telas.cpp diff --git a/arduino/data_flow_inventory/src/logica/telas.cpp b/arduino/data_flow_inventory/src/logica/telas.cpp new file mode 100644 index 0000000..bc220ca --- /dev/null +++ b/arduino/data_flow_inventory/src/logica/telas.cpp @@ -0,0 +1,98 @@ +#include "telas.h" + +#include + +#include "texto_flash.h" + +namespace { +// Acrescenta texto fixo (TXT) respeitando o limite de 16 colunas. +void anexaTxt(char* dst, const char* txt) { + uint8_t n = (uint8_t)strlen(dst); + for (char c = lerTxt(txt); c != 0 && n < LCD_COLUNAS; c = lerTxt(++txt)) dst[n++] = c; + dst[n] = 0; +} + +// Acrescenta texto em RAM respeitando o limite de 16 colunas. +void anexaRam(char* dst, const char* s) { + uint8_t n = (uint8_t)strlen(dst); + while (*s && n < LCD_COLUNAS) dst[n++] = *s++; + dst[n] = 0; +} + +void anexaNum(char* dst, int v) { + char tmp[7]; + uint8_t i = 0; + bool neg = v < 0; + unsigned u = neg ? (unsigned)(-v) : (unsigned)v; + do { tmp[i++] = (char)('0' + (u % 10)); u /= 10; } while (u > 0 && i < 6); + char s[8]; + uint8_t k = 0; + if (neg) s[k++] = '-'; + while (i > 0) s[k++] = tmp[--i]; + s[k] = 0; + anexaRam(dst, s); +} + +void anexaLetra(char* dst, char c) { + char s[2] = {c, 0}; + anexaRam(dst, s); +} + +void telaErro(const Fsm& f, char* l1, char* l2) { + const char p = letraPeca(f.peca); + switch (f.erroAtual) { + case ERR_MOTOR_SEM_AVANCO: + anexaTxt(l1, TXT("ERRO: Motor ")); anexaLetra(l1, p); anexaTxt(l2, TXT("Sem avanco")); + break; + case ERR_TIMEOUT: + anexaTxt(l1, TXT("ERRO: Timeout ")); anexaLetra(l1, p); anexaTxt(l2, TXT("Retire a peca")); + break; + case ERR_JUNCAO_OBSTRUIDA: + anexaTxt(l1, TXT("ERRO: Juncao J")); anexaNum(l1, f.peca); anexaTxt(l2, TXT("Obstruida")); + break; + case ERR_SEM_PECA_TOPO: + anexaTxt(l1, TXT("ERRO: Topo ")); anexaLetra(l1, p); anexaTxt(l2, TXT("Sem peca")); + break; + case ERR_SEM_ESTOQUE: + anexaTxt(l1, TXT("ERRO: Estoque ")); anexaLetra(l1, p); anexaTxt(l2, TXT("Zerado")); + break; + case ERR_PECA_PRESA_SAIDA: + anexaTxt(l1, TXT("ERRO: Saida ")); anexaLetra(l1, p); anexaTxt(l2, TXT("Peca presa")); + break; + default: + anexaTxt(l1, TXT("ERRO")); + anexaTxt(l2, nomeErro(f.erroAtual)); + break; + } +} +} // namespace + +void textoTela(const Fsm& f, char* l1, char* l2) { + l1[0] = 0; + l2[0] = 0; + const char p = letraPeca(f.peca); + switch (f.tela) { + case TELA_ENTREGANDO: + anexaTxt(l1, TXT("Entregando ")); anexaLetra(l1, p); + anexaTxt(l2, TXT("Aguarda juncao")); + break; + case TELA_SAINDO: + anexaTxt(l1, TXT("Saindo da")); + anexaTxt(l2, TXT("esteira...")); + break; + case TELA_ENTREGUE: + anexaTxt(l1, TXT("Entrega OK!")); + anexaTxt(l2, TXT("Entregue! ")); anexaLetra(l2, p); anexaTxt(l2, TXT(":")); + anexaNum(l2, (f.peca >= 1 && f.peca <= 3) ? f.estoque[f.peca - 1] : 0); + break; + case TELA_ERRO: + telaErro(f, l1, l2); + break; + default: // TELA_ESTOQUE + anexaTxt(l1, TXT("Estoque:")); + anexaTxt(l2, TXT("A:")); anexaNum(l2, f.estoque[0]); + anexaTxt(l2, TXT(" B:")); anexaNum(l2, f.estoque[1]); + anexaTxt(l2, TXT(" C:")); anexaNum(l2, f.estoque[2]); + break; + } +} diff --git a/arduino/data_flow_inventory/src/logica/telas.h b/arduino/data_flow_inventory/src/logica/telas.h new file mode 100644 index 0000000..4779b14 --- /dev/null +++ b/arduino/data_flow_inventory/src/logica/telas.h @@ -0,0 +1,13 @@ +// ============================================================ +// DATA FLOW INVENTORY — Textos do LCD 16x2 (lógica pura) +// ------------------------------------------------------------ +// Cada tela vira duas linhas de no máximo 16 caracteres. A camada de +// hardware só redesenha quando o texto muda. C++ puro (testado no PC). +// ============================================================ +#pragma once +#include "fsm.h" + +static const uint8_t LCD_COLUNAS = 16; + +// l1 e l2 precisam ter espaço para LCD_COLUNAS + 1 caracteres. +void textoTela(const Fsm& f, char* l1, char* l2); diff --git a/test/firmware_uno/teste_telas.cpp b/test/firmware_uno/teste_telas.cpp new file mode 100644 index 0000000..03b2210 --- /dev/null +++ b/test/firmware_uno/teste_telas.cpp @@ -0,0 +1,57 @@ +// Textos do LCD (16 colunas). +#include "fsm.h" +#include "telas.h" +#include "teste.h" + +static ParamEsteira paramPadrao() { + ParamEsteira p; + p.pwmRegime = 200; + p.kickMs = 150; + p.prazoPartidaMs = 3000; + p.timeoutMs = 12500; + p.saidaMs = 3000; + p.pulsoMinJuncaoMs = 20; + return p; +} + +// ---- Telas --------------------------------------------------------------------- + +static Fsm fsmNova() { + Fsm f; + ParamEsteira p[3] = {paramPadrao(), paramPadrao(), paramPadrao()}; + fsmIniciar(f, p, 15); + return f; +} + +TESTE(telas_cabem_em_16_colunas) { + char l1[17], l2[17]; + Fsm f = fsmNova(); + textoTela(f, l1, l2); + VERIFICA_TEXTO(l1, "Estoque:"); + VERIFICA_TEXTO(l2, "A:15 B:15 C:15"); + + f.peca = 2; + f.tela = TELA_ENTREGANDO; + textoTela(f, l1, l2); + VERIFICA_TEXTO(l1, "Entregando B"); + + f.tela = TELA_ENTREGUE; + f.estoque[1] = 14; + textoTela(f, l1, l2); + VERIFICA_TEXTO(l2, "Entregue! B:14"); + + const TipoErro erros[] = {ERR_MOTOR_SEM_AVANCO, ERR_TIMEOUT, ERR_JUNCAO_OBSTRUIDA, ERR_SEM_PECA_TOPO, + ERR_SEM_ESTOQUE, ERR_PECA_PRESA_SAIDA, ERR_COMANDO_DESCONHECIDO}; + const char* esperado1[] = {"ERRO: Motor B", "ERRO: Timeout B", "ERRO: Juncao J2", "ERRO: Topo B", + "ERRO: Estoque B", "ERRO: Saida B", "ERRO"}; + const char* esperado2[] = {"Sem avanco", "Retire a peca", "Obstruida", "Sem peca", + "Zerado", "Peca presa", "comando_desconhe"}; + f.tela = TELA_ERRO; + for (int i = 0; i < 7; i++) { + f.erroAtual = erros[i]; + textoTela(f, l1, l2); + VERIFICA_TEXTO(l1, esperado1[i]); + VERIFICA_TEXTO(l2, esperado2[i]); + VERIFICA(strlen(l1) <= 16 && strlen(l2) <= 16); + } +} From d5fa4c47b6d7651ccf070efcc78cdc85392f6c9e Mon Sep 17 00:00:00 2001 From: Matheus Nespolo Date: Sat, 3 Oct 2026 19:25:05 -0300 Subject: [PATCH 10/21] feat(firmware): Uno v3.0 modular com marcos, watchdog e diagnostico Co-Authored-By: Claude Opus 5.5 --- .github/workflows/lint-and-security.yaml | 9 +- arduino/data_flow_inventory/config.h | 50 ++ .../data_flow_inventory.ino | 763 ++---------------- .../src/hw/comunicacao.cpp | 81 ++ .../data_flow_inventory/src/hw/comunicacao.h | 25 + .../src/hw/diagnostico.cpp | 68 ++ .../data_flow_inventory/src/hw/diagnostico.h | 15 + arduino/data_flow_inventory/src/hw/lcd.cpp | 57 ++ arduino/data_flow_inventory/src/hw/lcd.h | 12 + .../data_flow_inventory/src/hw/motores.cpp | 54 ++ arduino/data_flow_inventory/src/hw/motores.h | 18 + .../data_flow_inventory/src/hw/sensores.cpp | 101 +++ arduino/data_flow_inventory/src/hw/sensores.h | 20 + 13 files changed, 594 insertions(+), 679 deletions(-) create mode 100644 arduino/data_flow_inventory/config.h create mode 100644 arduino/data_flow_inventory/src/hw/comunicacao.cpp create mode 100644 arduino/data_flow_inventory/src/hw/comunicacao.h create mode 100644 arduino/data_flow_inventory/src/hw/diagnostico.cpp create mode 100644 arduino/data_flow_inventory/src/hw/diagnostico.h create mode 100644 arduino/data_flow_inventory/src/hw/lcd.cpp create mode 100644 arduino/data_flow_inventory/src/hw/lcd.h create mode 100644 arduino/data_flow_inventory/src/hw/motores.cpp create mode 100644 arduino/data_flow_inventory/src/hw/motores.h create mode 100644 arduino/data_flow_inventory/src/hw/sensores.cpp create mode 100644 arduino/data_flow_inventory/src/hw/sensores.h diff --git a/.github/workflows/lint-and-security.yaml b/.github/workflows/lint-and-security.yaml index 410a582..6b2f816 100644 --- a/.github/workflows/lint-and-security.yaml +++ b/.github/workflows/lint-and-security.yaml @@ -119,8 +119,13 @@ jobs: - name: Criar secrets.h de exemplo (arquivo real não é versionado) run: cp esp32/gateway_mqtt/secrets.h.example esp32/gateway_mqtt/secrets.h - - name: Compilar Arduino Uno (principal) - run: arduino-cli compile --fqbn arduino:avr:uno --warnings all arduino/data_flow_inventory/data_flow_inventory.ino + - name: Compilar Arduino Uno (principal) e conferir RAM (máx. 65%) + run: | + set -o pipefail + arduino-cli compile --fqbn arduino:avr:uno --warnings all arduino/data_flow_inventory/data_flow_inventory.ino 2>&1 | tee uno.log + ram=$(grep -oP 'Global variables use \d+ bytes \(\K\d+' uno.log) + echo "RAM global do Uno: ${ram}%" + test "$ram" -le 65 - name: Compilar ESP32 (gateway MQTT) run: arduino-cli compile --fqbn esp32:esp32:esp32 --warnings all esp32/gateway_mqtt/gateway_mqtt.ino diff --git a/arduino/data_flow_inventory/config.h b/arduino/data_flow_inventory/config.h new file mode 100644 index 0000000..36238c2 --- /dev/null +++ b/arduino/data_flow_inventory/config.h @@ -0,0 +1,50 @@ +// ============================================================ +// DATA FLOW INVENTORY — Configuração do firmware do Uno +// ------------------------------------------------------------ +// Pinos (iguais ao firmware v2.x e ao diagrama elétrico) e parâmetros +// por esteira. Para calibrar uma esteira, mude só a linha dela. +// ============================================================ +#pragma once +#include "src/logica/supervisor_entrega.h" + +#define VERSAO_FIRMWARE "3.0" + +// ---- Serial (deve ser IGUAL ao Serial2 do ESP32) -------------------------- +#define BAUD_SERIAL 9600 + +// ---- Motores DC (IRF520: 1 pino PWM por motor) ---------------------------- +#define MOTOR_A 9 // Timer1 +#define MOTOR_B 10 // Timer1 +#define MOTOR_C 11 // Timer2 + +// ---- Sensores TCRT5000 (LOW = peça detectada; INPUT_PULLUP) --------------- +#define SENSOR_TOPO_A A0 +#define SENSOR_TOPO_B A1 +#define SENSOR_TOPO_C A2 +#define SENSOR_JUNCAO_J1 A3 // PCINT11 (porta C) +#define SENSOR_JUNCAO_J2 2 // PCINT18 (porta D) +#define SENSOR_JUNCAO_J3 4 // PCINT20 (porta D) + +// ---- LCD I2C 16x2 (endereços procurados no boot) --------------------------- +#define LCD_ENDERECO_1 0x27 +#define LCD_ENDERECO_2 0x3F + +// ---- Sistema ----------------------------------------------------------------- +#define ESTOQUE_INICIAL 15 // peças de cada tipo ao ligar (não persiste) +#define INTERVALO_AMOSTRA_MS 5 // amostragem dos sensores +#define FILTRO_TOPO_MS 20 // estabilidade exigida no sensor de topo +#define INTERVALO_PUBLICACAO 1000 // ciclo das mensagens periódicas +#define PASSOS_PUBLICACAO 4 // status, estoque, sensores, esteiras +#define TELA_ABERTURA_MS 2000 // "Data Flow / Inventory v3.0" no boot + +// ---- Parâmetros por esteira (A, B, C) ---------------------------------------- +// pwmRegime : PWM depois do kick (bancada: mínimo ~150 para mover com peça) +// kickMs : partida em PWM 255 para vencer o atrito estático +// prazoPartidaMs : M1 — o topo precisa ficar livre neste prazo +// timeoutMs : M2 — a junção precisa confirmar a passagem neste prazo +// saidaMs : M3 — motor ligado depois da confirmação +// pulsoMinJuncaoMs : pulso mínimo na junção para valer como passagem +// pwm kick partida timeout saida pulso +#define PARAM_ESTEIRA_A { 200, 150, 3000, 12500, 3000, 20 } +#define PARAM_ESTEIRA_B { 200, 150, 3000, 12500, 3000, 20 } +#define PARAM_ESTEIRA_C { 200, 150, 3000, 12500, 3000, 20 } diff --git a/arduino/data_flow_inventory/data_flow_inventory.ino b/arduino/data_flow_inventory/data_flow_inventory.ino index 09b46b3..869cb50 100644 --- a/arduino/data_flow_inventory/data_flow_inventory.ino +++ b/arduino/data_flow_inventory/data_flow_inventory.ino @@ -2,704 +2,113 @@ // DATA FLOW INVENTORY — Sistema de Intralogística Automatizada // SENAI São Caetano do Sul — Engenharia de Controle e Automação // Autores: Henrique Moni, Matheus Nespolo, Murilo Tolardo, Vitor Marcolongo -// Ano: 2026 +// Ano: 2026 — Firmware do Arduino Uno v3.0 // ============================================================ -// Descrição: -// Máquina de estados com 5 etapas: +// Máquina de estados com 5 etapas (fluxograma oficial): // AGUARDANDO_PEDIDO → VERIFICANDO_ESTOQUE → ACIONANDO_ESTEIRA // → ENTREGANDO_PECA → ERRO -// Entrega: a peça só é debitada do estoque depois que o sensor de junção da -// esteira confirma a passagem; sem confirmação em TIMEOUT_ENTREGA → ERRO. -// 3 esteiras secundárias (A, B, C) alimentam a esteira principal. -// Roda de estoque com 3 compartimentos ao final da principal. +// A entrega é supervisionada por marcos (src/logica/supervisor_entrega): +// M1 partida — o topo fica livre em 3 s (senão motor_sem_avanco) +// M2 trânsito — a junção confirma a passagem em 12,5 s (senão timeout) +// M3 saída — motor 3 s depois da confirmação; a junção precisa liberar +// O estoque só diminui na confirmação da junção da esteira pedida. // -// Comunicação bidirecional com ESP32 via Serial (JSON): -// - Publica eventos, estado, sensores e estoque -// - Recebe comandos do front-end via ESP32 (CMD:PECA:A, CMD:RESET) +// Organização: +// config.h pinos e parâmetros por esteira +// src/logica/ regras puras (FSM, marcos, filtro, protocolo, telas), +// testadas no PC em test/firmware_uno +// src/hw/ motores, sensores, serial, LCD e diagnóstico // -// Driver: IRF520 (MOSFET) — 1 pino PWM por motor -// Botões físicos: DESABILITADOS (controle via dashboard web) -// Separador (roda giratória): motor de passo 28BYJ-48 + driver ULN2003 -// CÓDIGO COMENTADO — habilitar quando o hardware for montado -// ============================================================ - -#include -#include -#include - -// ============================================================ -// CONFIGURAÇÃO DO LCD -// ============================================================ -LiquidCrystal_I2C lcd(0x27, 16, 2); +// Comunicação com o ESP32 (Serial 9600): mensagens JSON idênticas às do +// firmware v2.x, com campos opcionais novos. Comandos: CMD:PECA:X, CMD:RESET. +// Botões físicos e separador (motor de passo) seguem desabilitados; o código +// antigo deles está no histórico do Git (firmware v2.1). +// ============================================================ + +#include "config.h" +#include "src/hw/comunicacao.h" +#include "src/hw/diagnostico.h" +#include "src/hw/lcd.h" +#include "src/hw/motores.h" +#include "src/hw/sensores.h" +#include "src/logica/fsm.h" +#include "src/logica/telas.h" +#include "src/logica/texto_flash.h" + +static const ParamEsteira PARAMS[3] = {PARAM_ESTEIRA_A, PARAM_ESTEIRA_B, PARAM_ESTEIRA_C}; + +static Fsm fsm; +static bool lcdPresente = false; +static uint32_t inicioMs = 0; +static uint32_t ultimaAmostra = 0; +static uint32_t ultimaPublicacao = 0; +static uint8_t passoPublicacao = 0; -// ============================================================ -// PINOS — MOTORES DC (via driver IRF520) -// Cada motor usa 1 pino PWM (SIG do módulo IRF520) -// ============================================================ -#define MOTOR_A 9 // PWM — Esteira A (secundária) -#define MOTOR_B 10 // PWM — Esteira B (secundária) -#define MOTOR_C 11 // PWM — Esteira C (secundária) - -// ============================================================ -// PINOS — SENSORES IR (TCRT5000) -// ============================================================ -#define SENSOR_TOPO_A A0 -#define SENSOR_TOPO_B A1 -#define SENSOR_TOPO_C A2 -#define SENSOR_JUNCAO_J1 A3 -#define SENSOR_JUNCAO_J2 2 -#define SENSOR_JUNCAO_J3 4 - -// ============================================================ -// BOTÕES FÍSICOS — DESABILITADOS -// Controle exclusivo via dashboard web (Serial/ESP32) -// Para reativar, descomente as linhas marcadas com "// [HABILITAR]" -// ============================================================ -// #define BTN_PECA_A 22 // [HABILITAR] -// #define BTN_PECA_B 23 // [HABILITAR] -// #define BTN_PECA_C 24 // [HABILITAR] -// #define BTN_RESET 25 // [HABILITAR] - -// ============================================================ -// PINO — SEPARADOR (RODA GIRATÓRIA) — MOTOR DE PASSO -// MANTIDO COMO COMENTÁRIO — habilitar quando o hardware for montado -// Motor de passo 28BYJ-48 + driver ULN2003 (4 fios IN1-IN4). -// Pinos escolhidos: 5, 6, 7, 8 — sequenciais, únicos digitais livres no Uno. -// ============================================================ -// #include // [HABILITAR] biblioteca padrão da Arduino IDE -// #define STEPPER_IN1 5 // [HABILITAR] -// #define STEPPER_IN2 6 // [HABILITAR] -// #define STEPPER_IN3 7 // [HABILITAR] -// #define STEPPER_IN4 8 // [HABILITAR] -// #define STEPS_PER_REV 2048 // 28BYJ-48 em passo completo — conferir no datasheet do driver usado -// #define SEPARADOR_RPM 10 // velocidade do giro — ajustar conforme torque necessário -// Stepper motorSeparador(STEPS_PER_REV, STEPPER_IN1, STEPPER_IN3, STEPPER_IN2, STEPPER_IN4); // [HABILITAR] -// long SEPARADOR_POSICAO_INICIAL = 0; // 0° — compartimento neutro -// long SEPARADOR_POSICAO_A = STEPS_PER_REV / 3; // 120° — compartimento A -// long SEPARADOR_POSICAO_B = (STEPS_PER_REV * 2) / 3; // 240° — compartimento B -// long SEPARADOR_POSICAO_C = 0; // 0° — compartimento C (mesmo ponto que o neutro, ajustar layout físico se preciso) -// long posicaoAtualSeparador = 0; // posição atual em passos (0..STEPS_PER_REV-1) - -// ============================================================ -// PARÂMETROS DO SISTEMA -// ============================================================ -#define VELOCIDADE_PRINCIPAL 180 // PWM 0-255 -#define VELOCIDADE_SECUNDARIA 200 // PWM 0-255 (padrão das esteiras secundárias) -// Velocidade por esteira secundária (PWM 0-255). Cada esteira tem a sua constante -// para poder ser calibrada sozinha. Em bancada o PWM mínimo que move a esteira -// com peça foi ~150 (docs/testes/plano_de_testes.md): NÃO fique abaixo disso sem -// medir. A C começa igual a A e B; para igualar o tempo de entrega de um motor -// diferente, calibre o valor da C em bancada. -#define VELOCIDADE_ESTEIRA_A VELOCIDADE_SECUNDARIA -#define VELOCIDADE_ESTEIRA_B VELOCIDADE_SECUNDARIA -#define VELOCIDADE_ESTEIRA_C VELOCIDADE_SECUNDARIA -#define FILTRO_JUNCAO_MS 30 // ms — leitura do sensor de junção precisa ficar estável -#define TIMEOUT_ENTREGA 12500 // ms — tempo máximo para a peça sair do topo e ser - // CONFIRMADA pelo sensor de junção (medição 25/08: ~5 s - // até o sensor; folga ampla). NÃO inclui os - // TEMPO_SAIDA_ESTEIRA_MS de saída, que contam depois da - // confirmação. Estourou → ERRO e motor desligado. -#define DEBOUNCE_BTN 200 // ms -#define INTERVALO_PUBLICACAO 1000 // ms — ciclo completo: cada mensagem periódica sai 1x por ciclo -#define PASSOS_PUBLICACAO 4 // mensagens por ciclo (status, estoque, sensores, esteiras), uma por vez -#define ESTOQUE_INICIAL 15 // peças de cada tipo ao ligar (o LCD 16x2 comporta até 99 por peça) - -// ============================================================ -// ESTADOS DA MÁQUINA DE ESTADOS -// ============================================================ -enum Estado { - AGUARDANDO_PEDIDO, - VERIFICANDO_ESTOQUE, - ACIONANDO_ESTEIRA, - ENTREGANDO_PECA, - ERRO -}; - -const char* nomesEstados[] = { - "AGUARDANDO_PEDIDO", - "VERIFICANDO_ESTOQUE", - "ACIONANDO_ESTEIRA", - "ENTREGANDO_PECA", - "ERRO" -}; - -// ============================================================ -// VARIÁVEIS GLOBAIS -// ============================================================ -Estado estadoAtual = AGUARDANDO_PEDIDO; -int pecaSolicitada = 0; // 1 = A, 2 = B, 3 = C -int estoque[4] = {0, ESTOQUE_INICIAL, ESTOQUE_INICIAL, ESTOQUE_INICIAL}; // índice 1=A, 2=B, 3=C -unsigned long tempoInicio = 0; -unsigned long ultimoDebounce = 0; -unsigned long ultimaPublicacao = 0; -uint8_t passoPublicacao = 0; // qual mensagem periódica sai na próxima vez -bool erroTimeout = false; -bool erroSemEstoque = false; - -// Buffer para leitura NÃO-BLOQUEANTE de comandos da Serial (do ESP32). -// Substitui Serial.readStringUntil(), que trava o loop por até 1 s se a -// linha chegar fragmentada — crítico durante ENTREGANDO_PECA. -String bufferComando = ""; - -// Fase de SAÍDA DA ESTEIRA: depois que a peça atinge o sensor de junção, a -// esteira secundária continua ligada por este tempo para a peça sair -// fisicamente da esteira (a junção só marca "quase no fim"). Calibrar em -// bancada. Este tempo é contado DEPOIS da confirmação pelo sensor de junção e -// NÃO entra no TIMEOUT_ENTREGA (que só vigia o trajeto topo → junção). -const unsigned long TEMPO_SAIDA_ESTEIRA_MS = 3000; -bool saindoDaEsteira = false; -unsigned long tempoSaidaIniciada = 0; - -// O estoque só é debitado com a peça CONFIRMADA pelo sensor de junção, e a -// confirmação exige uma passagem: o sensor precisa ser visto livre (HIGH) depois -// que a esteira liga. Um sensor que já está em LOW na partida (peça parada sobre -// ele, desalinhamento) não confirma nada sozinho — o pedido termina em timeout. -bool juncaoLiberada = false; -bool juncaoUltima = false; // última leitura do sensor de junção -unsigned long juncaoMudouEm = 0; // quando a leitura mudou pela última vez - -// Pausa pós-entrega sem bloquear o loop (substitui o antigo delay(1500)). -bool aguardandoLimpezaEntrega = false; -unsigned long tempoLimpezaEntrega = 0; -const unsigned long PAUSA_POS_ENTREGA_MS = 1500; - -// ============================================================ -// FUNÇÕES AUXILIARES — MOTORES (IRF520) -// Com IRF520, basta controlar o PWM em 1 pino por motor. -// analogWrite(pin, velocidade) → liga motor -// analogWrite(pin, 0) → para motor -// ============================================================ - -void ligarMotor(int pin, int velocidade) { - analogWrite(pin, velocidade); -} - -void pararMotor(int pin) { - analogWrite(pin, 0); -} - -void ligarEsteiraA() { - ligarMotor(MOTOR_A, VELOCIDADE_ESTEIRA_A); -} - -void ligarEsteiraB() { - ligarMotor(MOTOR_B, VELOCIDADE_ESTEIRA_B); -} - -void ligarEsteiraC() { - ligarMotor(MOTOR_C, VELOCIDADE_ESTEIRA_C); -} - -void pararEsteiraA() { pararMotor(MOTOR_A); } -void pararEsteiraB() { pararMotor(MOTOR_B); } -void pararEsteiraC() { pararMotor(MOTOR_C); } - -void pararTodasSecundarias() { - pararEsteiraA(); - pararEsteiraB(); - pararEsteiraC(); -} - -// ============================================================ -// FUNÇÕES AUXILIARES — SENSORES -// ============================================================ -bool sensorTopoA() { return digitalRead(SENSOR_TOPO_A) == LOW; } -bool sensorTopoB() { return digitalRead(SENSOR_TOPO_B) == LOW; } -bool sensorTopoC() { return digitalRead(SENSOR_TOPO_C) == LOW; } -bool sensorJuncaoJ1() { return digitalRead(SENSOR_JUNCAO_J1) == LOW; } -bool sensorJuncaoJ2() { return digitalRead(SENSOR_JUNCAO_J2) == LOW; } -bool sensorJuncaoJ3() { return digitalRead(SENSOR_JUNCAO_J3) == LOW; } - -// ============================================================ -// FUNÇÕES AUXILIARES — LCD -// ============================================================ -void atualizarLCD(String linha1, String linha2) { - lcd.clear(); - lcd.setCursor(0, 0); - lcd.print(linha1); - lcd.setCursor(0, 1); - lcd.print(linha2); -} - -void exibirEstoque() { - String linha1 = "A:" + String(estoque[1]) + " B:" + String(estoque[2]) + " C:" + String(estoque[3]); - atualizarLCD("Estoque:", linha1); -} - -// ============================================================ -// FUNÇÕES AUXILIARES — SEPARADOR (COMENTADO) -// Implementação futura: motor de passo (28BYJ-48 + ULN2003) -// para direcionar a peça para o compartimento correto. -// -// Stepper.step() é RELATIVO (nº de passos a girar a partir de onde -// está), diferente de servo.write() que é absoluto — por isso o -// controle guarda posicaoAtualSeparador e calcula o delta a cada -// movimento, sempre pelo caminho mais curto. -// ============================================================ -/* -void moverSeparadorPara(long posicaoAlvo) { - long delta = (posicaoAlvo - posicaoAtualSeparador) % STEPS_PER_REV; - if (delta < 0) delta += STEPS_PER_REV; - if (delta > STEPS_PER_REV / 2) delta -= STEPS_PER_REV; // caminho mais curto (sentido horário ou anti-horário) - - motorSeparador.step(delta); - posicaoAtualSeparador = posicaoAlvo; -} - -void moverSeparador(char peca) { - long alvo = SEPARADOR_POSICAO_INICIAL; - if (peca == 'A') alvo = SEPARADOR_POSICAO_A; - if (peca == 'B') alvo = SEPARADOR_POSICAO_B; - if (peca == 'C') alvo = SEPARADOR_POSICAO_C; - - moverSeparadorPara(alvo); -} - -void resetarSeparador() { - moverSeparadorPara(SEPARADOR_POSICAO_INICIAL); -} -*/ - -// ============================================================ -// LEITURA DE BOTÕES — DESABILITADA -// Controle exclusivo via dashboard web (Serial/ESP32) -// Para reativar botões físicos, descomente abaixo: -// ============================================================ -/* -int lerBotao() { - if (millis() - ultimoDebounce < DEBOUNCE_BTN) return 0; - if (digitalRead(BTN_PECA_A) == LOW) { ultimoDebounce = millis(); return 1; } - if (digitalRead(BTN_PECA_B) == LOW) { ultimoDebounce = millis(); return 2; } - if (digitalRead(BTN_PECA_C) == LOW) { ultimoDebounce = millis(); return 3; } - if (digitalRead(BTN_RESET) == LOW) { ultimoDebounce = millis(); return -1; } - return 0; -} -*/ - -// ============================================================ -// PUBLICAÇÃO DE DADOS VIA SERIAL (para ESP32 → MQTT) -// IMPORTANTE: Baud rate deve ser IDÊNTICO ao do ESP32 (9600). -// O ESP32 usa Serial2.begin(9600) para receber estes dados. -// ============================================================ - -void publicarEstado() { - StaticJsonDocument<200> doc; - doc["type"] = "status"; - doc["estado"] = nomesEstados[estadoAtual]; - doc["pecaSolicitada"] = pecaSolicitada; - doc["uptime"] = millis() / 1000; - serializeJson(doc, Serial); - Serial.println(); -} - -void publicarEstoque() { - StaticJsonDocument<200> doc; - doc["type"] = "estoque"; - doc["pecaA"] = estoque[1]; - doc["pecaB"] = estoque[2]; - doc["pecaC"] = estoque[3]; - serializeJson(doc, Serial); - Serial.println(); -} - -void publicarSensores() { - StaticJsonDocument<300> doc; - doc["type"] = "sensores"; - JsonObject topo = doc.createNestedObject("topo"); - topo["A"] = sensorTopoA() ? 1 : 0; - topo["B"] = sensorTopoB() ? 1 : 0; - topo["C"] = sensorTopoC() ? 1 : 0; - JsonObject juncao = doc.createNestedObject("juncao"); - juncao["J1"] = sensorJuncaoJ1() ? 1 : 0; - juncao["J2"] = sensorJuncaoJ2() ? 1 : 0; - juncao["J3"] = sensorJuncaoJ3() ? 1 : 0; - serializeJson(doc, Serial); - Serial.println(); -} - -void publicarEntrega(char peca) { - StaticJsonDocument<250> doc; - doc["type"] = "evento"; - doc["evento"] = "entrega"; - doc["peca"] = String(peca); - doc["estoqueA"] = estoque[1]; - doc["estoqueB"] = estoque[2]; - doc["estoqueC"] = estoque[3]; - serializeJson(doc, Serial); - Serial.println(); -} - -// "peca" = letra A/B/C a que o erro se refere, ou 0 quando não se aplica -// (peca_invalida, comando_desconhecido). Antes o campo levava sempre a peça do -// pedido em curso, o que mostrava no dashboard "ocupado — Peça A" para um -// pedido de B. -void publicarErro(const char* tipo, char peca) { - StaticJsonDocument<200> doc; - doc["type"] = "evento"; - doc["evento"] = "erro"; - doc["tipo"] = tipo; - if (peca != 0) { - doc["peca"] = String(peca); - } - serializeJson(doc, Serial); - Serial.println(); -} +void setup() { + motoresIniciar(); // 1º: gates do IRF520 em 0 antes de qualquer coisa + diagnosticoIniciar(); // causa do reset + watchdog de 2 s + Serial.begin(BAUD_SERIAL); + comunicacaoIniciar(); -void publicarPedido(char peca) { - StaticJsonDocument<150> doc; - doc["type"] = "evento"; - doc["evento"] = "pedido"; - doc["peca"] = String(peca); - serializeJson(doc, Serial); - Serial.println(); -} + inicioMs = millis(); + sensoresIniciar(inicioMs); + lcdPresente = lcdIniciar(); + lcdAtualizar("Data Flow", "Inventory v" VERSAO_FIRMWARE); -// Esteira secundária realmente ligada: na pausa pós-entrega (Fase C) o motor -// já está parado, então o dashboard não deve mostrá-la como ligada. -bool esteiraLigada(int peca) { - if (pecaSolicitada != peca) return false; - return estadoAtual == ACIONANDO_ESTEIRA || - (estadoAtual == ENTREGANDO_PECA && !aguardandoLimpezaEntrega); -} + fsmIniciar(fsm, PARAMS, ESTOQUE_INICIAL); -void publicarStatusEsteiras() { - StaticJsonDocument<200> doc; - doc["type"] = "esteiras"; - doc["principal"] = 1; // sempre ligada - doc["secA"] = esteiraLigada(1) ? 1 : 0; - doc["secB"] = esteiraLigada(2) ? 1 : 0; - doc["secC"] = esteiraLigada(3) ? 1 : 0; - serializeJson(doc, Serial); - Serial.println(); + // Mesma ordem do firmware v2.x: estoque inicial e depois o evento de início. + publicarEstoque(fsm); + publicarInicio(TXT(VERSAO_FIRMWARE), diagnosticoCausaReset(), lcdPresente, ramLivre()); } -// ============================================================ -// RECEBIMENTO DE COMANDOS VIA SERIAL (do ESP32) — NÃO-BLOQUEANTE -// Acumula caracteres até '\n' e só então interpreta a linha, sem -// Serial.readStringUntil() (que bloquearia o loop se a linha viesse -// fragmentada). Espelha o lerSerial2() do gateway ESP32. -// Formato esperado: CMD:PECA:A | CMD:PECA:B | CMD:PECA:C | CMD:RESET -// Rejeições explícitas (não travam a FSM, informam o dashboard): -// peca_invalida — letra fora de A/B/C -// ocupado — pedido com a FSM fora de AGUARDANDO_PEDIDO -// comando_desconhecido — linha não reconhecida -// ============================================================ -void interpretarComando(String linha) { - linha.trim(); - if (linha.length() == 0) return; - - if (linha.startsWith("CMD:PECA:")) { - // Formato exato: "CMD:PECA:" + 1 letra (A, B ou C); qualquer outra coisa é inválida. - char pecaChar = (linha.length() == 10) ? linha.charAt(9) : 0; - int peca = 0; - if (pecaChar == 'A') peca = 1; - else if (pecaChar == 'B') peca = 2; - else if (pecaChar == 'C') peca = 3; - - if (peca == 0) { - publicarErro("peca_invalida", 0); - return; - } - if (estadoAtual != AGUARDANDO_PEDIDO) { - publicarErro("ocupado", pecaChar); - return; - } - pecaSolicitada = peca; - publicarPedido(pecaChar); - estadoAtual = VERIFICANDO_ESTOQUE; - - } else if (linha == "CMD:RESET") { - if (estadoAtual == ERRO) { - erroTimeout = false; - erroSemEstoque = false; - saindoDaEsteira = false; - juncaoLiberada = false; - aguardandoLimpezaEntrega = false; - pecaSolicitada = 0; - pararTodasSecundarias(); - estadoAtual = AGUARDANDO_PEDIDO; - exibirEstoque(); - publicarEstado(); - publicarEstoque(); // sincroniza dashboard com o LCD após reset - } - // RESET fora de ERRO: ignorado de propósito (sistema já estável) - - } else { - publicarErro("comando_desconhecido", 0); - } -} +void loop() { + const uint32_t agora = millis(); -void processarComando() { - while (Serial.available()) { - char c = (char)Serial.read(); - if (c == '\n') { - interpretarComando(bufferComando); - bufferComando = ""; - } else if (c != '\r') { - bufferComando += c; - // Proteção contra lixo / linha sem terminador na serial - if (bufferComando.length() > 80) bufferComando = ""; - } + if ((uint32_t)(agora - ultimaAmostra) >= INTERVALO_AMOSTRA_MS) { + ultimaAmostra = agora; + sensoresAmostrar(agora); } -} - -// ============================================================ -// SETUP -// ============================================================ -void setup() { - Serial.begin(9600); - // Motores primeiro: o gate do IRF520 não pode ficar flutuando durante a - // inicialização do LCD e o delay de 2 s (o motor poderia dar trancos). - // IRF520 — apenas 1 PWM por motor. - int motores[] = { MOTOR_A, MOTOR_B, MOTOR_C }; - for (int i = 0; i < 3; i++) { - pinMode(motores[i], OUTPUT); - analogWrite(motores[i], 0); // Inicia desligado + // Entradas do ciclo → FSM. + EntradasFsm in; + in.agora = agora; + for (uint8_t i = 0; i < 3; i++) { + in.topo[i] = sensorTopo(i); + in.juncao[i] = sensorJuncao(i); } + in.cmd = comunicacaoLer(&in.cmdPeca); - // Evita que um barramento I2C travado (ruído do motor) prenda o Uno inteiro. - Wire.setWireTimeout(25000, true); - - // Inicializa LCD - lcd.init(); - lcd.backlight(); - atualizarLCD("Data Flow", "Inventory v2.1"); - delay(2000); - - // Publica estoque inicial para sincronizar com o dashboard - publicarEstoque(); - - // Configura pinos dos sensores - int sensores[] = { - SENSOR_TOPO_A, SENSOR_TOPO_B, SENSOR_TOPO_C, - SENSOR_JUNCAO_J1, SENSOR_JUNCAO_J2, SENSOR_JUNCAO_J3 - }; - for (int i = 0; i < 6; i++) pinMode(sensores[i], INPUT_PULLUP); - - // Separador (roda giratória): DESABILITADO — motor de passo ainda não montado - // Para reativar, descomente (junto com o bloco de definições e funções auxiliares): - // motorSeparador.setSpeed(SEPARADOR_RPM); - // moverSeparadorPara(SEPARADOR_POSICAO_INICIAL); + SaidasFsm out; + fsmPasso(fsm, in, out); + if (out.zerarJuncao >= 0) sensorJuncaoZerar((uint8_t)out.zerarJuncao); - // Botões físicos: DESABILITADOS (controle via dashboard) - // Para reativar, descomente: - // int botoes[] = { BTN_PECA_A, BTN_PECA_B, BTN_PECA_C, BTN_RESET }; - // for (int i = 0; i < 4; i++) pinMode(botoes[i], INPUT_PULLUP); + // Motores derivados do estado: em ERRO nenhum fica ligado. + bool desejado[3]; + for (uint8_t i = 0; i < 3; i++) desejado[i] = fsmMotorLigado(fsm, i); + motoresAplicar(desejado, PARAMS, agora); - exibirEstoque(); + for (uint8_t i = 0; i < out.nEventos; i++) publicarEvento(out.eventos[i], fsm, agora); - // Publica mensagem de inicialização - StaticJsonDocument<150> doc; - doc["type"] = "evento"; - doc["evento"] = "inicio"; - doc["msg"] = "Sistema iniciado"; - doc["versao"] = "2.1"; - doc["driver"] = "IRF520"; - serializeJson(doc, Serial); - Serial.println(); -} - -// ============================================================ -// LOOP PRINCIPAL — MÁQUINA DE ESTADOS -// ============================================================ -void loop() { - - // Processa comandos recebidos do ESP32 - processarComando(); - - // Publicação periódica de status, ESCALONADA: uma mensagem por vez, a cada - // INTERVALO_PUBLICACAO / PASSOS_PUBLICACAO ms, de modo que cada mensagem - // continua saindo 1x por INTERVALO_PUBLICACAO. A Serial do Uno (9600 baud, - // buffer de 64 bytes) bloqueia o loop enquanto esvazia o buffer; enviando as - // 4 mensagens de uma vez o loop ficava preso por centenas de ms por segundo e - // um pulso curto do sensor de junção podia passar despercebido (falso timeout). - // Inclui o estoque: no trecho UART Arduino→ESP32 as mensagens são QoS 0; - // republicar a cada ciclo reconcilia o dashboard caso o pacote do boot - // ou de uma entrega se perca. - if (millis() - ultimaPublicacao >= INTERVALO_PUBLICACAO / PASSOS_PUBLICACAO) { - ultimaPublicacao = millis(); + // Publicação periódica escalonada: uma mensagem a cada 250 ms, cada uma + // 1x por segundo (evita prender o loop na serial de 9600 baud). + if ((uint32_t)(agora - ultimaPublicacao) >= INTERVALO_PUBLICACAO / PASSOS_PUBLICACAO) { + ultimaPublicacao = agora; switch (passoPublicacao) { - case 0: publicarEstado(); break; - case 1: publicarEstoque(); break; - case 2: publicarSensores(); break; - default: publicarStatusEsteiras(); break; + case 0: publicarEstado(fsm, agora); break; + case 1: publicarEstoque(fsm); break; + case 2: publicarSensores(); break; + default: publicarEsteiras(fsm); break; } - passoPublicacao = (passoPublicacao + 1) % PASSOS_PUBLICACAO; + passoPublicacao = (uint8_t)((passoPublicacao + 1) % PASSOS_PUBLICACAO); } - switch (estadoAtual) { - - // ---------------------------------------------------------- - // ESTADO 1: AGUARDANDO PEDIDO - // Comandos recebidos via Serial (ESP32/dashboard web) - // Botões físicos desabilitados (verificar reativação futura) - // ---------------------------------------------------------- - case AGUARDANDO_PEDIDO: { - // Botões físicos: DESABILITADOS - // Para reativar, descomente o bloco abaixo: - // int btn = lerBotao(); - // if (btn > 0) { - // pecaSolicitada = btn; - // publicarPedido((char)('A' + btn - 1)); - // estadoAtual = VERIFICANDO_ESTOQUE; - // } else if (btn == -1) { - // // Reset via botão físico — já processado em processarComando() - // } - break; - } - - // ---------------------------------------------------------- - // ESTADO 2: VERIFICANDO ESTOQUE - // ---------------------------------------------------------- - case VERIFICANDO_ESTOQUE: { - bool temPeca = false; - String nomePeca = ""; - - if (pecaSolicitada == 1) { temPeca = sensorTopoA(); nomePeca = "Peca A"; } - if (pecaSolicitada == 2) { temPeca = sensorTopoB(); nomePeca = "Peca B"; } - if (pecaSolicitada == 3) { temPeca = sensorTopoC(); nomePeca = "Peca C"; } - - if (temPeca && estoque[pecaSolicitada] > 0) { - atualizarLCD("Separando:", nomePeca); - estadoAtual = ACIONANDO_ESTEIRA; - publicarEstado(); - } else { - atualizarLCD("ERRO:Sem estoque", nomePeca); // 16 colunas - erroSemEstoque = true; - estadoAtual = ERRO; - publicarErro("sem_estoque", (char)('A' + pecaSolicitada - 1)); - publicarEstado(); - } - break; - } - - // ---------------------------------------------------------- - // ESTADO 3: ACIONANDO ESTEIRA - // ---------------------------------------------------------- - case ACIONANDO_ESTEIRA: { - if (pecaSolicitada == 1) ligarEsteiraA(); - if (pecaSolicitada == 2) ligarEsteiraB(); - if (pecaSolicitada == 3) ligarEsteiraC(); - - tempoInicio = millis(); - juncaoLiberada = false; - juncaoUltima = false; - juncaoMudouEm = millis(); - publicarStatusEsteiras(); - estadoAtual = ENTREGANDO_PECA; - break; - } - - // ---------------------------------------------------------- - // ESTADO 4: ENTREGANDO PEÇA - // ---------------------------------------------------------- - case ENTREGANDO_PECA: { - // --- Fase C: pausa pós-entrega (LCD "Entrega OK!"), motor já parado. - // Não-bloqueante: processarComando() e a publicação periódica - // seguem rodando durante a espera. - if (aguardandoLimpezaEntrega) { - if (millis() - tempoLimpezaEntrega >= PAUSA_POS_ENTREGA_MS) { - aguardandoLimpezaEntrega = false; - pecaSolicitada = 0; - estadoAtual = AGUARDANDO_PEDIDO; - exibirEstoque(); - publicarEstado(); - publicarStatusEsteiras(); - } - break; - } - - // --- Fase B: peça já passou pelo sensor de junção. Mantém a esteira - // ligada por TEMPO_SAIDA_ESTEIRA_MS para a peça SAIR da esteira; - // só então para o motor e conclui a entrega. - if (saindoDaEsteira) { - if (millis() - tempoSaidaIniciada >= TEMPO_SAIDA_ESTEIRA_MS) { - saindoDaEsteira = false; - - if (pecaSolicitada == 1) pararEsteiraA(); - if (pecaSolicitada == 2) pararEsteiraB(); - if (pecaSolicitada == 3) pararEsteiraC(); - - // Separador: CÓDIGO COMENTADO — verificar implementação futura - // moverSeparador((char)('A' + pecaSolicitada - 1)); - - estoque[pecaSolicitada]--; - - String msg = "Entregue! "; - msg += (char)('A' + pecaSolicitada - 1); - msg += ":" + String(estoque[pecaSolicitada]); - atualizarLCD("Entrega OK!", msg); - - publicarEntrega((char)('A' + pecaSolicitada - 1)); - publicarEstoque(); - - aguardandoLimpezaEntrega = true; - tempoLimpezaEntrega = millis(); - } - break; - } - - // --- Fase A: esteira ligada, aguardando a peça chegar ao sensor. - bool pecaChegou = false; - - if (pecaSolicitada == 1) pecaChegou = sensorJuncaoJ1(); - if (pecaSolicitada == 2) pecaChegou = sensorJuncaoJ2(); - if (pecaSolicitada == 3) pecaChegou = sensorJuncaoJ3(); - - // Filtro: a leitura precisa ficar estável por FILTRO_JUNCAO_MS antes de valer - // (ruído do PWM / comparador do TCRT5000 não confirma passagem falsa). - if (pecaChegou != juncaoUltima) { - juncaoUltima = pecaChegou; - juncaoMudouEm = millis(); - } - bool juncaoEstavel = (millis() - juncaoMudouEm) >= FILTRO_JUNCAO_MS; - - // A confirmação exige passagem: primeiro o sensor precisa ser visto livre. - if (juncaoEstavel && !pecaChegou) juncaoLiberada = true; - - if (juncaoEstavel && pecaChegou && juncaoLiberada) { - // NÃO para o motor ainda — inicia a fase de saída da esteira. - atualizarLCD("Saindo da", "esteira..."); - saindoDaEsteira = true; - tempoSaidaIniciada = millis(); - - } else if (millis() - tempoInicio > TIMEOUT_ENTREGA) { - // Timeout — peça não foi confirmada pelo sensor de junção: motor desligado, - // estoque intacto (o débito só ocorre após a confirmação, na Fase B). - pararTodasSecundarias(); - erroTimeout = true; - atualizarLCD("ERRO: Timeout", "Aguarde comando"); - estadoAtual = ERRO; - publicarErro("timeout", (char)('A' + pecaSolicitada - 1)); - publicarEstado(); - publicarStatusEsteiras(); - } - break; - } - - // ---------------------------------------------------------- - // ESTADO 5: ERRO - // ---------------------------------------------------------- - case ERRO: { - // A mensagem do LCD é desenhada UMA vez, na transição para ERRO (timeout ou - // sem estoque). Redesenhar a cada volta do loop (lcd.clear() + Strings) - // fazia o display piscar e fragmentava o heap do Uno (2 KB de RAM). - - // Reset via botão físico: DESABILITADO - // Para reativar, descomente: - // int btn = lerBotao(); - // if (btn == -1) { - // erroTimeout = false; - // erroSemEstoque = false; - // pecaSolicitada = 0; - // pararTodasSecundarias(); - // exibirEstoque(); - // publicarEstado(); - // publicarStatusEsteiras(); - // estadoAtual = AGUARDANDO_PEDIDO; - // } - break; - } + // LCD: depois da tela de abertura, mostra a tela da FSM (redesenha só se mudou). + if (lcdPresente && (uint32_t)(agora - inicioMs) >= TELA_ABERTURA_MS) { + char l1[LCD_COLUNAS + 1], l2[LCD_COLUNAS + 1]; + textoTela(fsm, l1, l2); + lcdAtualizar(l1, l2); } -} \ No newline at end of file + + watchdogAlimentar(); +} diff --git a/arduino/data_flow_inventory/src/hw/comunicacao.cpp b/arduino/data_flow_inventory/src/hw/comunicacao.cpp new file mode 100644 index 0000000..c46a81f --- /dev/null +++ b/arduino/data_flow_inventory/src/hw/comunicacao.cpp @@ -0,0 +1,81 @@ +#include "comunicacao.h" + +#include + +#include "../../config.h" +#include "../logica/protocolo.h" +#include "diagnostico.h" +#include "sensores.h" + +namespace { +LeitorLinha leitor; +char buf[160]; // buffer único de formatação (maior mensagem ~140 B) + +void enviar(size_t n) { + if (n == 0) return; // não coube: não envia linha quebrada + Serial.write(reinterpret_cast(buf), n); + Serial.write('\r'); + Serial.write('\n'); +} +} // namespace + +void comunicacaoIniciar() { leitorIniciar(leitor); } + +ComandoTipo comunicacaoLer(char* peca) { + *peca = 0; + while (Serial.available() > 0) { + if (leitorAlimentar(leitor, (char)Serial.read())) { + ComandoTipo c = interpretarComando(leitor.buf, peca); + if (c != CMD_NENHUM) return c; + } + } + return CMD_NENHUM; +} + +void publicarEstado(const Fsm& f, uint32_t agora) { + enviar(formatarStatus(buf, sizeof buf, nomeEstado(f.estado), f.peca, agora / 1000UL, ramLivre())); +} + +void publicarEstoque(const Fsm& f) { enviar(formatarEstoque(buf, sizeof buf, f.estoque)); } + +void publicarSensores() { + bool topo[3], jun[3]; + for (uint8_t i = 0; i < 3; i++) { + topo[i] = sensorTopo(i); + jun[i] = sensorJuncaoNivel(i); + } + enviar(formatarSensores(buf, sizeof buf, topo, jun)); +} + +void publicarEsteiras(const Fsm& f) { + bool sec[3]; + for (uint8_t i = 0; i < 3; i++) sec[i] = fsmMotorLigado(f, i); + enviar(formatarEsteiras(buf, sizeof buf, sec)); +} + +void publicarInicio(const char* versao, const char* reset, bool lcd, long ram) { + enviar(formatarInicio(buf, sizeof buf, versao, reset, lcd ? 1 : 0, ram)); +} + +void publicarEvento(const Evento& e, const Fsm& f, uint32_t agora) { + switch (e.tipo) { + case EV_PEDIDO: + enviar(formatarPedido(buf, sizeof buf, e.peca)); + break; + case EV_ENTREGA: + enviar(formatarEntrega(buf, sizeof buf, e.peca, f.estoque)); + break; + case EV_ERRO: + enviar(formatarErro(buf, sizeof buf, nomeErro(e.erro), e.peca, nomeFaseErro(e.fase), e.tMs, e.bordas)); + break; + case EV_ESTADO: + publicarEstado(f, agora); + break; + case EV_ESTOQUE: + publicarEstoque(f); + break; + case EV_ESTEIRAS: + publicarEsteiras(f); + break; + } +} diff --git a/arduino/data_flow_inventory/src/hw/comunicacao.h b/arduino/data_flow_inventory/src/hw/comunicacao.h new file mode 100644 index 0000000..cf179e3 --- /dev/null +++ b/arduino/data_flow_inventory/src/hw/comunicacao.h @@ -0,0 +1,25 @@ +// ============================================================ +// DATA FLOW INVENTORY — Serial Uno ↔ ESP32 (hardware) +// ------------------------------------------------------------ +// Lê comandos sem bloquear e publica as mensagens JSON (formatadas por +// src/logica/protocolo, idênticas ao firmware v2.x + campos opcionais). +// Cada mensagem é uma linha terminada em "\r\n" (como Serial.println). +// ============================================================ +#pragma once +#include + +#include "../logica/fsm.h" + +void comunicacaoIniciar(); + +// Consome bytes disponíveis até completar UMA linha; devolve o comando +// (CMD_NENHUM se ainda não há linha completa). Bytes restantes ficam +// para o próximo ciclo. +ComandoTipo comunicacaoLer(char* peca); + +void publicarEvento(const Evento& e, const Fsm& f, uint32_t agora); +void publicarEstado(const Fsm& f, uint32_t agora); +void publicarEstoque(const Fsm& f); +void publicarSensores(); +void publicarEsteiras(const Fsm& f); +void publicarInicio(const char* versao, const char* reset, bool lcd, long ramLivre); diff --git a/arduino/data_flow_inventory/src/hw/diagnostico.cpp b/arduino/data_flow_inventory/src/hw/diagnostico.cpp new file mode 100644 index 0000000..07072f5 --- /dev/null +++ b/arduino/data_flow_inventory/src/hw/diagnostico.cpp @@ -0,0 +1,68 @@ +#include "diagnostico.h" + +#include +#include + +#include "../logica/causa_reset.h" + +namespace { +const uint32_t ASSINATURA = 0xDF1A3B5CUL; +const uint8_t MARCA_WATCHDOG = 0xA5; +const char* causa = "energia"; +} // namespace + +// Variáveis que sobrevivem a resets com energia (não são zeradas no boot). +uint8_t dfiMcusrNoBoot __attribute__((section(".noinit"))); +uint32_t dfiAssinatura __attribute__((section(".noinit"))); +volatile uint8_t dfiMarcaWatchdog __attribute__((section(".noinit"))); + +// Roda antes de main(): guarda o MCUSR e desliga o watchdog (padrão avr-libc). +extern "C" void dfiCapturarMcusr(void) __attribute__((naked, used, section(".init3"))); +extern "C" void dfiCapturarMcusr(void) { + dfiMcusrNoBoot = MCUSR; + MCUSR = 0; + wdt_disable(); +} + +ISR(WDT_vect) { + // O loop não alimentou o watchdog por 2 s: marca e deixa o próximo + // estouro reiniciar o Uno. + dfiMarcaWatchdog = MARCA_WATCHDOG; +} + +static void armarWatchdog() { + noInterrupts(); + wdt_reset(); + WDTCSR = _BV(WDCE) | _BV(WDE); + WDTCSR = _BV(WDIE) | _BV(WDE) | _BV(WDP2) | _BV(WDP1) | _BV(WDP0); // 2 s + interrupts(); +} + +void diagnosticoIniciar() { + const bool assinaturaValida = dfiAssinatura == ASSINATURA; + const bool marcaWd = dfiMarcaWatchdog == MARCA_WATCHDOG; + causa = causaReset(dfiMcusrNoBoot, assinaturaValida, marcaWd); + dfiAssinatura = ASSINATURA; + dfiMarcaWatchdog = 0; + armarWatchdog(); +} + +const char* diagnosticoCausaReset() { return causa; } + +extern int __heap_start; +extern int* __brkval; + +long ramLivre() { + int v; + return (long)((int)&v - (__brkval == 0 ? (int)&__heap_start : (int)__brkval)); +} + +void watchdogAlimentar() { + wdt_reset(); + if (dfiMarcaWatchdog == MARCA_WATCHDOG) { + // O loop voltou depois de um aviso do watchdog: limpa a marca e + // rearma o modo interrupção + reset. + dfiMarcaWatchdog = 0; + armarWatchdog(); + } +} diff --git a/arduino/data_flow_inventory/src/hw/diagnostico.h b/arduino/data_flow_inventory/src/hw/diagnostico.h new file mode 100644 index 0000000..a4c8bda --- /dev/null +++ b/arduino/data_flow_inventory/src/hw/diagnostico.h @@ -0,0 +1,15 @@ +// ============================================================ +// DATA FLOW INVENTORY — Diagnóstico: causa do reset, RAM, watchdog +// ------------------------------------------------------------ +// O watchdog (2 s, modo interrupção + reset) reinicia o Uno se o loop +// travar; a interrupção grava a marca "watchdog" em RAM .noinit antes +// do reset. O watchdog é desligado logo no boot (.init3), senão um +// reset por watchdog deixaria o Uno reiniciando em laço. +// ============================================================ +#pragma once +#include + +void diagnosticoIniciar(); // calcula a causa do reset e arma o watchdog +const char* diagnosticoCausaReset(); // "energia", "reinicio", "watchdog" ou "brownout" +long ramLivre(); // bytes entre o heap e a pilha +void watchdogAlimentar(); // chamar uma vez por ciclo do loop diff --git a/arduino/data_flow_inventory/src/hw/lcd.cpp b/arduino/data_flow_inventory/src/hw/lcd.cpp new file mode 100644 index 0000000..3289eda --- /dev/null +++ b/arduino/data_flow_inventory/src/hw/lcd.cpp @@ -0,0 +1,57 @@ +#include "lcd.h" + +#include +#include +#include +#include + +#include "../../config.h" + +namespace { +LiquidCrystal_I2C lcd1(LCD_ENDERECO_1, 16, 2); +LiquidCrystal_I2C lcd2(LCD_ENDERECO_2, 16, 2); +LiquidCrystal_I2C* ativo = nullptr; +char linha1[17] = ""; +char linha2[17] = ""; + +bool responde(uint8_t endereco) { + Wire.beginTransmission(endereco); + return Wire.endTransmission() == 0; +} + +void escreverLinha(uint8_t linha, const char* texto) { + ativo->setCursor(0, linha); + uint8_t i = 0; + for (; texto[i] && i < 16; i++) ativo->write(texto[i]); + for (; i < 16; i++) ativo->write(' '); +} +} // namespace + +bool lcdIniciar() { + Wire.begin(); + // Um barramento I2C preso (ruído do motor) não pode travar o Uno. + Wire.setWireTimeout(25000, true); + if (responde(LCD_ENDERECO_1)) ativo = &lcd1; + else if (responde(LCD_ENDERECO_2)) ativo = &lcd2; + if (ativo == nullptr) return false; + ativo->init(); + ativo->backlight(); + ativo->clear(); + linha1[0] = 0; + linha2[0] = 0; + return true; +} + +void lcdAtualizar(const char* l1, const char* l2) { + if (ativo == nullptr) return; + if (strncmp(l1, linha1, 16) != 0) { + escreverLinha(0, l1); + strncpy(linha1, l1, 16); + linha1[16] = 0; + } + if (strncmp(l2, linha2, 16) != 0) { + escreverLinha(1, l2); + strncpy(linha2, l2, 16); + linha2[16] = 0; + } +} diff --git a/arduino/data_flow_inventory/src/hw/lcd.h b/arduino/data_flow_inventory/src/hw/lcd.h new file mode 100644 index 0000000..d119998 --- /dev/null +++ b/arduino/data_flow_inventory/src/hw/lcd.h @@ -0,0 +1,12 @@ +// ============================================================ +// DATA FLOW INVENTORY — LCD I2C 16x2 (hardware) +// ------------------------------------------------------------ +// Procura o módulo em LCD_ENDERECO_1 e LCD_ENDERECO_2. Se nenhum +// responder, o sistema segue SEM LCD (nada trava). Redesenha só quando +// o texto muda, sobrescrevendo com espaços (sem lcd.clear(), sem piscar). +// ============================================================ +#pragma once +#include + +bool lcdIniciar(); // true se encontrou o LCD +void lcdAtualizar(const char* l1, const char* l2); // no-op sem LCD diff --git a/arduino/data_flow_inventory/src/hw/motores.cpp b/arduino/data_flow_inventory/src/hw/motores.cpp new file mode 100644 index 0000000..e60863a --- /dev/null +++ b/arduino/data_flow_inventory/src/hw/motores.cpp @@ -0,0 +1,54 @@ +#include "motores.h" + +#include + +#include "../../config.h" + +namespace { +const uint8_t PINOS[3] = {MOTOR_A, MOTOR_B, MOTOR_C}; + +struct EstadoMotor { + bool ligado; + bool emRegime; + uint32_t ligouEm; +}; +EstadoMotor motor[3]; +} // namespace + +void motoresIniciar() { + for (uint8_t i = 0; i < 3; i++) { + pinMode(PINOS[i], OUTPUT); + analogWrite(PINOS[i], 0); + motor[i].ligado = false; + motor[i].emRegime = false; + motor[i].ligouEm = 0; + } +} + +void motoresAplicar(const bool desejado[3], const ParamEsteira params[3], uint32_t agora) { + for (uint8_t i = 0; i < 3; i++) { + EstadoMotor& m = motor[i]; + if (!desejado[i]) { + if (m.ligado) { + analogWrite(PINOS[i], 0); + m.ligado = false; + m.emRegime = false; + } + continue; + } + if (!m.ligado) { + m.ligado = true; + m.ligouEm = agora; + if (params[i].kickMs > 0) { + analogWrite(PINOS[i], 255); // kick-start + m.emRegime = false; + } else { + analogWrite(PINOS[i], params[i].pwmRegime); + m.emRegime = true; + } + } else if (!m.emRegime && (uint32_t)(agora - m.ligouEm) >= params[i].kickMs) { + analogWrite(PINOS[i], params[i].pwmRegime); + m.emRegime = true; + } + } +} diff --git a/arduino/data_flow_inventory/src/hw/motores.h b/arduino/data_flow_inventory/src/hw/motores.h new file mode 100644 index 0000000..ab5e918 --- /dev/null +++ b/arduino/data_flow_inventory/src/hw/motores.h @@ -0,0 +1,18 @@ +// ============================================================ +// DATA FLOW INVENTORY — Motores das esteiras secundárias (hardware) +// ------------------------------------------------------------ +// IRF520: 1 pino PWM por motor. Partida com kick-start (PWM 255 por +// kickMs) e depois o PWM de regime da esteira. O estado desejado vem +// da FSM a cada ciclo; o que não está desejado fica em 0. +// ============================================================ +#pragma once +#include + +#include "../logica/supervisor_entrega.h" + +// Configura os 3 pinos como saída em 0. Chamar ANTES de qualquer outra +// inicialização (o gate do IRF520 não pode ficar flutuando). +void motoresIniciar(); + +// desejado[i] = esteira i deve estar ligada agora. +void motoresAplicar(const bool desejado[3], const ParamEsteira params[3], uint32_t agora); diff --git a/arduino/data_flow_inventory/src/hw/sensores.cpp b/arduino/data_flow_inventory/src/hw/sensores.cpp new file mode 100644 index 0000000..640dea0 --- /dev/null +++ b/arduino/data_flow_inventory/src/hw/sensores.cpp @@ -0,0 +1,101 @@ +#include "sensores.h" + +#include + +#include "../../config.h" +#include "../logica/filtro_entrada.h" + +namespace { +const uint8_t PINOS_TOPO[3] = {SENSOR_TOPO_A, SENSOR_TOPO_B, SENSOR_TOPO_C}; +FiltroEntrada topo[3]; + +struct CapturaJuncao { + volatile bool ocupada; + volatile uint32_t ocupouEm; + volatile uint16_t maiorPulsoMs; + volatile uint8_t bordas; +}; +CapturaJuncao juncao[3]; + +// Leitura direta das portas (rápida, dentro da ISR). LOW = ocupada. +inline bool lerJ1() { return (PINC & _BV(PC3)) == 0; } // A3 +inline bool lerJ2() { return (PIND & _BV(PD2)) == 0; } // D2 +inline bool lerJ3() { return (PIND & _BV(PD4)) == 0; } // D4 + +// Chamada só com interrupções desabilitadas (dentro da ISR). +inline void registrar(uint8_t i, bool ocupada, uint32_t agora) { + CapturaJuncao& c = juncao[i]; + if (ocupada == c.ocupada) return; + c.ocupada = ocupada; + if (c.bordas < 255) c.bordas = c.bordas + 1; + if (ocupada) { + c.ocupouEm = agora; + } else { + uint32_t largura = agora - c.ocupouEm; + uint16_t l = largura > 65535UL ? 65535 : (uint16_t)largura; + if (l > c.maiorPulsoMs) c.maiorPulsoMs = l; + } +} +} // namespace + +ISR(PCINT1_vect) { // porta C: A3 (J1) + registrar(0, lerJ1(), millis()); +} + +ISR(PCINT2_vect) { // porta D: D2 (J2) e D4 (J3) + const uint32_t agora = millis(); + registrar(1, lerJ2(), agora); + registrar(2, lerJ3(), agora); +} + +void sensoresIniciar(uint32_t agora) { + for (uint8_t i = 0; i < 3; i++) { + pinMode(PINOS_TOPO[i], INPUT_PULLUP); + filtroIniciar(topo[i], digitalRead(PINOS_TOPO[i]) == LOW, agora, FILTRO_TOPO_MS); + } + pinMode(SENSOR_JUNCAO_J1, INPUT_PULLUP); + pinMode(SENSOR_JUNCAO_J2, INPUT_PULLUP); + pinMode(SENSOR_JUNCAO_J3, INPUT_PULLUP); + + noInterrupts(); + const bool inicial[3] = {lerJ1(), lerJ2(), lerJ3()}; + for (uint8_t i = 0; i < 3; i++) { + juncao[i].ocupada = inicial[i]; + juncao[i].ocupouEm = agora; + juncao[i].maiorPulsoMs = 0; + juncao[i].bordas = 0; + } + PCMSK1 |= _BV(PCINT11); // A3 + PCMSK2 |= _BV(PCINT18) | _BV(PCINT20); // D2, D4 + PCIFR = _BV(PCIF1) | _BV(PCIF2); // descarta pendências antigas + PCICR |= _BV(PCIE1) | _BV(PCIE2); + interrupts(); +} + +void sensoresAmostrar(uint32_t agora) { + for (uint8_t i = 0; i < 3; i++) { + filtroAtualizar(topo[i], digitalRead(PINOS_TOPO[i]) == LOW, agora); + } +} + +bool sensorTopo(uint8_t i) { return topo[i].estavel; } + +bool sensorJuncaoNivel(uint8_t i) { return juncao[i].ocupada; } + +LeituraJuncao sensorJuncao(uint8_t i) { + LeituraJuncao l; + noInterrupts(); + l.ocupada = juncao[i].ocupada; + l.ocupouEm = juncao[i].ocupouEm; + l.maiorPulsoMs = juncao[i].maiorPulsoMs; + l.bordas = juncao[i].bordas; + interrupts(); + return l; +} + +void sensorJuncaoZerar(uint8_t i) { + noInterrupts(); + juncao[i].maiorPulsoMs = 0; + juncao[i].bordas = 0; + interrupts(); +} diff --git a/arduino/data_flow_inventory/src/hw/sensores.h b/arduino/data_flow_inventory/src/hw/sensores.h new file mode 100644 index 0000000..d880272 --- /dev/null +++ b/arduino/data_flow_inventory/src/hw/sensores.h @@ -0,0 +1,20 @@ +// ============================================================ +// DATA FLOW INVENTORY — Sensores TCRT5000 (hardware) +// ------------------------------------------------------------ +// Topo (A0–A2): amostrados a cada INTERVALO_AMOSTRA_MS e filtrados. +// Junção (J1=A3, J2=D2, J3=D4): mudanças capturadas por interrupção de +// mudança de pino (PCINT), para que um pulso curto não se perca quando +// o loop está ocupado (serial, LCD). LOW no pino = peça detectada. +// ============================================================ +#pragma once +#include + +#include "../logica/supervisor_entrega.h" + +void sensoresIniciar(uint32_t agora); +void sensoresAmostrar(uint32_t agora); + +bool sensorTopo(uint8_t i); // filtrado (true = peça presente) +bool sensorJuncaoNivel(uint8_t i); // nível atual da junção +LeituraJuncao sensorJuncao(uint8_t i); // cópia atômica da captura +void sensorJuncaoZerar(uint8_t i); // zera pulsos/bordas (início da entrega) From a549e2f8a204932595369146e76ec98f78ab3e0f Mon Sep 17 00:00:00 2001 From: Matheus Nespolo Date: Sat, 3 Oct 2026 19:33:33 -0300 Subject: [PATCH 11/21] fix(firmware): LCD com I2C preso desliga o LCD, watchdog para os motores, saida segura a cada ciclo Co-Authored-By: Claude Opus 5.5 --- .../data_flow_inventory.ino | 4 +- .../src/hw/diagnostico.cpp | 4 +- arduino/data_flow_inventory/src/hw/lcd.cpp | 43 ++++++++++++++++--- .../data_flow_inventory/src/hw/motores.cpp | 27 +++++++++--- arduino/data_flow_inventory/src/hw/motores.h | 4 ++ .../data_flow_inventory/src/hw/sensores.cpp | 8 ++-- 6 files changed, 75 insertions(+), 15 deletions(-) diff --git a/arduino/data_flow_inventory/data_flow_inventory.ino b/arduino/data_flow_inventory/data_flow_inventory.ino index 869cb50..402b203 100644 --- a/arduino/data_flow_inventory/data_flow_inventory.ino +++ b/arduino/data_flow_inventory/data_flow_inventory.ino @@ -40,6 +40,7 @@ static const ParamEsteira PARAMS[3] = {PARAM_ESTEIRA_A, PARAM_ESTEIRA_B, PARAM_E static Fsm fsm; static bool lcdPresente = false; static uint32_t inicioMs = 0; +static uint32_t aberturaMs = 0; static uint32_t ultimaAmostra = 0; static uint32_t ultimaPublicacao = 0; static uint8_t passoPublicacao = 0; @@ -54,6 +55,7 @@ void setup() { sensoresIniciar(inicioMs); lcdPresente = lcdIniciar(); lcdAtualizar("Data Flow", "Inventory v" VERSAO_FIRMWARE); + aberturaMs = millis(); // lcdIniciar leva ~1 s: a abertura conta daqui fsmIniciar(fsm, PARAMS, ESTOQUE_INICIAL); @@ -104,7 +106,7 @@ void loop() { } // LCD: depois da tela de abertura, mostra a tela da FSM (redesenha só se mudou). - if (lcdPresente && (uint32_t)(agora - inicioMs) >= TELA_ABERTURA_MS) { + if (lcdPresente && (uint32_t)(agora - aberturaMs) >= TELA_ABERTURA_MS) { char l1[LCD_COLUNAS + 1], l2[LCD_COLUNAS + 1]; textoTela(fsm, l1, l2); lcdAtualizar(l1, l2); diff --git a/arduino/data_flow_inventory/src/hw/diagnostico.cpp b/arduino/data_flow_inventory/src/hw/diagnostico.cpp index 07072f5..d5f445e 100644 --- a/arduino/data_flow_inventory/src/hw/diagnostico.cpp +++ b/arduino/data_flow_inventory/src/hw/diagnostico.cpp @@ -4,11 +4,12 @@ #include #include "../logica/causa_reset.h" +#include "motores.h" namespace { const uint32_t ASSINATURA = 0xDF1A3B5CUL; const uint8_t MARCA_WATCHDOG = 0xA5; -const char* causa = "energia"; +const char* causa = nullptr; // diagnosticoIniciar sempre preenche antes do uso } // namespace // Variáveis que sobrevivem a resets com energia (não são zeradas no boot). @@ -28,6 +29,7 @@ ISR(WDT_vect) { // O loop não alimentou o watchdog por 2 s: marca e deixa o próximo // estouro reiniciar o Uno. dfiMarcaWatchdog = MARCA_WATCHDOG; + motoresPararNaInterrupcao(); // não deixa o motor ligado até o reset } static void armarWatchdog() { diff --git a/arduino/data_flow_inventory/src/hw/lcd.cpp b/arduino/data_flow_inventory/src/hw/lcd.cpp index 3289eda..305e495 100644 --- a/arduino/data_flow_inventory/src/hw/lcd.cpp +++ b/arduino/data_flow_inventory/src/hw/lcd.cpp @@ -19,11 +19,32 @@ bool responde(uint8_t endereco) { return Wire.endTransmission() == 0; } -void escreverLinha(uint8_t linha, const char* texto) { +// Devolve false se o I2C estourou o timeout (barramento preso). Cada +// transação presa custa 25 ms; sem abortar no primeiro timeout, uma linha +// inteira bloquearia o loop por segundos e o watchdog reiniciaria o Uno +// com o motor ligado. +bool escreverLinha(uint8_t linha, const char* texto) { ativo->setCursor(0, linha); + if (Wire.getWireTimeoutFlag()) { + Wire.clearWireTimeoutFlag(); + return false; + } uint8_t i = 0; - for (; texto[i] && i < 16; i++) ativo->write(texto[i]); - for (; i < 16; i++) ativo->write(' '); + for (; texto[i] && i < 16; i++) { + ativo->write(texto[i]); + if (Wire.getWireTimeoutFlag()) { + Wire.clearWireTimeoutFlag(); + return false; + } + } + for (; i < 16; i++) { + ativo->write(' '); + if (Wire.getWireTimeoutFlag()) { + Wire.clearWireTimeoutFlag(); + return false; + } + } + return true; } } // namespace @@ -37,6 +58,12 @@ bool lcdIniciar() { ativo->init(); ativo->backlight(); ativo->clear(); + // Barramento preso já no boot: segue sem LCD, como se ele não existisse. + if (Wire.getWireTimeoutFlag()) { + Wire.clearWireTimeoutFlag(); + ativo = nullptr; + return false; + } linha1[0] = 0; linha2[0] = 0; return true; @@ -45,12 +72,18 @@ bool lcdIniciar() { void lcdAtualizar(const char* l1, const char* l2) { if (ativo == nullptr) return; if (strncmp(l1, linha1, 16) != 0) { - escreverLinha(0, l1); + if (!escreverLinha(0, l1)) { + ativo = nullptr; // I2C preso: desiste do LCD para não travar o loop + return; + } strncpy(linha1, l1, 16); linha1[16] = 0; } if (strncmp(l2, linha2, 16) != 0) { - escreverLinha(1, l2); + if (!escreverLinha(1, l2)) { + ativo = nullptr; + return; + } strncpy(linha2, l2, 16); linha2[16] = 0; } diff --git a/arduino/data_flow_inventory/src/hw/motores.cpp b/arduino/data_flow_inventory/src/hw/motores.cpp index e60863a..bdb50de 100644 --- a/arduino/data_flow_inventory/src/hw/motores.cpp +++ b/arduino/data_flow_inventory/src/hw/motores.cpp @@ -13,6 +13,7 @@ struct EstadoMotor { uint32_t ligouEm; }; EstadoMotor motor[3]; +volatile bool paradosPorEmergencia = false; } // namespace void motoresIniciar() { @@ -25,15 +26,31 @@ void motoresIniciar() { } } +void motoresPararNaInterrupcao() { + TCCR1A &= ~(_BV(COM1A1) | _BV(COM1B1)); // pinos 9 e 10 + TCCR2A &= ~_BV(COM2A1); // pino 11 + PORTB &= ~(_BV(PB1) | _BV(PB2) | _BV(PB3)); + paradosPorEmergencia = true; +} + void motoresAplicar(const bool desejado[3], const ParamEsteira params[3], uint32_t agora) { + if (paradosPorEmergencia) { + // O watchdog cortou as saídas: o estado interno não vale mais, então + // um motor desejado volta a partir com kick. + noInterrupts(); + paradosPorEmergencia = false; + interrupts(); + for (uint8_t i = 0; i < 3; i++) { + motor[i].ligado = false; + motor[i].emRegime = false; + } + } for (uint8_t i = 0; i < 3; i++) { EstadoMotor& m = motor[i]; if (!desejado[i]) { - if (m.ligado) { - analogWrite(PINOS[i], 0); - m.ligado = false; - m.emRegime = false; - } + analogWrite(PINOS[i], 0); // saída segura a cada ciclo + m.ligado = false; + m.emRegime = false; continue; } if (!m.ligado) { diff --git a/arduino/data_flow_inventory/src/hw/motores.h b/arduino/data_flow_inventory/src/hw/motores.h index ab5e918..62f1780 100644 --- a/arduino/data_flow_inventory/src/hw/motores.h +++ b/arduino/data_flow_inventory/src/hw/motores.h @@ -14,5 +14,9 @@ // inicialização (o gate do IRF520 não pode ficar flutuando). void motoresIniciar(); +// Segura para chamar de dentro de ISR (watchdog): desliga as saídas PWM e +// os pinos direto nos registradores. A próxima motoresAplicar religa com kick. +void motoresPararNaInterrupcao(); + // desejado[i] = esteira i deve estar ligada agora. void motoresAplicar(const bool desejado[3], const ParamEsteira params[3], uint32_t agora); diff --git a/arduino/data_flow_inventory/src/hw/sensores.cpp b/arduino/data_flow_inventory/src/hw/sensores.cpp index 640dea0..f0bf9da 100644 --- a/arduino/data_flow_inventory/src/hw/sensores.cpp +++ b/arduino/data_flow_inventory/src/hw/sensores.cpp @@ -58,6 +58,11 @@ void sensoresIniciar(uint32_t agora) { pinMode(SENSOR_JUNCAO_J3, INPUT_PULLUP); noInterrupts(); + // Máscaras e flags primeiro; a leitura inicial depois: uma borda após a + // leitura deixa a flag pendente e a ISR ressincroniza. + PCMSK1 |= _BV(PCINT11); // A3 + PCMSK2 |= _BV(PCINT18) | _BV(PCINT20); // D2, D4 + PCIFR = _BV(PCIF1) | _BV(PCIF2); // descarta pendências antigas const bool inicial[3] = {lerJ1(), lerJ2(), lerJ3()}; for (uint8_t i = 0; i < 3; i++) { juncao[i].ocupada = inicial[i]; @@ -65,9 +70,6 @@ void sensoresIniciar(uint32_t agora) { juncao[i].maiorPulsoMs = 0; juncao[i].bordas = 0; } - PCMSK1 |= _BV(PCINT11); // A3 - PCMSK2 |= _BV(PCINT18) | _BV(PCINT20); // D2, D4 - PCIFR = _BV(PCIF1) | _BV(PCIF2); // descarta pendências antigas PCICR |= _BV(PCIE1) | _BV(PCIE2); interrupts(); } From 283361cb0ee606b168f972e1c894d78de67305aa Mon Sep 17 00:00:00 2001 From: Matheus Nespolo Date: Sat, 3 Oct 2026 19:37:46 -0300 Subject: [PATCH 12/21] docs(firmware): marcos, novos erros, campos opcionais, CI e roteiro de bancada da v3.0 Co-Authored-By: Claude Opus 5.5 --- README.md | 6 +- docs/ARCHITECTURE.md | 53 ++++++++++++--- docs/CHANGELOG.md | 6 ++ docs/CI-CD.md | 7 +- .../roteiros/firmware_uno_v3_validacao.md | 65 +++++++++++++++++++ 5 files changed, 124 insertions(+), 13 deletions(-) create mode 100644 docs/testes/roteiros/firmware_uno_v3_validacao.md diff --git a/README.md b/README.md index d894fb4..5c6d830 100644 --- a/README.md +++ b/README.md @@ -244,9 +244,9 @@ O Arduino opera com **5 estados**: |---|--------|-----------| | 1 | **AGUARDANDO_PEDIDO** | Sistema em repouso, aguarda comandos remotos (botões físicos desabilitados) | | 2 | **VERIFICANDO_ESTOQUE** | Verifica sensor do topo + contador interno | -| 3 | **ACIONANDO_ESTEIRA** | Liga motor da esteira secundária correspondente | -| 4 | **ENTREGANDO_PECA** | Monitora o sensor da junção (timeout de 12,5 s); só debita o estoque após a confirmação e mantém o motor 3 s depois dela | -| 5 | **ERRO** | Sinaliza falha no LCD, aguarda reset manual | +| 3 | **ACIONANDO_ESTEIRA** | Liga o motor da esteira escolhida (kick-start e depois o PWM de regime) | +| 4 | **ENTREGANDO_PECA** | Supervisão por marcos: topo livre em 3 s, junção em 12,5 s (débito na confirmação) e 3 s de saída | +| 5 | **ERRO** | Motores desligados; LCD e dashboard mostram onde falhou (ver `docs/ARCHITECTURE.md` §3); aguarda reset | > 📊 Diagramas completos (máquina de estados, fluxo operacional e sequência de comunicação) em [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md#3-máquina-de-estados-arduino). > diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 3273e85..855691f 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -105,22 +105,57 @@ Linhas que não são JSON (logs de debug do Arduino) só aparecem no monitor ser ## 3. Máquina de Estados (Arduino) -**5 estados:** +**5 estados** (firmware v3.0 — código em `arduino/data_flow_inventory/src/logica/fsm.cpp`): 1. **AGUARDANDO_PEDIDO** → recebe comando -2. **VERIFICANDO_ESTOQUE** → verifica se há peça -3. **ACIONANDO_ESTEIRA** → liga o motor da esteira secundária escolhida -4. **ENTREGANDO_PECA** → monitora o sensor de junção (timeout de 12,5 s); ao confirmar a passagem, mantém o motor 3 s, decrementa o estoque e publica a entrega -5. **ERRO** → `sem_estoque` ou `timeout` (requer CMD:RESET) +2. **VERIFICANDO_ESTOQUE** → checagem prévia, sem ligar o motor: contador > 0, peça no sensor do topo, junção livre +3. **ACIONANDO_ESTEIRA** → liga o motor da esteira escolhida (kick-start em PWM 255 por 150 ms, depois o PWM de regime da esteira) +4. **ENTREGANDO_PECA** → supervisão por marcos (abaixo); o estoque é debitado na confirmação da junção +5. **ERRO** → motores desligados a cada ciclo; só sai com `CMD:RESET` + +**Supervisão da entrega por marcos** (prazos por esteira em `arduino/data_flow_inventory/config.h`): + +| Marco | Prova | Prazo padrão | Falha → erro | Estoque | +|---|---|---|---|---| +| M1 Partida | sensor de topo fica livre | 3 s após ligar | `motor_sem_avanco` (motor parado na hora) | não debita | +| M2 Trânsito | junção registra uma passagem (pulso ≥ 20 ms, capturado por interrupção) | 12,5 s após ligar | `timeout` (retirar a peça da esteira) | não debita | +| Confirmação | — | — | — | **debita** e publica a entrega | +| M3 Saída | motor segue 3 s; no fim a junção precisa estar livre | 3 s | `peca_presa_saida` | já debitado | + +```mermaid +flowchart LR + P[Pedido] --> V{Checagem prévia} + V -- falha --> E[ERRO] + V -- ok --> M1{M1: topo livre em 3 s?} + M1 -- não --> E + M1 -- sim --> M2{M2: junção em 12,5 s?} + M2 -- não --> E + M2 -- sim --> D[Debita estoque e publica entrega] + D --> M3{M3: junção livre após 3 s?} + M3 -- não --> E + M3 -- sim --> A[AGUARDANDO_PEDIDO] + E -- CMD:RESET --> A +``` **Erros que levam a ERRO (exigem CMD:RESET):** -- `sem_estoque` — sem peça no sensor do topo ou estoque zerado -- `timeout` — a peça não foi confirmada pelo sensor de junção em 12,5 s (motor desligado, estoque intacto) +- `sem_estoque` — contador da peça zerado +- `sem_peca_topo` — contador > 0, mas sem peça no sensor do topo +- `juncao_obstruida` — sensor de junção já ocupado antes de ligar o motor +- `motor_sem_avanco` — M1: o topo não liberou no prazo (motor travado, fraco ou peça presa no início) +- `timeout` — M2: a peça saiu do topo e não chegou à junção no prazo +- `peca_presa_saida` — M3: a junção continuou ocupada no fim da saída **Rejeições (não mudam o estado):** -- `peca_invalida` — letra diferente de A, B ou C -- `ocupado` — pedido com a FSM fora de AGUARDANDO_PEDIDO +- `peca_invalida` — formato diferente de `CMD:PECA:` + A, B ou C +- `ocupado` — pedido com a FSM fora de AGUARDANDO_PEDIDO (o campo `peca` é a peça pedida) - `comando_desconhecido` — linha não reconhecida +**Campos opcionais (v3.0)** — as mensagens da v2.x continuam idênticas; os campos abaixo só são acrescentados no fim: +- `evento: inicio` → `reset` (`energia`, `reinicio`, `watchdog` ou `brownout`), `lcd` (`true`/`false`), `ram_livre` +- `type: status` → `ram_livre` +- `evento: erro` → `fase` (`verificacao`, `partida`, `transito`, `saida`), `t_ms` (desde a partida do motor), `bordas_juncao` + +**Robustez:** watchdog de 2 s (o Uno reinicia sozinho se o loop travar; quando o watchdog avisa a trava, os motores são desligados na hora, antes do reset); o LCD é procurado em 0x27 e 0x3F e, se não responder, o sistema segue sem ele (se o barramento I²C travar durante a operação, o LCD é desligado em vez de travar o loop); a confirmação da junção ignora uma ocupação que começou antes da partida do motor; o estoque não é persistido (volta a 15 no boot) e o campo `reset` do `inicio` diz por que o Uno reiniciou. Abrir a Serial Monitor reinicia o Uno (auto-reset da USB), o que aparece como `reset: reinicio`. + **Diagramas:** ![Máquina de estados (FSM) - Arduino Uno](fluxogramas/maquina_de_estados_fsm_arduino_uno.png) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index df92b3f..ed2eba3 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -14,6 +14,12 @@ Convenção de seções: `Adicionado`, `Alterado`, `Corrigido`, `Segurança`, `R ### Alterado +- **Firmware do Uno v3.0 — supervisão por marcos e robustez (03/10/2026)** + - Firmware reestruturado em módulos: `src/logica/` (C++ puro: protocolo, filtro, supervisor, FSM, telas, causa do reset) testado no PC (`test/firmware_uno`, 40 testes, job `firmware-logica` no CI) e `src/hw/` (motores, sensores, serial, LCD, diagnóstico) + - **Compatibilidade:** mensagens JSON idênticas às da v2.x (testes de contrato byte a byte); ESP32 e servidor não mudaram. Novos campos opcionais (`reset`, `lcd`, `ram_livre`, `fase`, `t_ms`, `bordas_juncao`) e novos erros (`sem_peca_topo`, `juncao_obstruida`, `motor_sem_avanco`, `peca_presa_saida`) + - **Entrega por marcos:** checagem prévia sem ligar o motor; M1 topo livre em 3 s (motor travado para na hora); M2 junção em 12,5 s; débito na confirmação; M3 junção livre após 3 s de saída. Junções capturadas por interrupção, com pulso mínimo de 20 ms; a confirmação ignora uma ocupação que começou antes da partida do motor + - **Robustez:** kick-start dos motores, watchdog de 2 s (quando avisa que o loop travou, desliga os motores na hora, antes do reset), causa do reset no `inicio`, LCD procurado em 0x27/0x3F e opcional (se o I²C travar durante a operação, o LCD é desligado e o sistema segue sem ele), textos na flash (RAM global de 55% para 45%), sem ArduinoJson no Uno + - **Rollback:** firmware v2.x no commit `fddde08`. **Validação em bancada pendente:** `docs/testes/roteiros/firmware_uno_v3_validacao.md` - **Firmware Uno — revisão do código e reaplicação das atualizações de 02/10/2026** - **Reaplicado:** `TIMEOUT_ENTREGA` de 9 s para **12,5 s** (vigia só o trajeto topo → junção; estourou, as três esteiras são desligadas e o sistema vai para `ERRO` sem debitar estoque), confirmação por **passagem** no sensor de junção (o sensor precisa ser visto livre depois que a esteira liga) e estoque inicial de **15** peças por tipo. O PWM por esteira (`VELOCIDADE_ESTEIRA_A/B/C`) foi mantido, mas o valor da C passou de 140 para **200** (igual a A e B): o plano de testes registra ~150 como o PWM mínimo que move a esteira com peça, então 140 não movia a peça; calibre a C em bancada se o motor novo for mais rápido - **Corrigido:** o LCD era redesenhado (`lcd.clear()` e criação de `String`) a cada volta do loop enquanto o sistema estava em `ERRO` por timeout, o que fazia o display piscar e fragmentava o heap do Uno (2 KB de RAM); a mensagem agora é desenhada uma única vez, na transição para `ERRO` diff --git a/docs/CI-CD.md b/docs/CI-CD.md index bade6d7..4fa6611 100644 --- a/docs/CI-CD.md +++ b/docs/CI-CD.md @@ -99,10 +99,15 @@ npm update - `test/esteira_peca_b/esp32_esteiras_ab/esp32_esteiras_ab.ino` (ESP32 teste AB) - **Placas:** Arduino:avr:uno (Uno) · esp32:esp32:esp32 (ESP32) - **Cache:** Habilitado para cores e bibliotecas (reduz tempo de CI em ~60%) -- **Falha em:** Erros de compilação +- **Falha em:** Erros de compilação ou RAM global do Uno acima de 65% - **Passos antes de compilar:** instala as bibliotecas `LiquidCrystal I2C`, `ArduinoJson@6.21.5` e `PubSubClient` e copia `esp32/gateway_mqtt/secrets.h.example` para `secrets.h` (o arquivo real não é versionado) - Para repetir o job na sua máquina, veja [Compilar o firmware localmente](#compilar-o-firmware-localmente-sem-hardware) +#### 5. Lógica do firmware do Uno (`firmware-logica`) +- **O quê:** compila `arduino/data_flow_inventory/src/logica/` com g++ e roda `test/firmware_uno` (FSM, marcos, filtro, protocolo, telas e causa do reset, com tempo simulado) +- **Contrato:** os testes `*_igual_v2` garantem que as mensagens JSON continuam idênticas às da v2.x +- **Local:** `docker run --rm -v ":/w" -w /w gcc:14 make -C test/firmware_uno test` (ou `make -C test/firmware_uno test` com g++ instalado) + --- diff --git a/docs/testes/roteiros/firmware_uno_v3_validacao.md b/docs/testes/roteiros/firmware_uno_v3_validacao.md new file mode 100644 index 0000000..13bae71 --- /dev/null +++ b/docs/testes/roteiros/firmware_uno_v3_validacao.md @@ -0,0 +1,65 @@ +# Roteiro — Validação em bancada do firmware do Uno v3.0 + +> Objetivo: confirmar na bancada o que os testes automatizados não alcançam (sensores, motores, alimentação). Execute na ordem; anote o resultado de cada passo. Firmware: `arduino/data_flow_inventory/` (v3.0). Desconecte o ESP32 dos pinos 0/1 durante o upload no Uno. + +Ferramentas: Serial Monitor (9600 baud) **ou** `test/mqtt_probe` (com o ESP32 conectado) e o dashboard. + +> Abrir a Serial Monitor reinicia o Uno (auto-reset da USB) e o estoque volta a 15. Isso é esperado e aparece como `"reset":"reinicio"`. + +## 1. Boot + +1. Ligue o Uno. +2. **Esperado:** a linha `{"type":"evento","evento":"inicio",...,"versao":"3.0",...,"reset":"energia","lcd":true|false,"ram_livre":N}`. +3. Anote `lcd` (se `false`, o LCD não respondeu em 0x27 nem 0x3F: confira 5 V, GND, SDA=A4, SCL=A5) e `ram_livre`. + +## 2. Sensores (antes de qualquer entrega) + +1. Com as três esteiras **vazias**, leia a mensagem `"type":"sensores"`: esperado `"juncao":{"J1":0,"J2":0,"J3":0}`. +2. Passe uma peça sobre cada sensor de junção, um por vez: o valor correspondente deve ir a 1. +3. Repita para os sensores de topo (`"topo"`), com e sem peça. +4. **Se algum sensor ficar sempre em 0 ou sempre em 1**, ajuste o potenciômetro do módulo TCRT5000 (ou a fiação) antes de seguir. Este passo resolve a causa provável da "entrega da B sem débito". + +## 3. Uma entrega por esteira (A, B, C) + +1. Coloque uma peça no topo da esteira e peça pelo dashboard. +2. **Esperado:** `pedido` → status `ACIONANDO_ESTEIRA` → esteira liga → `entrega` com o estoque daquela peça diminuído em 1 → após ~3 s a esteira para → status `AGUARDANDO_PEDIDO`. +3. Confira o débito no dashboard e no LCD. + +## 4. Motor travado (esteira C) + +1. Segure a esteira C com a mão (ou trave a peça no topo) e peça uma peça C. +2. **Esperado:** em ~3 s, `erro` com `"tipo":"motor_sem_avanco","fase":"partida"`, motor parado, estado `ERRO`, **sem reset do Uno** (nenhum novo `inicio`). +3. Envie Reiniciar no dashboard: volta a `AGUARDANDO_PEDIDO`. + +## 5. Timeout no trânsito + +1. Peça uma peça e, depois que ela sair do topo, retire-a da esteira antes da junção. +2. **Esperado:** em 12,5 s desde a partida, `erro` com `"tipo":"timeout","fase":"transito"`, motor parado, estoque **intacto**. + +## 6. Peça presa na saída + +1. Peça uma peça e, quando ela chegar à junção, segure-a sobre o sensor. +2. **Esperado:** `entrega` (débito feito) e, 3 s depois, `erro` com `"tipo":"peca_presa_saida","fase":"saida"`. + +## 7. Junção obstruída + +1. Deixe uma peça parada sobre o sensor de junção e peça uma peça daquela esteira. +2. **Esperado:** `erro` com `"tipo":"juncao_obstruida","fase":"verificacao"`, **sem ligar o motor**. + +## 8. Resistência + +1. Faça 10 entregas seguidas (alternando A, B e C). +2. **Esperado:** nenhum `inicio` novo no meio (sem resets) e `ram_livre` do status estável. + +## Registro + +| Passo | Resultado | Observações | +|---|---|---| +| 1 Boot | | | +| 2 Sensores | | | +| 3 Entregas A/B/C | | | +| 4 Motor travado | | | +| 5 Timeout | | | +| 6 Peça presa | | | +| 7 Junção obstruída | | | +| 8 Resistência | | | From 11166ece99bf7ab7343d3d3bd5d727d16609776b Mon Sep 17 00:00:00 2001 From: Matheus Nespolo Date: Sat, 3 Oct 2026 19:40:54 -0300 Subject: [PATCH 13/21] docs(firmware): roteiro com peca no topo nos passos de falha e notas de versao Co-Authored-By: Claude Opus 5.5 --- docs/ARCHITECTURE.md | 2 ++ docs/CI-CD.md | 2 +- docs/testes/roteiros/firmware_uno_v3_validacao.md | 10 +++++----- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 855691f..591271f 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -158,6 +158,8 @@ flowchart LR **Diagramas:** +As imagens abaixo são anteriores à v3.0 e não mostram os marcos; vale o diagrama Mermaid acima. + ![Máquina de estados (FSM) - Arduino Uno](fluxogramas/maquina_de_estados_fsm_arduino_uno.png) ![Fluxograma operacional — ciclo de um pedido](fluxogramas/fluxograma_operacional_ciclo_pedido.png) diff --git a/docs/CI-CD.md b/docs/CI-CD.md index 4fa6611..7ca577c 100644 --- a/docs/CI-CD.md +++ b/docs/CI-CD.md @@ -273,7 +273,7 @@ Código de saída `0` em todos = compilou. A compilação do ESP32 leva alguns m **Observações:** - Com `ArduinoJson` 7.x (o que a Arduino IDE instala por padrão) aparecem avisos de depreciação do `StaticJsonDocument`; não impedem a compilação. Para igualar o CI, instale a 6.21.5. -- Última compilação local registrada: 02/10/2026, com os 4 sketches compilando (Uno principal usa 54% da flash e 55% da RAM). Isso **não** valida o comportamento: timeout, confirmação por passagem e PWM da esteira C seguem pendentes de bancada. +- Última compilação local registrada: 02/10/2026, com os 4 sketches compilando (Uno principal usa 54% da flash e 55% da RAM). Isso **não** valida o comportamento: timeout, confirmação por passagem e PWM da esteira C seguem pendentes de bancada. (registro anterior ao firmware v3.0; a v3.0 compila com 45% da RAM e a validação em bancada segue o roteiro `docs/testes/roteiros/firmware_uno_v3_validacao.md`) --- diff --git a/docs/testes/roteiros/firmware_uno_v3_validacao.md b/docs/testes/roteiros/firmware_uno_v3_validacao.md index 13bae71..d398369 100644 --- a/docs/testes/roteiros/firmware_uno_v3_validacao.md +++ b/docs/testes/roteiros/firmware_uno_v3_validacao.md @@ -9,7 +9,7 @@ Ferramentas: Serial Monitor (9600 baud) **ou** `test/mqtt_probe` (com o ESP32 co ## 1. Boot 1. Ligue o Uno. -2. **Esperado:** a linha `{"type":"evento","evento":"inicio",...,"versao":"3.0",...,"reset":"energia","lcd":true|false,"ram_livre":N}`. +2. **Esperado** (se o Uno já estava ligado e foi reiniciado pela USB, `reset` pode ser `"reinicio"`): a linha `{"type":"evento","evento":"inicio",...,"versao":"3.0",...,"reset":"energia","lcd":true|false,"ram_livre":N}`. 3. Anote `lcd` (se `false`, o LCD não respondeu em 0x27 nem 0x3F: confira 5 V, GND, SDA=A4, SCL=A5) e `ram_livre`. ## 2. Sensores (antes de qualquer entrega) @@ -22,12 +22,12 @@ Ferramentas: Serial Monitor (9600 baud) **ou** `test/mqtt_probe` (com o ESP32 co ## 3. Uma entrega por esteira (A, B, C) 1. Coloque uma peça no topo da esteira e peça pelo dashboard. -2. **Esperado:** `pedido` → status `ACIONANDO_ESTEIRA` → esteira liga → `entrega` com o estoque daquela peça diminuído em 1 → após ~3 s a esteira para → status `AGUARDANDO_PEDIDO`. +2. **Esperado:** `pedido` → status `ACIONANDO_ESTEIRA` → esteira liga → `entrega` com o estoque daquela peça diminuído em 1 → cerca de 3 s depois da `entrega` a esteira para → status `AGUARDANDO_PEDIDO` (volta ~1,5 s depois). 3. Confira o débito no dashboard e no LCD. ## 4. Motor travado (esteira C) -1. Segure a esteira C com a mão (ou trave a peça no topo) e peça uma peça C. +1. Com uma peça no topo da esteira C (sem ela o erro seria `sem_peca_topo`), segure a esteira C com a mão (ou trave a peça no topo) e peça uma peça C. 2. **Esperado:** em ~3 s, `erro` com `"tipo":"motor_sem_avanco","fase":"partida"`, motor parado, estado `ERRO`, **sem reset do Uno** (nenhum novo `inicio`). 3. Envie Reiniciar no dashboard: volta a `AGUARDANDO_PEDIDO`. @@ -43,8 +43,8 @@ Ferramentas: Serial Monitor (9600 baud) **ou** `test/mqtt_probe` (com o ESP32 co ## 7. Junção obstruída -1. Deixe uma peça parada sobre o sensor de junção e peça uma peça daquela esteira. -2. **Esperado:** `erro` com `"tipo":"juncao_obstruida","fase":"verificacao"`, **sem ligar o motor**. +1. Com uma peça no topo da esteira, deixe outra peça parada sobre o sensor de junção e peça uma peça daquela esteira. +2. **Esperado:** `erro` com `"tipo":"juncao_obstruida","fase":"verificacao"`, **sem ligar o motor**. Sem peça no topo, o erro publicado é `sem_peca_topo` (a checagem prévia confere estoque, depois topo, depois junção). ## 8. Resistência From bd8cb27790c2bfafcc0c45abe0d5446fd0d92c40 Mon Sep 17 00:00:00 2001 From: Matheus Nespolo Date: Sat, 3 Oct 2026 20:01:56 -0300 Subject: [PATCH 14/21] fix(firmware): juncao ocupada no ciclo da partida vai para erro sem ligar o motor Um pulso que comecava antes da partida e terminava depois confirmava a entrega pelo maiorPulsoMs. O ACIONANDO_ESTEIRA repete a checagem da juncao antes de zerar a captura e iniciar o supervisor. Co-Authored-By: Claude Opus 5.5 --- .../data_flow_inventory/src/logica/fsm.cpp | 7 +++++ test/firmware_uno/teste_fsm.cpp | 27 +++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/arduino/data_flow_inventory/src/logica/fsm.cpp b/arduino/data_flow_inventory/src/logica/fsm.cpp index 600e9f2..a922b51 100644 --- a/arduino/data_flow_inventory/src/logica/fsm.cpp +++ b/arduino/data_flow_inventory/src/logica/fsm.cpp @@ -148,6 +148,13 @@ void fsmPasso(Fsm& f, const EntradasFsm& in, SaidasFsm& out) { break; case ACIONANDO_ESTEIRA: + // A junção pode ter ocupado entre a verificação e este ciclo. Um pulso + // que começou antes da partida não pode confirmar a entrega: erro sem + // ligar o motor (e sem zerar a captura nem iniciar o supervisor). + if (in.juncao[idx].ocupada) { + falhar(f, out, ERR_JUNCAO_OBSTRUIDA, FERR_VERIFICACAO, -1, -1); + break; + } out.zerarJuncao = (int8_t)idx; supervisorIniciar(f.sup, f.params[idx], in.agora); f.estado = ENTREGANDO_PECA; diff --git a/test/firmware_uno/teste_fsm.cpp b/test/firmware_uno/teste_fsm.cpp index db0b62a..7498e6a 100644 --- a/test/firmware_uno/teste_fsm.cpp +++ b/test/firmware_uno/teste_fsm.cpp @@ -137,6 +137,33 @@ TESTE(checagem_previa_vai_para_erro_sem_ligar_motor) { } } +TESTE(juncao_ocupada_na_partida_vai_para_erro_sem_motor_e_sem_debito) { + // A verificação passou (junção livre), mas no ciclo seguinte, o da + // partida, a junção já está ocupada: um pulso que começou antes da partida + // não pode confirmar a entrega (R1). Erro sem ligar o motor. + for (int idx = 0; idx < 3; idx++) { + Bancada b; + const char letra = (char)('A' + idx); + b.passo(0, CMD_PECA, letra); + VERIFICA(b.f.estado == ACIONANDO_ESTEIRA); + b.in.juncao[idx].ocupada = true; + b.in.juncao[idx].ocupouEm = 2; + b.passo(5); + VERIFICA(b.f.estado == ERRO && !b.algumMotor()); + VERIFICA(b.out.zerarJuncao < 0); + char esperado[64]; + snprintf(esperado, sizeof esperado, "erro juncao_obstruida %c verificacao\nestado ERRO\nesteiras 000", letra); + VERIFICA(b.contem(esperado)); + // O pulso termina depois com largura "longa": nada debita. + b.in.juncao[idx].ocupada = false; + b.in.juncao[idx].maiorPulsoMs = 40; + b.in.juncao[idx].bordas = 1; + b.ate(10, 20000); + VERIFICA(b.debitos == 0 && b.confirmacoes == 0); + VERIFICA(!b.algumMotor()); + } +} + TESTE(caso_p2_esteira_c_travada_para_em_3s_sem_debito) { Bancada b; b.passo(0, CMD_PECA, 'C'); From 816c5243f988c16f93b5237b559b555e439ebdb6 Mon Sep 17 00:00:00 2001 From: Matheus Nespolo Date: Sat, 3 Oct 2026 20:01:57 -0300 Subject: [PATCH 15/21] fix(firmware): energizacao com PORF e BORF sai como energia, nao brownout Co-Authored-By: Claude Opus 5.5 --- arduino/data_flow_inventory/src/logica/causa_reset.cpp | 3 ++- test/firmware_uno/teste_filtro_reset.cpp | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/arduino/data_flow_inventory/src/logica/causa_reset.cpp b/arduino/data_flow_inventory/src/logica/causa_reset.cpp index 259dcd4..a83cf3e 100644 --- a/arduino/data_flow_inventory/src/logica/causa_reset.cpp +++ b/arduino/data_flow_inventory/src/logica/causa_reset.cpp @@ -4,7 +4,8 @@ const char* causaReset(uint8_t mcusr, bool assinaturaValida, bool marcaWatchdog) { if ((mcusr & MCUSR_WDRF) || (assinaturaValida && marcaWatchdog)) return TXT("watchdog"); - if (mcusr & MCUSR_BORF) return TXT("brownout"); + // Energia antes de brownout: a energização acende PORF e BORF juntos (0x05). if ((mcusr & MCUSR_PORF) || !assinaturaValida) return TXT("energia"); + if (mcusr & MCUSR_BORF) return TXT("brownout"); return TXT("reinicio"); } diff --git a/test/firmware_uno/teste_filtro_reset.cpp b/test/firmware_uno/teste_filtro_reset.cpp index 01eab9f..30df6c0 100644 --- a/test/firmware_uno/teste_filtro_reset.cpp +++ b/test/firmware_uno/teste_filtro_reset.cpp @@ -39,6 +39,9 @@ TESTE(causa_reset) { VERIFICA_TEXTO(causaReset(MCUSR_WDRF, false, false), "watchdog"); VERIFICA_TEXTO(causaReset(0, true, true), "watchdog"); VERIFICA_TEXTO(causaReset(MCUSR_BORF, true, false), "brownout"); + // Energização costuma acender PORF e BORF juntos (0x05): é energia. + VERIFICA_TEXTO(causaReset(MCUSR_PORF | MCUSR_BORF, true, false), "energia"); + VERIFICA_TEXTO(causaReset(MCUSR_BORF, false, false), "energia"); // RAM perdida VERIFICA_TEXTO(causaReset(MCUSR_PORF, true, false), "energia"); VERIFICA_TEXTO(causaReset(0, false, false), "energia"); VERIFICA_TEXTO(causaReset(0, true, false), "reinicio"); From ad3ddd9ebe7d76131d7d43c3544edc6b53b6861d Mon Sep 17 00:00:00 2001 From: Matheus Nespolo Date: Sat, 3 Oct 2026 20:02:01 -0300 Subject: [PATCH 16/21] test(firmware): bordas do timeout (12499/12500 ms) e do pulso minimo (19/20 ms) Co-Authored-By: Claude Opus 5.5 --- test/firmware_uno/teste_supervisor.cpp | 43 ++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/test/firmware_uno/teste_supervisor.cpp b/test/firmware_uno/teste_supervisor.cpp index 50f9011..cc99584 100644 --- a/test/firmware_uno/teste_supervisor.cpp +++ b/test/firmware_uno/teste_supervisor.cpp @@ -107,6 +107,49 @@ TESTE(supervisor_ocupou_depois_da_leitura_do_millis_nao_confirma) { VERIFICA(supervisorAtualizar(s, 2021, false, j) == SUP_CONFIRMOU); // 20 ms reais depois } +TESTE(supervisor_borda_do_timeout_12499_confirma_12500_falha) { + { // passagem válida 1 ms antes do prazo: confirma + Supervisor s; + supervisorIniciar(s, paramPadrao(), 0); + LeituraJuncao j = livre(); + VERIFICA(supervisorAtualizar(s, 1000, false, j) == SUP_SEGUE); + VERIFICA(supervisorAtualizar(s, 12498, false, j) == SUP_SEGUE); + j.maiorPulsoMs = 20; + j.bordas = 2; + VERIFICA(supervisorAtualizar(s, 12499, false, j) == SUP_CONFIRMOU); + } + { // sem passagem até 12500: timeout + Supervisor s; + supervisorIniciar(s, paramPadrao(), 0); + LeituraJuncao j = livre(); + VERIFICA(supervisorAtualizar(s, 1000, false, j) == SUP_SEGUE); + VERIFICA(supervisorAtualizar(s, 12500, false, j) == SUP_FALHA_TIMEOUT); + VERIFICA(s.fase == FASE_FIM); + } +} + +TESTE(supervisor_borda_do_pulso_minimo_19_nao_20_confirma) { + { + Supervisor s; + supervisorIniciar(s, paramPadrao(), 0); + LeituraJuncao j = livre(); + supervisorAtualizar(s, 1000, false, j); + j.maiorPulsoMs = 19; + j.bordas = 2; + VERIFICA(supervisorAtualizar(s, 2000, false, j) == SUP_SEGUE); + VERIFICA(!passagemValida(j, 2000, 20)); + } + { + Supervisor s; + supervisorIniciar(s, paramPadrao(), 0); + LeituraJuncao j = livre(); + supervisorAtualizar(s, 1000, false, j); + j.maiorPulsoMs = 20; + j.bordas = 2; + VERIFICA(supervisorAtualizar(s, 2000, false, j) == SUP_CONFIRMOU); + } +} + TESTE(supervisor_juncao_ocupada_antes_da_partida_nao_confirma) { // Ocupou entre a checagem prévia e a partida do motor: não é passagem. Supervisor s; From 9ee5ca7cf97949d2a07b5187790533af9fe8fe57 Mon Sep 17 00:00:00 2001 From: Matheus Nespolo Date: Sat, 3 Oct 2026 20:05:22 -0300 Subject: [PATCH 17/21] fix(firmware): captura da juncao descarta pulso anterior a partida e limita rajada de PCINT - sensorJuncaoZerar marca o pulso em curso; ao liberar, a largura dele nao entra em maiorPulsoMs (R1). - Mais de 8 bordas de uma juncao no mesmo millis() desligam a PCINT dela; sensoresAmostrar ressincroniza o nivel e religa. Uma rajada do LM393 no limiar nao deixa mais o loop e a WDT_vect sem CPU. Co-Authored-By: Claude Opus 5.5 --- .../data_flow_inventory/src/hw/sensores.cpp | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/arduino/data_flow_inventory/src/hw/sensores.cpp b/arduino/data_flow_inventory/src/hw/sensores.cpp index f0bf9da..23d99ef 100644 --- a/arduino/data_flow_inventory/src/hw/sensores.cpp +++ b/arduino/data_flow_inventory/src/hw/sensores.cpp @@ -9,11 +9,20 @@ namespace { const uint8_t PINOS_TOPO[3] = {SENSOR_TOPO_A, SENSOR_TOPO_B, SENSOR_TOPO_C}; FiltroEntrada topo[3]; +// Mais que isto de bordas de uma junção no mesmo milissegundo = rajada. +const uint8_t MAX_BORDAS_POR_MS = 8; + struct CapturaJuncao { volatile bool ocupada; volatile uint32_t ocupouEm; volatile uint16_t maiorPulsoMs; volatile uint8_t bordas; + // O pulso em curso começou antes do último zerar: a largura dele não vale. + volatile bool descartarPulsoAtual; + // Limitador de rajada (ver registrar): interrupção da junção desligada. + volatile bool suspensa; + volatile uint16_t msUltimaBorda; // 16 bits baixos do millis() da última borda + volatile uint8_t bordasNoMs; }; CapturaJuncao juncao[3]; @@ -22,20 +31,88 @@ inline bool lerJ1() { return (PINC & _BV(PC3)) == 0; } // A3 inline bool lerJ2() { return (PIND & _BV(PD2)) == 0; } // D2 inline bool lerJ3() { return (PIND & _BV(PD4)) == 0; } // D4 +inline bool lerJuncao(uint8_t i) { + return i == 0 ? lerJ1() : (i == 1 ? lerJ2() : lerJ3()); +} + +// Liga/desliga o bit da junção no PCMSK do grupo dela. +inline void mascara(uint8_t i, bool ligar) { + if (i == 0) { + if (ligar) PCMSK1 |= _BV(PCINT11); else PCMSK1 &= (uint8_t)~_BV(PCINT11); + } else { + const uint8_t bit = (i == 1) ? _BV(PCINT18) : _BV(PCINT20); + if (ligar) PCMSK2 |= bit; else PCMSK2 &= (uint8_t)~bit; + } +} + // Chamada só com interrupções desabilitadas (dentro da ISR). +// +// Limitador de rajada: com a peça parada no limiar, o comparador LM393 do +// TCRT5000 pode oscilar a dezenas de kHz. As PCINT têm prioridade maior que +// WDT_vect e TIMER0_OVF; uma rajada contínua deixaria o loop e a interrupção +// do watchdog sem CPU (sem reset, motor ligado). Por isso, se uma junção +// registra mais de MAX_BORDAS_POR_MS bordas no mesmo valor de millis() (que +// nem avança durante a rajada), a interrupção dela é desligada e a junção é +// marcada como suspensa; sensoresAmostrar ressincroniza o nível e religa. inline void registrar(uint8_t i, bool ocupada, uint32_t agora) { CapturaJuncao& c = juncao[i]; + if (c.suspensa) return; if (ocupada == c.ocupada) return; + const uint16_t ms = (uint16_t)agora; + if (ms == c.msUltimaBorda) { + if (++c.bordasNoMs > MAX_BORDAS_POR_MS) { + mascara(i, false); + c.suspensa = true; + return; + } + } else { + c.msUltimaBorda = ms; + c.bordasNoMs = 1; + } c.ocupada = ocupada; if (c.bordas < 255) c.bordas = c.bordas + 1; if (ocupada) { c.ocupouEm = agora; + c.descartarPulsoAtual = false; + } else if (c.descartarPulsoAtual) { + // Pulso que já estava em curso no zerar (começou antes da partida): + // a largura dele não pode confirmar a entrega. + c.descartarPulsoAtual = false; } else { uint32_t largura = agora - c.ocupouEm; uint16_t l = largura > 65535UL ? 65535 : (uint16_t)largura; if (l > c.maiorPulsoMs) c.maiorPulsoMs = l; } } + +// Religa uma junção suspensa pelo limitador. Chamada com interrupções +// desabilitadas. Ordem: limpa a flag pendente do grupo, religa o bit e só +// então lê os níveis; uma borda depois da leitura deixa a flag pendente e a +// ISR trata. Como limpar a flag do grupo pode descartar uma borda da outra +// junção do mesmo grupo (J2 e J3 dividem PCINT2), ela também é relida. +void retomar(uint8_t i, uint32_t agora) { + if (i == 0) { + PCIFR = _BV(PCIF1); + } else { + PCIFR = _BV(PCIF2); + } + mascara(i, true); + CapturaJuncao& c = juncao[i]; + const bool nivel = lerJuncao(i); + if (nivel != c.ocupada) { + // A largura do pulso durante a rajada é incerta: não vira maiorPulsoMs. + c.ocupada = nivel; + c.ocupouEm = agora; + c.descartarPulsoAtual = false; + if (c.bordas < 255) c.bordas = c.bordas + 1; + } + c.bordasNoMs = 0; + c.suspensa = false; + if (i != 0) { + const uint8_t outra = (i == 1) ? 2 : 1; + registrar(outra, lerJuncao(outra), agora); + } +} } // namespace ISR(PCINT1_vect) { // porta C: A3 (J1) @@ -69,6 +146,10 @@ void sensoresIniciar(uint32_t agora) { juncao[i].ocupouEm = agora; juncao[i].maiorPulsoMs = 0; juncao[i].bordas = 0; + juncao[i].descartarPulsoAtual = inicial[i]; // largura desconhecida + juncao[i].suspensa = false; + juncao[i].msUltimaBorda = 0; + juncao[i].bordasNoMs = 0; } PCICR |= _BV(PCIE1) | _BV(PCIE2); interrupts(); @@ -78,6 +159,12 @@ void sensoresAmostrar(uint32_t agora) { for (uint8_t i = 0; i < 3; i++) { filtroAtualizar(topo[i], digitalRead(PINOS_TOPO[i]) == LOW, agora); } + for (uint8_t i = 0; i < 3; i++) { + if (!juncao[i].suspensa) continue; + noInterrupts(); + retomar(i, millis()); + interrupts(); + } } bool sensorTopo(uint8_t i) { return topo[i].estavel; } @@ -99,5 +186,8 @@ void sensorJuncaoZerar(uint8_t i) { noInterrupts(); juncao[i].maiorPulsoMs = 0; juncao[i].bordas = 0; + // Se a junção já está ocupada, o pulso começou antes da partida: quando + // ela liberar, a largura medida desde o ocupouEm antigo é descartada. + juncao[i].descartarPulsoAtual = juncao[i].ocupada; interrupts(); } From f5912632e59f582e2f380eae1327941ac1ff9d5c Mon Sep 17 00:00:00 2001 From: Matheus Nespolo Date: Sat, 3 Oct 2026 20:05:24 -0300 Subject: [PATCH 18/21] fix(firmware): watchdog em volta do lcdIniciar, kick fora do tempo da serial e static_assert dos prazos - setup alimenta o watchdog antes e depois de lcdIniciar; lcdIniciar limpa a flag de timeout do I2C antes do init (sondagem de 0x27 sem resposta desligava a toa um LCD em 0x3F). - motoresAplicar roda de novo depois da serial e do LCD. - static_assert: prazoPartidaMs <= timeoutMs nas tres esteiras. Co-Authored-By: Claude Opus 5.5 --- .../data_flow_inventory/data_flow_inventory.ino | 14 ++++++++++++++ arduino/data_flow_inventory/src/hw/lcd.cpp | 3 +++ 2 files changed, 17 insertions(+) diff --git a/arduino/data_flow_inventory/data_flow_inventory.ino b/arduino/data_flow_inventory/data_flow_inventory.ino index 402b203..76a796c 100644 --- a/arduino/data_flow_inventory/data_flow_inventory.ino +++ b/arduino/data_flow_inventory/data_flow_inventory.ino @@ -37,6 +37,12 @@ static const ParamEsteira PARAMS[3] = {PARAM_ESTEIRA_A, PARAM_ESTEIRA_B, PARAM_ESTEIRA_C}; +// M1 precisa caber dentro de M2: o prazo da partida não pode passar do timeout. +#define PRAZOS_COERENTES(P) (ParamEsteira P.prazoPartidaMs <= ParamEsteira P.timeoutMs) +static_assert(PRAZOS_COERENTES(PARAM_ESTEIRA_A), "esteira A: prazoPartidaMs > timeoutMs"); +static_assert(PRAZOS_COERENTES(PARAM_ESTEIRA_B), "esteira B: prazoPartidaMs > timeoutMs"); +static_assert(PRAZOS_COERENTES(PARAM_ESTEIRA_C), "esteira C: prazoPartidaMs > timeoutMs"); + static Fsm fsm; static bool lcdPresente = false; static uint32_t inicioMs = 0; @@ -53,7 +59,11 @@ void setup() { inicioMs = millis(); sensoresIniciar(inicioMs); + // lcdIniciar pode levar mais de 1 s com o I2C ruim: alimenta o watchdog + // antes e depois para não entrar em laço de reset no boot. + watchdogAlimentar(); lcdPresente = lcdIniciar(); + watchdogAlimentar(); lcdAtualizar("Data Flow", "Inventory v" VERSAO_FIRMWARE); aberturaMs = millis(); // lcdIniciar leva ~1 s: a abertura conta daqui @@ -112,5 +122,9 @@ void loop() { lcdAtualizar(l1, l2); } + // De novo depois da serial e do LCD: no ciclo da partida, o kick de 255 + // termina no tempo certo mesmo que a publicação tenha demorado. + motoresAplicar(desejado, PARAMS, millis()); + watchdogAlimentar(); } diff --git a/arduino/data_flow_inventory/src/hw/lcd.cpp b/arduino/data_flow_inventory/src/hw/lcd.cpp index 305e495..eeec678 100644 --- a/arduino/data_flow_inventory/src/hw/lcd.cpp +++ b/arduino/data_flow_inventory/src/hw/lcd.cpp @@ -55,6 +55,9 @@ bool lcdIniciar() { if (responde(LCD_ENDERECO_1)) ativo = &lcd1; else if (responde(LCD_ENDERECO_2)) ativo = &lcd2; if (ativo == nullptr) return false; + // A sondagem de 0x27 sem resposta pode ter deixado a flag ligada; sem + // limpar aqui, um LCD em 0x3F seria desligado à toa logo abaixo. + Wire.clearWireTimeoutFlag(); ativo->init(); ativo->backlight(); ativo->clear(); From 5af4eb02186ae2ff2722ab934588a53ae8ff7652 Mon Sep 17 00:00:00 2001 From: Matheus Nespolo Date: Sat, 3 Oct 2026 20:06:06 -0300 Subject: [PATCH 19/21] ci(firmware): fixa LiquidCrystal I2C 1.1.2 e lista firmware-logica nos checks obrigatorios Co-Authored-By: Claude Opus 5.5 --- .github/workflows/lint-and-security.yaml | 2 +- docs/CI-CD.md | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/lint-and-security.yaml b/.github/workflows/lint-and-security.yaml index 6b2f816..ed89b0a 100644 --- a/.github/workflows/lint-and-security.yaml +++ b/.github/workflows/lint-and-security.yaml @@ -114,7 +114,7 @@ jobs: - name: Instalar bibliotecas Arduino run: | - arduino-cli lib install "LiquidCrystal I2C" "ArduinoJson@6.21.5" "PubSubClient" + arduino-cli lib install "LiquidCrystal I2C@1.1.2" "ArduinoJson@6.21.5" "PubSubClient" - name: Criar secrets.h de exemplo (arquivo real não é versionado) run: cp esp32/gateway_mqtt/secrets.h.example esp32/gateway_mqtt/secrets.h diff --git a/docs/CI-CD.md b/docs/CI-CD.md index 7ca577c..3119827 100644 --- a/docs/CI-CD.md +++ b/docs/CI-CD.md @@ -100,7 +100,8 @@ npm update - **Placas:** Arduino:avr:uno (Uno) · esp32:esp32:esp32 (ESP32) - **Cache:** Habilitado para cores e bibliotecas (reduz tempo de CI em ~60%) - **Falha em:** Erros de compilação ou RAM global do Uno acima de 65% -- **Passos antes de compilar:** instala as bibliotecas `LiquidCrystal I2C`, `ArduinoJson@6.21.5` e `PubSubClient` e copia `esp32/gateway_mqtt/secrets.h.example` para `secrets.h` (o arquivo real não é versionado) +- **Passos antes de compilar:** instala as bibliotecas `LiquidCrystal I2C@1.1.2`, `ArduinoJson@6.21.5` e `PubSubClient` e copia `esp32/gateway_mqtt/secrets.h.example` para `secrets.h` (o arquivo real não é versionado) +- **LCD:** o CI compila com a LiquidCrystal I2C **1.1.2** (Frank de Brabander), com versão fixa para o build não mudar sozinho. A Arduino IDE da equipe instala outra variante (LiquidCrystal_I2C 2.0.0, Martin Kubovčík / Frank de Brabander); a API usada pelo firmware (`LiquidCrystal_I2C(endereço, 16, 2)`, `init`, `backlight`, `clear`, `setCursor`, `write`) existe nas duas. - Para repetir o job na sua máquina, veja [Compilar o firmware localmente](#compilar-o-firmware-localmente-sem-hardware) #### 5. Lógica do firmware do Uno (`firmware-logica`) @@ -137,11 +138,14 @@ No GitHub Projects, marcar como **Required Status Checks**: - ✅ lint-and-security / lint-javascript - ✅ lint-and-security / secret-detection - ✅ lint-and-security / arduino-compile +- ✅ lint-and-security / firmware-logica - ✅ test / jest (futuro) - ✅ test / e2e-docker (futuro) **Bloqueio:** PR não pode fazer merge sem passar em todos. +> **Nota:** a lista acima só bloqueia o merge depois que a regra de proteção da branch `main` é ativada no GitHub. Isso é feito pela equipe nas configurações do repositório (Settings → Branches → Branch protection rules), marcando estes checks como obrigatórios; o workflow sozinho não ativa a regra. + --- ## 4. Pre-commit Hooks (Local) @@ -253,7 +257,7 @@ arduino-cli config add board_manager.additional_urls https://raw.githubuserconte arduino-cli core update-index arduino-cli core install arduino:avr arduino-cli core install esp32:esp32 -arduino-cli lib install "LiquidCrystal I2C" "ArduinoJson@6.21.5" "PubSubClient" +arduino-cli lib install "LiquidCrystal I2C@1.1.2" "ArduinoJson@6.21.5" "PubSubClient" ``` **3. Criar o `secrets.h`** (só se ainda não existir; nunca versione o real): From 22f357a9576845aac7ae87eee7042c7a03270055 Mon Sep 17 00:00:00 2001 From: Matheus Nespolo Date: Sat, 3 Oct 2026 20:06:30 -0300 Subject: [PATCH 20/21] docs(firmware): juncao ocupada na partida, limitador de rajada e causa do reset Co-Authored-By: Claude Opus 5.5 --- docs/ARCHITECTURE.md | 2 +- docs/CHANGELOG.md | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 591271f..b01c87f 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -139,7 +139,7 @@ flowchart LR **Erros que levam a ERRO (exigem CMD:RESET):** - `sem_estoque` — contador da peça zerado - `sem_peca_topo` — contador > 0, mas sem peça no sensor do topo -- `juncao_obstruida` — sensor de junção já ocupado antes de ligar o motor +- `juncao_obstruida` — sensor de junção já ocupado antes de ligar o motor (na verificação ou no próprio ciclo da partida) - `motor_sem_avanco` — M1: o topo não liberou no prazo (motor travado, fraco ou peça presa no início) - `timeout` — M2: a peça saiu do topo e não chegou à junção no prazo - `peca_presa_saida` — M3: a junção continuou ocupada no fim da saída diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index ed2eba3..789b658 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -15,10 +15,10 @@ Convenção de seções: `Adicionado`, `Alterado`, `Corrigido`, `Segurança`, `R ### Alterado - **Firmware do Uno v3.0 — supervisão por marcos e robustez (03/10/2026)** - - Firmware reestruturado em módulos: `src/logica/` (C++ puro: protocolo, filtro, supervisor, FSM, telas, causa do reset) testado no PC (`test/firmware_uno`, 40 testes, job `firmware-logica` no CI) e `src/hw/` (motores, sensores, serial, LCD, diagnóstico) + - Firmware reestruturado em módulos: `src/logica/` (C++ puro: protocolo, filtro, supervisor, FSM, telas, causa do reset) testado no PC (`test/firmware_uno`, 43 testes, job `firmware-logica` no CI) e `src/hw/` (motores, sensores, serial, LCD, diagnóstico) - **Compatibilidade:** mensagens JSON idênticas às da v2.x (testes de contrato byte a byte); ESP32 e servidor não mudaram. Novos campos opcionais (`reset`, `lcd`, `ram_livre`, `fase`, `t_ms`, `bordas_juncao`) e novos erros (`sem_peca_topo`, `juncao_obstruida`, `motor_sem_avanco`, `peca_presa_saida`) - - **Entrega por marcos:** checagem prévia sem ligar o motor; M1 topo livre em 3 s (motor travado para na hora); M2 junção em 12,5 s; débito na confirmação; M3 junção livre após 3 s de saída. Junções capturadas por interrupção, com pulso mínimo de 20 ms; a confirmação ignora uma ocupação que começou antes da partida do motor - - **Robustez:** kick-start dos motores, watchdog de 2 s (quando avisa que o loop travou, desliga os motores na hora, antes do reset), causa do reset no `inicio`, LCD procurado em 0x27/0x3F e opcional (se o I²C travar durante a operação, o LCD é desligado e o sistema segue sem ele), textos na flash (RAM global de 55% para 45%), sem ArduinoJson no Uno + - **Entrega por marcos:** checagem prévia sem ligar o motor; M1 topo livre em 3 s (motor travado para na hora); M2 junção em 12,5 s; débito na confirmação; M3 junção livre após 3 s de saída. Junções capturadas por interrupção, com pulso mínimo de 20 ms; a confirmação ignora uma ocupação que começou antes da partida do motor (junção ocupada no próprio ciclo da partida vai para `juncao_obstruida` sem ligar o motor, e a largura de um pulso que já estava em curso na partida é descartada) + - **Robustez:** kick-start dos motores, watchdog de 2 s (quando avisa que o loop travou, desliga os motores na hora, antes do reset), causa do reset no `inicio`, LCD procurado em 0x27/0x3F e opcional (se o I²C travar durante a operação, o LCD é desligado e o sistema segue sem ele), rajada de interrupções de um sensor de junção oscilando no limiar é limitada (a interrupção dele é suspensa por até 5 ms e o nível é ressincronizado), energização com PORF e BORF juntos sai como `energia`, textos na flash (RAM global de 55% para 46%), sem ArduinoJson no Uno - **Rollback:** firmware v2.x no commit `fddde08`. **Validação em bancada pendente:** `docs/testes/roteiros/firmware_uno_v3_validacao.md` - **Firmware Uno — revisão do código e reaplicação das atualizações de 02/10/2026** - **Reaplicado:** `TIMEOUT_ENTREGA` de 9 s para **12,5 s** (vigia só o trajeto topo → junção; estourou, as três esteiras são desligadas e o sistema vai para `ERRO` sem debitar estoque), confirmação por **passagem** no sensor de junção (o sensor precisa ser visto livre depois que a esteira liga) e estoque inicial de **15** peças por tipo. O PWM por esteira (`VELOCIDADE_ESTEIRA_A/B/C`) foi mantido, mas o valor da C passou de 140 para **200** (igual a A e B): o plano de testes registra ~150 como o PWM mínimo que move a esteira com peça, então 140 não movia a peça; calibre a C em bancada se o motor novo for mais rápido From 2d9f2b69d8a526ec48eac6ba274a41a4f06f3046 Mon Sep 17 00:00:00 2001 From: Matheus Nespolo Date: Sun, 4 Oct 2026 12:05:36 -0300 Subject: [PATCH 21/21] docs: remove spec e plano do firmware v3 Co-Authored-By: Claude Opus 5.5 --- .../plans/2026-10-03-firmware-uno-v3.md | 2991 ----------------- .../2026-10-03-firmware-uno-v3-design.md | 196 -- 2 files changed, 3187 deletions(-) delete mode 100644 docs/superpowers/plans/2026-10-03-firmware-uno-v3.md delete mode 100644 docs/superpowers/specs/2026-10-03-firmware-uno-v3-design.md diff --git a/docs/superpowers/plans/2026-10-03-firmware-uno-v3.md b/docs/superpowers/plans/2026-10-03-firmware-uno-v3.md deleted file mode 100644 index 2354546..0000000 --- a/docs/superpowers/plans/2026-10-03-firmware-uno-v3.md +++ /dev/null @@ -1,2991 +0,0 @@ -# Firmware do Uno v3.0 — Supervisão por marcos e robustez — Plano de Implementação - -> **Para agentes de execução:** SUB-SKILL OBRIGATÓRIA: use superpowers:subagent-driven-development (recomendado) ou superpowers:executing-plans para executar este plano tarefa a tarefa. Os passos usam caixas de seleção (`- [ ]`). - -**Objetivo:** Reestruturar o firmware do Arduino Uno em módulos (lógica pura testável no PC + camada de hardware fina) com supervisão da entrega por marcos, watchdog, causa do reset e tolerância a falhas de periféricos, mantendo intacta a conversa com o ESP32 e o servidor. - -**Arquitetura:** `src/logica/` contém regras em C++ puro (protocolo, filtro, supervisor, FSM, telas, causa do reset), compiladas e testadas no PC com g++ (Docker local, Ubuntu no CI). `src/hw/` liga essas regras aos pinos, à serial, ao LCD e ao watchdog. O `.ino` só orquestra. Textos fixos ficam na flash (`TXT()`), e o JSON é montado sem ArduinoJson, com testes de contrato que reproduzem byte a byte as mensagens do firmware v2.x. - -**Stack:** Arduino Uno (ATmega328P, AVR core 1.8.x), C++11, `LiquidCrystal_I2C`, `Wire`, `avr/wdt.h`; g++ 14 (imagem Docker `gcc:14`) para os testes; `arduino-cli` para compilar. - -**Spec:** `docs/superpowers/specs/2026-10-03-firmware-uno-v3-design.md` (autoridade; em conflito, o spec vence). - -## Refinamentos em relação ao spec - -- **Sem ArduinoJson no Uno:** o JSON é montado por `src/logica/protocolo` (sem printf), com testes de contrato byte a byte contra o formato v2.x. Isso elimina o risco "ArduinoJson 6 vs 7" do spec e libera RAM. O CI continua instalando a biblioteca (o ESP32 usa). -- **Textos na flash (`TXT()`):** a primeira versão compilada usava 85% da RAM porque, no AVR, todo literal ocupa RAM. Com `PROGMEM` a RAM global caiu para 46% (o v2.x usava 55%). -- **Módulos lógicos a mais:** `protocolo`, `telas`, `causa_reset` e `texto_flash` saíram de dentro dos módulos de hardware para serem testáveis no PC. Os nomes de erro viraram `enum` (`TipoErro`). -- **Código já validado:** todo o código deste plano foi compilado para o Uno e testado no PC antes de o plano ser escrito; as contagens de testes de cada tarefa (17, 21, 27, 37, 38) foram conferidas. - -## Restrições Globais - -- **Nada pode quebrar o funcionamento parcial atual:** as mensagens JSON do v2.x (`status`, `estoque`, `sensores`, `esteiras`, eventos `pedido`, `entrega`, `erro`, `inicio`) continuam **idênticas byte a byte**; só entram campos opcionais **no fim** e novos valores de `tipo`. Os testes de contrato da Tarefa 1 guardam isso. -- **Não mexer no ESP32 nem no servidor** (`esp32/`, `server/`, `frontend/`, `simulator/` intocados). -- Comandos aceitos: `CMD:PECA:` + uma letra (A, B ou C) e `CMD:RESET`. Serial a **9600 baud**, linhas terminadas em `\r\n`. -- Pinos inalterados: motores A=9, B=10, C=11; topo A0/A1/A2; junção J1=A3, J2=D2, J3=D4; LCD I²C (A4/A5). -- Estoque inicial **15**, sem persistência; o `inicio` informa a causa do reset. -- Débito **somente** na confirmação válida da junção da esteira pedida. -- Parâmetros por esteira (padrão A, B e C): PWM 200, kick 150 ms, partida 3000 ms, timeout 12500 ms, saída 3000 ms, pulso mínimo da junção 20 ms. -- RAM global do Uno **≤ 65%** (verificado na compilação). Arquivos de `src/logica/` **não** incluem `Arduino.h`. -- Idioma de código, comentários, mensagens e documentação: **português do Brasil**. -- Trailer de todo commit: `Co-Authored-By: Claude Opus 5.5 `. -- Não colar cercas de markdown (três crases) em arquivos que não sejam `.md`. -- `core.autocrlf=true`: o aviso "LF will be replaced by CRLF" é esperado. -- Nunca matar processos que você não iniciou. - -## Como rodar as verificações - -- **Testes da lógica (PC):** Docker Desktop precisa estar aberto. Na raiz do repositório: - -```bash -MSYS_NO_PATHCONV=1 docker run --rm -v "$(cygpath -w "$PWD"):/w" -w /w gcc:14 make -C test/firmware_uno test -``` - -- **Compilação do Uno:** o `arduino-cli` vem com a Arduino IDE nesta máquina: - -```bash -CLI="/c/Users/matheusn/AppData/Local/Programs/Arduino IDE/resources/app/lib/backend/resources/arduino-cli.exe" -"$CLI" compile --fqbn arduino:avr:uno --warnings all arduino/data_flow_inventory -``` - -## Mapa de arquivos - -| Arquivo | Tarefa | Responsabilidade | -|---|---|---| -| `test/firmware_uno/teste.h`, `principal.cpp`, `Makefile` | 1 | mini-framework de testes e build no PC | -| `src/logica/texto_flash.h` | 1 | `TXT()`/`lerTxt()`: textos na flash do AVR | -| `src/logica/protocolo.h/.cpp` + `teste_protocolo.cpp` | 1 | comandos e mensagens JSON (contrato v2.x) | -| `src/logica/filtro_entrada.h/.cpp`, `causa_reset.h/.cpp` + `teste_filtro_reset.cpp` | 2 | debounce por tempo; causa do reset | -| `src/logica/supervisor_entrega.h/.cpp` + `teste_supervisor.cpp` | 3 | marcos M1/M2/M3 | -| `src/logica/fsm.h/.cpp` + `teste_fsm.cpp` | 4 | 5 estados, comandos, estoque, eventos | -| `src/logica/telas.h/.cpp` + `teste_telas.cpp` | 5 | textos do LCD 16x2 | -| `config.h`, `src/hw/*`, `data_flow_inventory.ino`, CI | 6 | hardware, troca do firmware, CI | -| `docs/...` | 7 | documentação e roteiro de bancada | -| — | 8 | verificação final | - -Caminhos `src/...` acima são relativos a `arduino/data_flow_inventory/`. - ---- - -### Tarefa 1: Infra de testes no PC e protocolo serial (contrato v2.x) - -**Arquivos:** -- Criar: `test/firmware_uno/teste.h`, `test/firmware_uno/principal.cpp`, `test/firmware_uno/Makefile` -- Criar: `arduino/data_flow_inventory/src/logica/texto_flash.h`, `protocolo.h`, `protocolo.cpp` -- Teste: `test/firmware_uno/teste_protocolo.cpp` -- Modificar: `.gitignore` (binário dos testes), `.github/workflows/lint-and-security.yaml` (job `firmware-logica`) - -**Interfaces:** -- Produz: `ComandoTipo interpretarComando(const char* linha, char* peca)`; `LeitorLinha` + `leitorIniciar`/`leitorAlimentar`; `formatarStatus/Estoque/Sensores/Esteiras/Pedido/Entrega/Erro/Inicio(char* buf, size_t cap, ...)` (devolvem o tamanho ou 0); macro `TXT(s)` e `lerTxt(p)`. Textos fixos passados às funções `formatar*` são `TXT(...)`. - -**Contexto:** o firmware atual (v2.x, `arduino/data_flow_inventory/data_flow_inventory.ino`) publica JSON com ArduinoJson. Os testes desta tarefa fixam o formato exato dessas mensagens; o novo código monta o JSON à mão (sem printf, sem ArduinoJson) e precisa reproduzi-lo byte a byte. O `.ino` atual **não muda** nesta tarefa (a troca é na Tarefa 6). - -- [ ] **Passo 1: Mini-framework e build** - -Crie `test/firmware_uno/teste.h` com exatamente este conteúdo: - -```cpp -// Mini-framework de testes do firmware (sem dependências externas). -#pragma once -#include -#include - -namespace teste { -inline int& falhas() { static int n = 0; return n; } -inline int& total() { static int n = 0; return n; } -typedef void (*FuncaoTeste)(); -struct Registro { const char* nome; FuncaoTeste f; Registro* prox; }; -inline Registro*& lista() { static Registro* r = nullptr; return r; } -struct Registrar { - Registro r; - Registrar(const char* nome, FuncaoTeste f) { - r.nome = nome; r.f = f; r.prox = nullptr; - Registro** p = &lista(); - while (*p) p = &(*p)->prox; // mantém a ordem de declaração - *p = &r; - } -}; -} // namespace teste - -#define TESTE(nome) \ - static void nome(); \ - static teste::Registrar registro_##nome(#nome, nome); \ - static void nome() - -#define VERIFICA(cond) \ - do { \ - teste::total()++; \ - if (!(cond)) { \ - teste::falhas()++; \ - printf(" FALHA %s:%d: %s\n", __FILE__, __LINE__, #cond); \ - } \ - } while (0) - -#define VERIFICA_TEXTO(a, b) \ - do { \ - teste::total()++; \ - if (strcmp((a), (b)) != 0) { \ - teste::falhas()++; \ - printf(" FALHA %s:%d:\n obtido: %s\n esperado: %s\n", \ - __FILE__, __LINE__, (a), (b)); \ - } \ - } while (0) -``` - -Crie `test/firmware_uno/principal.cpp` com exatamente este conteúdo: - -```cpp -// Executor dos testes do firmware: roda todos os TESTE(...) registrados. -#include "teste.h" - -int main() { - int nTestes = 0; - for (teste::Registro* r = teste::lista(); r; r = r->prox) { - int antes = teste::falhas(); - r->f(); - nTestes++; - printf("%s %s\n", teste::falhas() == antes ? "ok " : "FALHOU", r->nome); - } - printf("\n%d testes, %d verificacoes, %d falhas\n", nTestes, teste::total(), teste::falhas()); - return teste::falhas() == 0 ? 0 : 1; -} -``` - -Crie `test/firmware_uno/Makefile` com exatamente este conteúdo: - -```make -# Testes da lógica pura do firmware do Uno (compila no PC com g++). -# Uso: make -C test/firmware_uno test -LOGICA := ../../arduino/data_flow_inventory/src/logica -CXX ?= g++ -CXXFLAGS := -std=c++11 -Wall -Wextra -Werror -O1 -I$(LOGICA) - -FONTES_LOGICA := $(wildcard $(LOGICA)/*.cpp) -FONTES_TESTE := principal.cpp $(wildcard teste_*.cpp) - -.PHONY: test clean -test: testes - ./testes - -testes: $(FONTES_LOGICA) $(FONTES_TESTE) teste.h $(wildcard $(LOGICA)/*.h) - $(CXX) $(CXXFLAGS) -o $@ $(FONTES_TESTE) $(FONTES_LOGICA) - -clean: - rm -f testes -``` - -Acrescente ao final do `.gitignore`: - -``` -# Binário dos testes da lógica do firmware (test/firmware_uno) -test/firmware_uno/testes -``` - -- [ ] **Passo 2: Escrever os testes de contrato (falham: o módulo não existe)** - -Crie `test/firmware_uno/teste_protocolo.cpp` com exatamente este conteúdo: - -```cpp -// Contrato do protocolo serial. As strings "esperado" reproduzem byte a -// byte o que o firmware v2.x (ArduinoJson) publicava: se um destes testes -// quebrar, o ESP32/servidor/dashboard podem deixar de entender o Uno. -#include "protocolo.h" -#include "teste.h" - -static char buf[160]; // mesmo tamanho do buffer do firmware (src/hw/comunicacao.cpp) - -// ---- Mensagens existentes (formato v2.x, sem campos novos) ---------------- - -TESTE(status_igual_v2) { - formatarStatus(buf, sizeof buf, "AGUARDANDO_PEDIDO", 0, 42, -1); - VERIFICA_TEXTO(buf, "{\"type\":\"status\",\"estado\":\"AGUARDANDO_PEDIDO\",\"pecaSolicitada\":0,\"uptime\":42}"); -} - -TESTE(estoque_igual_v2) { - const int est[3] = {15, 14, 0}; - formatarEstoque(buf, sizeof buf, est); - VERIFICA_TEXTO(buf, "{\"type\":\"estoque\",\"pecaA\":15,\"pecaB\":14,\"pecaC\":0}"); -} - -TESTE(sensores_igual_v2) { - const bool topo[3] = {true, false, true}; - const bool jun[3] = {false, true, false}; - formatarSensores(buf, sizeof buf, topo, jun); - VERIFICA_TEXTO(buf, "{\"type\":\"sensores\",\"topo\":{\"A\":1,\"B\":0,\"C\":1},\"juncao\":{\"J1\":0,\"J2\":1,\"J3\":0}}"); -} - -TESTE(esteiras_igual_v2) { - const bool sec[3] = {false, true, false}; - formatarEsteiras(buf, sizeof buf, sec); - VERIFICA_TEXTO(buf, "{\"type\":\"esteiras\",\"principal\":1,\"secA\":0,\"secB\":1,\"secC\":0}"); -} - -TESTE(pedido_igual_v2) { - formatarPedido(buf, sizeof buf, 'B'); - VERIFICA_TEXTO(buf, "{\"type\":\"evento\",\"evento\":\"pedido\",\"peca\":\"B\"}"); -} - -TESTE(entrega_igual_v2) { - const int est[3] = {15, 14, 15}; - formatarEntrega(buf, sizeof buf, 'B', est); - VERIFICA_TEXTO(buf, "{\"type\":\"evento\",\"evento\":\"entrega\",\"peca\":\"B\",\"estoqueA\":15,\"estoqueB\":14,\"estoqueC\":15}"); -} - -TESTE(erro_igual_v2_com_e_sem_peca) { - formatarErro(buf, sizeof buf, "ocupado", 'C', nullptr, -1, -1); - VERIFICA_TEXTO(buf, "{\"type\":\"evento\",\"evento\":\"erro\",\"tipo\":\"ocupado\",\"peca\":\"C\"}"); - formatarErro(buf, sizeof buf, "peca_invalida", 0, nullptr, -1, -1); - VERIFICA_TEXTO(buf, "{\"type\":\"evento\",\"evento\":\"erro\",\"tipo\":\"peca_invalida\"}"); -} - -TESTE(inicio_igual_v2_sem_campos_novos) { - formatarInicio(buf, sizeof buf, "2.1", nullptr, -1, -1); - VERIFICA_TEXTO(buf, "{\"type\":\"evento\",\"evento\":\"inicio\",\"msg\":\"Sistema iniciado\",\"versao\":\"2.1\",\"driver\":\"IRF520\"}"); -} - -// ---- Campos novos: sempre DEPOIS dos atuais e só quando informados --------- - -TESTE(status_com_ram_livre) { - formatarStatus(buf, sizeof buf, "ERRO", 2, 4294967295UL, 812); - VERIFICA_TEXTO(buf, "{\"type\":\"status\",\"estado\":\"ERRO\",\"pecaSolicitada\":2,\"uptime\":4294967295,\"ram_livre\":812}"); -} - -TESTE(erro_com_diagnostico) { - formatarErro(buf, sizeof buf, "timeout", 'B', "transito", 12503, 0); - VERIFICA_TEXTO(buf, "{\"type\":\"evento\",\"evento\":\"erro\",\"tipo\":\"timeout\",\"peca\":\"B\",\"fase\":\"transito\",\"t_ms\":12503,\"bordas_juncao\":0}"); -} - -TESTE(inicio_com_campos_novos) { - formatarInicio(buf, sizeof buf, "3.0", "watchdog", 0, 640); - VERIFICA_TEXTO(buf, "{\"type\":\"evento\",\"evento\":\"inicio\",\"msg\":\"Sistema iniciado\",\"versao\":\"3.0\",\"driver\":\"IRF520\",\"reset\":\"watchdog\",\"lcd\":false,\"ram_livre\":640}"); -} - -TESTE(maiores_mensagens_cabem_no_buffer_de_160_bytes) { - const bool t[3] = {true, true, true}; - VERIFICA(formatarSensores(buf, sizeof buf, t, t) > 0); - VERIFICA(formatarErro(buf, sizeof buf, "peca_presa_saida", 'C', "transito", 2147483647L, 255) > 0); - VERIFICA(formatarInicio(buf, sizeof buf, "3.0", "reinicio", 1, 2048) > 0); - VERIFICA(strlen(buf) < 160); -} - -TESTE(buffer_pequeno_devolve_zero_e_string_vazia) { - char pequeno[10]; - VERIFICA(formatarPedido(pequeno, sizeof pequeno, 'A') == 0); - VERIFICA_TEXTO(pequeno, ""); -} - -// ---- Comandos ------------------------------------------------------------------- - -TESTE(comandos_validos) { - char p; - VERIFICA(interpretarComando("CMD:PECA:A", &p) == CMD_PECA && p == 'A'); - VERIFICA(interpretarComando("CMD:PECA:C", &p) == CMD_PECA && p == 'C'); - VERIFICA(interpretarComando(" CMD:PECA:B \r", &p) == CMD_PECA && p == 'B'); - VERIFICA(interpretarComando("CMD:RESET", &p) == CMD_RESET && p == 0); -} - -TESTE(comandos_invalidos) { - char p; - VERIFICA(interpretarComando("", &p) == CMD_NENHUM); - VERIFICA(interpretarComando(" ", &p) == CMD_NENHUM); - VERIFICA(interpretarComando("CMD:PECA:", &p) == CMD_PECA_INVALIDA); - VERIFICA(interpretarComando("CMD:PECA:Z", &p) == CMD_PECA_INVALIDA && p == 0); - VERIFICA(interpretarComando("CMD:PECA:AB", &p) == CMD_PECA_INVALIDA); - VERIFICA(interpretarComando("CMD:PECA:a", &p) == CMD_PECA_INVALIDA); - VERIFICA(interpretarComando("CMD:RESETX", &p) == CMD_DESCONHECIDO); - VERIFICA(interpretarComando("{\"type\":\"x\"}", &p) == CMD_DESCONHECIDO); -} - -TESTE(leitor_monta_linhas_e_ignora_cr) { - LeitorLinha l; - leitorIniciar(l); - const char* entrada = "CMD:PE"; - for (const char* c = entrada; *c; c++) VERIFICA(!leitorAlimentar(l, *c)); - const char* resto = "CA:A\r\n"; - bool pronto = false; - for (const char* c = resto; *c; c++) pronto = leitorAlimentar(l, *c); - VERIFICA(pronto); - VERIFICA_TEXTO(l.buf, "CMD:PECA:A"); -} - -TESTE(leitor_descarta_linha_longa_inteira) { - LeitorLinha l; - leitorIniciar(l); - for (int i = 0; i < 50; i++) VERIFICA(!leitorAlimentar(l, 'x')); - VERIFICA(!leitorAlimentar(l, '\n')); // a linha longa some inteira - const char* prox = "CMD:RESET\n"; - bool pronto = false; - for (const char* c = prox; *c; c++) pronto = leitorAlimentar(l, *c); - VERIFICA(pronto); - VERIFICA_TEXTO(l.buf, "CMD:RESET"); -} -``` - -- [ ] **Passo 3: Rodar e ver falhar** - -Rode (na raiz do repositório, Git Bash): - -```bash -MSYS_NO_PATHCONV=1 docker run --rm -v "$(cygpath -w "$PWD"):/w" -w /w gcc:14 make -C test/firmware_uno test -``` - -Esperado: erro de compilação `protocolo.h: No such file or directory`. - -- [ ] **Passo 4: Implementar** - -Crie `arduino/data_flow_inventory/src/logica/texto_flash.h` com exatamente este conteúdo: - -```cpp -// ============================================================ -// DATA FLOW INVENTORY — Textos fixos na memória flash -// ------------------------------------------------------------ -// No AVR, um literal "..." ocupa RAM (o Uno só tem 2 KB). TXT("...") -// grava o texto na flash (PROGMEM) e lerTxt() lê um caractere dele. -// No PC (testes) os dois viram o comportamento normal de C++. -// Convenção: todo `const char*` de texto FIXO nas APIs de src/logica -// é um TXT (flash no AVR). Textos montados em RAM (buffers) não são. -// ============================================================ -#pragma once - -#if defined(__AVR__) -#include -#define TXT(s) PSTR(s) -inline char lerTxt(const char* p) { return (char)pgm_read_byte(p); } -#else -#define TXT(s) (s) -inline char lerTxt(const char* p) { return *p; } -#endif -``` - -Crie `arduino/data_flow_inventory/src/logica/protocolo.h` com exatamente este conteúdo: - -```cpp -// ============================================================ -// DATA FLOW INVENTORY — Protocolo serial Uno ↔ ESP32 (lógica pura) -// ------------------------------------------------------------ -// Interpreta comandos (CMD:PECA:X, CMD:RESET) e formata as mensagens -// JSON publicadas pelo Uno. C++ puro, sem Arduino.h: testado no PC -// (test/firmware_uno). O formato de cada mensagem é idêntico ao do -// firmware v2.x (ArduinoJson): mesma ordem de campos, sem espaços. -// Campos novos são sempre OPCIONAIS (só saem quando informados). -// Textos fixos passados às funções formatar* (estado, tipo, fase, versão, -// reset) devem ser TXT(...) — ver texto_flash.h. -// ============================================================ -#pragma once -#include -#include - -// ---- Comandos ----------------------------------------------------------- -enum ComandoTipo : uint8_t { - CMD_NENHUM = 0, // linha vazia - CMD_PECA, // CMD:PECA:A|B|C (peça válida em *peca) - CMD_PECA_INVALIDA, // CMD:PECA: com letra/forma inválida - CMD_RESET, // CMD:RESET - CMD_DESCONHECIDO // qualquer outra linha -}; - -// Interpreta uma linha (sem '\n'). Ignora espaços nas pontas. -// Em CMD_PECA, *peca recebe 'A', 'B' ou 'C'; nos demais casos, 0. -ComandoTipo interpretarComando(const char* linha, char* peca); - -// Acumula bytes da serial até '\n'. '\r' é ignorado. Linha maior que -// a capacidade é DESCARTADA inteira (até o próximo '\n'). -struct LeitorLinha { - static const uint8_t CAPACIDADE = 32; - char buf[CAPACIDADE + 1]; - uint8_t n; - bool descartando; -}; -void leitorIniciar(LeitorLinha& l); -// Devolve true quando uma linha completa está em l.buf (terminada em 0). -bool leitorAlimentar(LeitorLinha& l, char c); - -// ---- Mensagens -------------------------------------------------------------- -// Todas escrevem em buf (capacidade cap), terminam em 0 e devolvem o -// tamanho escrito (sem o 0), ou 0 se não couber. -// Valores "opcionais": passe -1 (inteiros) ou nullptr (texto) para omitir. -size_t formatarStatus(char* buf, size_t cap, const char* estado, - int pecaSolicitada, uint32_t uptimeS, long ramLivre); -size_t formatarEstoque(char* buf, size_t cap, const int estoque[3]); -size_t formatarSensores(char* buf, size_t cap, const bool topo[3], - const bool juncao[3]); -size_t formatarEsteiras(char* buf, size_t cap, const bool secundaria[3]); -size_t formatarPedido(char* buf, size_t cap, char peca); -size_t formatarEntrega(char* buf, size_t cap, char peca, const int estoque[3]); -// peca = 0 omite o campo; fase = nullptr omite; tMs/bordas = -1 omitem. -size_t formatarErro(char* buf, size_t cap, const char* tipo, char peca, - const char* fase, long tMs, long bordas); -// reset = nullptr omite; lcd = -1 omite (0/1 → false/true); ramLivre = -1 omite. -size_t formatarInicio(char* buf, size_t cap, const char* versao, - const char* reset, int lcd, long ramLivre); -``` - -Crie `arduino/data_flow_inventory/src/logica/protocolo.cpp` com exatamente este conteúdo: - -```cpp -#include "protocolo.h" - -#include - -#include "texto_flash.h" - -// ---- Comandos ----------------------------------------------------------- - -static bool ehEspaco(char c) { return c == ' ' || c == '\t' || c == '\r' || c == '\n'; } - -// ram[0..n) é igual ao texto TXT (inteiro)? -static bool igualTxt(const char* ram, size_t n, const char* txt) { - size_t i = 0; - for (; i < n; i++) { - char c = lerTxt(txt + i); - if (c == 0 || c != ram[i]) return false; - } - return lerTxt(txt + i) == 0; -} - -ComandoTipo interpretarComando(const char* linha, char* peca) { - *peca = 0; - const char* ini = linha; - while (*ini && ehEspaco(*ini)) ini++; - size_t n = strlen(ini); - while (n > 0 && ehEspaco(ini[n - 1])) n--; - if (n == 0) return CMD_NENHUM; - - const size_t TAM_PREFIXO = 9; // "CMD:PECA:" - if (n >= TAM_PREFIXO && igualTxt(ini, TAM_PREFIXO, TXT("CMD:PECA:"))) { - if (n == TAM_PREFIXO + 1) { - char c = ini[TAM_PREFIXO]; - if (c == 'A' || c == 'B' || c == 'C') { - *peca = c; - return CMD_PECA; - } - } - return CMD_PECA_INVALIDA; - } - if (igualTxt(ini, n, TXT("CMD:RESET"))) return CMD_RESET; - return CMD_DESCONHECIDO; -} - -void leitorIniciar(LeitorLinha& l) { - l.n = 0; - l.buf[0] = 0; - l.descartando = false; -} - -bool leitorAlimentar(LeitorLinha& l, char c) { - if (c == '\r') return false; - if (c == '\n') { - if (l.descartando) { - leitorIniciar(l); - return false; - } - l.buf[l.n] = 0; - l.n = 0; - return true; - } - if (l.descartando) return false; - if (l.n >= LeitorLinha::CAPACIDADE) { - l.descartando = true; - l.n = 0; - l.buf[0] = 0; - return false; - } - l.buf[l.n++] = c; - return false; -} - -// ---- Escritor de JSON (sem printf: menos flash no AVR) --------------------- - -namespace { -struct Escritor { - char* b; - size_t cap; - size_t n; - bool ok; -}; - -void iniciar(Escritor& e, char* b, size_t cap) { - e.b = b; - e.cap = cap; - e.n = 0; - e.ok = cap > 0; - if (e.ok) b[0] = 0; -} - -void car(Escritor& e, char c) { - if (!e.ok) return; - if (e.n + 1 >= e.cap) { e.ok = false; return; } - e.b[e.n++] = c; - e.b[e.n] = 0; -} - -// Texto fixo (TXT: flash no AVR). -void txt(Escritor& e, const char* s) { - for (char c = lerTxt(s); c != 0; c = lerTxt(++s)) car(e, c); -} - -void num(Escritor& e, long v) { - char tmp[12]; - int i = 0; - bool neg = v < 0; - unsigned long u = neg ? (unsigned long)(-(v + 1)) + 1UL : (unsigned long)v; - do { tmp[i++] = (char)('0' + (u % 10)); u /= 10; } while (u > 0); - if (neg) car(e, '-'); - while (i > 0) car(e, tmp[--i]); -} - -void numU(Escritor& e, uint32_t v) { - char tmp[11]; - int i = 0; - do { tmp[i++] = (char)('0' + (v % 10)); v /= 10; } while (v > 0); - while (i > 0) car(e, tmp[--i]); -} - -// ,"chave": (k é TXT) -void chave(Escritor& e, const char* k, bool primeira = false) { - if (!primeira) car(e, ','); - car(e, '"'); - txt(e, k); - car(e, '"'); - car(e, ':'); -} - -void campoTexto(Escritor& e, const char* k, const char* v, bool primeira = false) { - chave(e, k, primeira); - car(e, '"'); - txt(e, v); - car(e, '"'); -} - -void campoLetra(Escritor& e, const char* k, char v) { - chave(e, k); - car(e, '"'); - car(e, v); - car(e, '"'); -} - -void campoNum(Escritor& e, const char* k, long v) { - chave(e, k); - num(e, v); -} - -size_t fim(Escritor& e) { - car(e, '}'); - if (!e.ok) { - if (e.cap > 0) e.b[0] = 0; - return 0; - } - return e.n; -} - -void inicioMsg(Escritor& e, char* buf, size_t cap, const char* type) { - iniciar(e, buf, cap); - car(e, '{'); - campoTexto(e, TXT("type"), type, true); -} -} // namespace - -// ---- Mensagens -------------------------------------------------------------- - -size_t formatarStatus(char* buf, size_t cap, const char* estado, - int pecaSolicitada, uint32_t uptimeS, long ramLivre) { - Escritor e; - inicioMsg(e, buf, cap, TXT("status")); - campoTexto(e, TXT("estado"), estado); - campoNum(e, TXT("pecaSolicitada"), pecaSolicitada); - chave(e, TXT("uptime")); - numU(e, uptimeS); - if (ramLivre >= 0) campoNum(e, TXT("ram_livre"), ramLivre); - return fim(e); -} - -size_t formatarEstoque(char* buf, size_t cap, const int estoque[3]) { - Escritor e; - inicioMsg(e, buf, cap, TXT("estoque")); - campoNum(e, TXT("pecaA"), estoque[0]); - campoNum(e, TXT("pecaB"), estoque[1]); - campoNum(e, TXT("pecaC"), estoque[2]); - return fim(e); -} - -size_t formatarSensores(char* buf, size_t cap, const bool topo[3], - const bool juncao[3]) { - Escritor e; - inicioMsg(e, buf, cap, TXT("sensores")); - chave(e, TXT("topo")); - car(e, '{'); - chave(e, TXT("A"), true); num(e, topo[0] ? 1 : 0); - chave(e, TXT("B")); num(e, topo[1] ? 1 : 0); - chave(e, TXT("C")); num(e, topo[2] ? 1 : 0); - car(e, '}'); - chave(e, TXT("juncao")); - car(e, '{'); - chave(e, TXT("J1"), true); num(e, juncao[0] ? 1 : 0); - chave(e, TXT("J2")); num(e, juncao[1] ? 1 : 0); - chave(e, TXT("J3")); num(e, juncao[2] ? 1 : 0); - car(e, '}'); - return fim(e); -} - -size_t formatarEsteiras(char* buf, size_t cap, const bool secundaria[3]) { - Escritor e; - inicioMsg(e, buf, cap, TXT("esteiras")); - campoNum(e, TXT("principal"), 1); // sempre ligada (direto na fonte) - campoNum(e, TXT("secA"), secundaria[0] ? 1 : 0); - campoNum(e, TXT("secB"), secundaria[1] ? 1 : 0); - campoNum(e, TXT("secC"), secundaria[2] ? 1 : 0); - return fim(e); -} - -size_t formatarPedido(char* buf, size_t cap, char peca) { - Escritor e; - inicioMsg(e, buf, cap, TXT("evento")); - campoTexto(e, TXT("evento"), TXT("pedido")); - campoLetra(e, TXT("peca"), peca); - return fim(e); -} - -size_t formatarEntrega(char* buf, size_t cap, char peca, const int estoque[3]) { - Escritor e; - inicioMsg(e, buf, cap, TXT("evento")); - campoTexto(e, TXT("evento"), TXT("entrega")); - campoLetra(e, TXT("peca"), peca); - campoNum(e, TXT("estoqueA"), estoque[0]); - campoNum(e, TXT("estoqueB"), estoque[1]); - campoNum(e, TXT("estoqueC"), estoque[2]); - return fim(e); -} - -size_t formatarErro(char* buf, size_t cap, const char* tipo, char peca, - const char* fase, long tMs, long bordas) { - Escritor e; - inicioMsg(e, buf, cap, TXT("evento")); - campoTexto(e, TXT("evento"), TXT("erro")); - campoTexto(e, TXT("tipo"), tipo); - if (peca != 0) campoLetra(e, TXT("peca"), peca); - if (fase != nullptr) campoTexto(e, TXT("fase"), fase); - if (tMs >= 0) campoNum(e, TXT("t_ms"), tMs); - if (bordas >= 0) campoNum(e, TXT("bordas_juncao"), bordas); - return fim(e); -} - -size_t formatarInicio(char* buf, size_t cap, const char* versao, - const char* reset, int lcd, long ramLivre) { - Escritor e; - inicioMsg(e, buf, cap, TXT("evento")); - campoTexto(e, TXT("evento"), TXT("inicio")); - campoTexto(e, TXT("msg"), TXT("Sistema iniciado")); - campoTexto(e, TXT("versao"), versao); - campoTexto(e, TXT("driver"), TXT("IRF520")); - if (reset != nullptr) campoTexto(e, TXT("reset"), reset); - if (lcd >= 0) { - chave(e, TXT("lcd")); - txt(e, lcd ? TXT("true") : TXT("false")); - } - if (ramLivre >= 0) campoNum(e, TXT("ram_livre"), ramLivre); - return fim(e); -} -``` - -- [ ] **Passo 5: Rodar e ver passar** - -Rode (na raiz do repositório, Git Bash): - -```bash -MSYS_NO_PATHCONV=1 docker run --rm -v "$(cygpath -w "$PWD"):/w" -w /w gcc:14 make -C test/firmware_uno test -``` - -Esperado: `17 testes, ... 0 falhas` (os testes `*_igual_v2` provam que o formato não mudou). - -- [ ] **Passo 6: Job de CI** - -Em `.github/workflows/lint-and-security.yaml`, acrescente este job ao final da seção `jobs:` (mesma indentação dos outros jobs; não altere os existentes): - -```yaml - firmware-logica: - runs-on: ubuntu-latest - steps: - - name: Checkout código - uses: actions/checkout@v4 - - - name: Testes da lógica do firmware do Uno (g++ no PC) - run: make -C test/firmware_uno test -``` - -- [ ] **Passo 7: Commit** - -```bash -git add test/firmware_uno arduino/data_flow_inventory/src/logica .gitignore .github/workflows/lint-and-security.yaml -git commit -m "test(firmware): protocolo serial em logica pura com contrato do formato v2.x - -Co-Authored-By: Claude Opus 5.5 " -``` - ---- - -### Tarefa 2: Filtro de entrada e causa do reset - -**Arquivos:** -- Criar: `arduino/data_flow_inventory/src/logica/filtro_entrada.h/.cpp`, `causa_reset.h/.cpp` -- Teste: `test/firmware_uno/teste_filtro_reset.cpp` - -**Interfaces:** -- Consome: `TXT()` (Tarefa 1). -- Produz: `struct FiltroEntrada { bool estavel; bool bruto; uint32_t mudouEm; uint16_t janelaMs; }`, `enum Borda`, `filtroIniciar(f, valor, agora, janelaMs)`, `Borda filtroAtualizar(f, bruto, agora)`; constantes `MCUSR_PORF/EXTRF/BORF/WDRF` e `const char* causaReset(uint8_t mcusr, bool assinaturaValida, bool marcaWatchdog)` (devolve TXT). - -- [ ] **Passo 1: Escrever os testes (falham)** - -Crie `test/firmware_uno/teste_filtro_reset.cpp` com exatamente este conteúdo: - -```cpp -// Filtro de entrada (debounce) e causa do reset. -#include "causa_reset.h" -#include "filtro_entrada.h" -#include "teste.h" - -// ---- Filtro ------------------------------------------------------------------ - -TESTE(filtro_ignora_pulso_curto) { - FiltroEntrada f; - filtroIniciar(f, false, 0, 20); - VERIFICA(filtroAtualizar(f, true, 100) == BORDA_NENHUMA); - VERIFICA(filtroAtualizar(f, true, 110) == BORDA_NENHUMA); // 10 ms - VERIFICA(filtroAtualizar(f, false, 115) == BORDA_NENHUMA); // voltou - VERIFICA(filtroAtualizar(f, false, 200) == BORDA_NENHUMA); - VERIFICA(!f.estavel); -} - -TESTE(filtro_aceita_nivel_estavel_e_gera_bordas) { - FiltroEntrada f; - filtroIniciar(f, false, 0, 20); - filtroAtualizar(f, true, 100); - VERIFICA(filtroAtualizar(f, true, 120) == BORDA_OCUPOU); - VERIFICA(f.estavel); - filtroAtualizar(f, false, 300); - VERIFICA(filtroAtualizar(f, false, 330) == BORDA_LIBEROU); -} - -TESTE(filtro_funciona_no_estouro_do_millis) { - FiltroEntrada f; - const uint32_t quase = 0xFFFFFFF0UL; - filtroIniciar(f, false, quase, 20); - filtroAtualizar(f, true, quase + 5); - VERIFICA(filtroAtualizar(f, true, quase + 30) == BORDA_OCUPOU); // passou de 0 -} - -// ---- Causa do reset ------------------------------------------------------------ - -TESTE(causa_reset) { - VERIFICA_TEXTO(causaReset(MCUSR_WDRF, false, false), "watchdog"); - VERIFICA_TEXTO(causaReset(0, true, true), "watchdog"); - VERIFICA_TEXTO(causaReset(MCUSR_BORF, true, false), "brownout"); - VERIFICA_TEXTO(causaReset(MCUSR_PORF, true, false), "energia"); - VERIFICA_TEXTO(causaReset(0, false, false), "energia"); - VERIFICA_TEXTO(causaReset(0, true, false), "reinicio"); - VERIFICA_TEXTO(causaReset(MCUSR_EXTRF, true, false), "reinicio"); - VERIFICA_TEXTO(causaReset(0, false, true), "energia"); // marca sem assinatura não vale -} -``` - -- [ ] **Passo 2: Rodar e ver falhar** - -Rode (na raiz do repositório, Git Bash): - -```bash -MSYS_NO_PATHCONV=1 docker run --rm -v "$(cygpath -w "$PWD"):/w" -w /w gcc:14 make -C test/firmware_uno test -``` - -Esperado: erro de compilação (`causa_reset.h`/`filtro_entrada.h` não existem). - -- [ ] **Passo 3: Implementar** - -Crie `arduino/data_flow_inventory/src/logica/filtro_entrada.h` com exatamente este conteúdo: - -```cpp -// ============================================================ -// DATA FLOW INVENTORY — Filtro de entrada digital (lógica pura) -// ------------------------------------------------------------ -// Debounce por tempo: a leitura bruta só vira "estável" depois de -// permanecer igual por janelaMs. Usado nos sensores de topo e para o -// nível atual das junções. C++ puro (testado no PC). -// ============================================================ -#pragma once -#include - -struct FiltroEntrada { - bool estavel; // valor filtrado (true = sensor ocupado) - bool bruto; // última leitura bruta - uint32_t mudouEm; // instante da última mudança da leitura bruta - uint16_t janelaMs; // tempo mínimo de estabilidade -}; - -enum Borda : int8_t { BORDA_NENHUMA = 0, BORDA_OCUPOU = 1, BORDA_LIBEROU = -1 }; - -void filtroIniciar(FiltroEntrada& f, bool valor, uint32_t agora, uint16_t janelaMs); - -// Atualiza com a leitura bruta atual e devolve a borda do valor ESTÁVEL -// (se houve). Robusto ao estouro de millis() (aritmética sem sinal). -Borda filtroAtualizar(FiltroEntrada& f, bool bruto, uint32_t agora); -``` - -Crie `arduino/data_flow_inventory/src/logica/filtro_entrada.cpp` com exatamente este conteúdo: - -```cpp -#include "filtro_entrada.h" - -void filtroIniciar(FiltroEntrada& f, bool valor, uint32_t agora, uint16_t janelaMs) { - f.estavel = valor; - f.bruto = valor; - f.mudouEm = agora; - f.janelaMs = janelaMs; -} - -Borda filtroAtualizar(FiltroEntrada& f, bool bruto, uint32_t agora) { - if (bruto != f.bruto) { - f.bruto = bruto; - f.mudouEm = agora; - return BORDA_NENHUMA; - } - if (bruto != f.estavel && (uint32_t)(agora - f.mudouEm) >= f.janelaMs) { - f.estavel = bruto; - return bruto ? BORDA_OCUPOU : BORDA_LIBEROU; - } - return BORDA_NENHUMA; -} -``` - -Crie `arduino/data_flow_inventory/src/logica/causa_reset.h` com exatamente este conteúdo: - -```cpp -// ============================================================ -// DATA FLOW INVENTORY — Causa do último reset (lógica pura) -// ------------------------------------------------------------ -// O Optiboot do Uno costuma zerar o MCUSR antes do sketch; por isso a -// causa combina: o MCUSR (quando vier preenchido), uma assinatura em -// RAM não inicializada (.noinit, some quando a energia cai) e a marca -// gravada pela interrupção do watchdog. C++ puro (testado no PC). -// ============================================================ -#pragma once -#include - -// Bits do MCUSR do ATmega328P. -static const uint8_t MCUSR_PORF = 0x01; // energização -static const uint8_t MCUSR_EXTRF = 0x02; // pino de reset -static const uint8_t MCUSR_BORF = 0x04; // brownout -static const uint8_t MCUSR_WDRF = 0x08; // watchdog - -// Devolve TXT("watchdog"), TXT("brownout"), TXT("energia") ou TXT("reinicio"). -const char* causaReset(uint8_t mcusr, bool assinaturaValida, bool marcaWatchdog); -``` - -Crie `arduino/data_flow_inventory/src/logica/causa_reset.cpp` com exatamente este conteúdo: - -```cpp -#include "causa_reset.h" - -#include "texto_flash.h" - -const char* causaReset(uint8_t mcusr, bool assinaturaValida, bool marcaWatchdog) { - if ((mcusr & MCUSR_WDRF) || (assinaturaValida && marcaWatchdog)) return TXT("watchdog"); - if (mcusr & MCUSR_BORF) return TXT("brownout"); - if ((mcusr & MCUSR_PORF) || !assinaturaValida) return TXT("energia"); - return TXT("reinicio"); -} -``` - -- [ ] **Passo 4: Rodar e ver passar** - -Rode (na raiz do repositório, Git Bash): - -```bash -MSYS_NO_PATHCONV=1 docker run --rm -v "$(cygpath -w "$PWD"):/w" -w /w gcc:14 make -C test/firmware_uno test -``` - -Esperado: `21 testes, ... 0 falhas`. - -- [ ] **Passo 5: Commit** - -```bash -git add test/firmware_uno arduino/data_flow_inventory/src/logica -git commit -m "feat(firmware): filtro de entrada e causa do reset em logica pura - -Co-Authored-By: Claude Opus 5.5 " -``` - ---- - -### Tarefa 3: Supervisor de entrega por marcos - -**Arquivos:** -- Criar: `arduino/data_flow_inventory/src/logica/supervisor_entrega.h/.cpp` -- Teste: `test/firmware_uno/teste_supervisor.cpp` - -**Interfaces:** -- Produz: `struct ParamEsteira { uint8_t pwmRegime; uint16_t kickMs, prazoPartidaMs, timeoutMs, saidaMs, pulsoMinJuncaoMs; }` (nesta ordem — o `config.h` da Tarefa 6 usa inicialização por chaves nessa ordem); `struct LeituraJuncao { bool ocupada; uint32_t ocupouEm; uint16_t maiorPulsoMs; uint8_t bordas; }`; `enum FaseEntrega { FASE_PARTIDA, FASE_TRANSITO, FASE_SAIDA, FASE_FIM }`; `enum ResultadoSupervisor { SUP_SEGUE, SUP_CONFIRMOU, SUP_SAIDA_OK, SUP_FALHA_SEM_AVANCO, SUP_FALHA_TIMEOUT, SUP_FALHA_PRESA_SAIDA }`; `struct Supervisor { ParamEsteira p; FaseEntrega fase; uint32_t inicioMs; uint32_t confirmouEm; }`; `supervisorIniciar(s, p, agora)`; `ResultadoSupervisor supervisorAtualizar(s, agora, topoOcupado, const LeituraJuncao& j)`; `bool passagemValida(j, agora, pulsoMinMs)`. - -**Regras (spec §4.1):** prazos contados da partida do motor com aritmética sem sinal; uma passagem válida na junção confirma mesmo antes de o topo liberar; o caso P1 (junção nunca muda) termina em `SUP_FALHA_TIMEOUT`; o P2 (topo não libera) em `SUP_FALHA_SEM_AVANCO`. - -- [ ] **Passo 1: Escrever os testes (falham)** - -Crie `test/firmware_uno/teste_supervisor.cpp` com exatamente este conteúdo: - -```cpp -// Supervisor de entrega por marcos (M1 partida, M2 trânsito, M3 saída). -#include "supervisor_entrega.h" -#include "teste.h" - -// ---- Supervisor --------------------------------------------------------------- - -static ParamEsteira paramPadrao() { - ParamEsteira p; - p.pwmRegime = 200; - p.kickMs = 150; - p.prazoPartidaMs = 3000; - p.timeoutMs = 12500; - p.saidaMs = 3000; - p.pulsoMinJuncaoMs = 20; - return p; -} - -static LeituraJuncao livre() { - LeituraJuncao j; - j.ocupada = false; - j.ocupouEm = 0; - j.maiorPulsoMs = 0; - j.bordas = 0; - return j; -} - -TESTE(supervisor_m1_motor_sem_avanco) { - Supervisor s; - supervisorIniciar(s, paramPadrao(), 1000); - LeituraJuncao j = livre(); - VERIFICA(supervisorAtualizar(s, 3999, true, j) == SUP_SEGUE); - VERIFICA(supervisorAtualizar(s, 4000, true, j) == SUP_FALHA_SEM_AVANCO); -} - -TESTE(supervisor_m2_timeout_sem_bordas) { - Supervisor s; - supervisorIniciar(s, paramPadrao(), 0); - LeituraJuncao j = livre(); - VERIFICA(supervisorAtualizar(s, 1000, false, j) == SUP_SEGUE); // topo liberou - VERIFICA(s.fase == FASE_TRANSITO); - VERIFICA(supervisorAtualizar(s, 12499, false, j) == SUP_SEGUE); - VERIFICA(supervisorAtualizar(s, 12500, false, j) == SUP_FALHA_TIMEOUT); -} - -TESTE(supervisor_confirma_por_pulso_completo_capturado) { - Supervisor s; - supervisorIniciar(s, paramPadrao(), 0); - LeituraJuncao j = livre(); - supervisorAtualizar(s, 1000, false, j); - j.maiorPulsoMs = 25; // a interrupção viu um pulso de 25 ms entre dois ciclos - j.bordas = 2; - VERIFICA(supervisorAtualizar(s, 5000, false, j) == SUP_CONFIRMOU); - VERIFICA(s.fase == FASE_SAIDA); -} - -TESTE(supervisor_ignora_pulso_curto_de_ruido) { - Supervisor s; - supervisorIniciar(s, paramPadrao(), 0); - LeituraJuncao j = livre(); - supervisorAtualizar(s, 1000, false, j); - j.maiorPulsoMs = 5; - j.bordas = 2; - VERIFICA(supervisorAtualizar(s, 2000, false, j) == SUP_SEGUE); - j.ocupada = true; - j.ocupouEm = 2000; - VERIFICA(supervisorAtualizar(s, 2010, false, j) == SUP_SEGUE); // 10 ms - VERIFICA(supervisorAtualizar(s, 2020, false, j) == SUP_CONFIRMOU); // 20 ms -} - -TESTE(supervisor_m3_saida_ok_e_peca_presa) { - Supervisor s; - supervisorIniciar(s, paramPadrao(), 0); - LeituraJuncao j = livre(); - j.maiorPulsoMs = 30; - VERIFICA(supervisorAtualizar(s, 4000, false, j) == SUP_CONFIRMOU); - VERIFICA(supervisorAtualizar(s, 6999, false, j) == SUP_SEGUE); - VERIFICA(supervisorAtualizar(s, 7000, false, j) == SUP_SAIDA_OK); - - supervisorIniciar(s, paramPadrao(), 0); - LeituraJuncao k = livre(); - k.ocupada = true; - k.ocupouEm = 4000; - VERIFICA(supervisorAtualizar(s, 4030, false, k) == SUP_CONFIRMOU); - VERIFICA(supervisorAtualizar(s, 7030, false, k) == SUP_FALHA_PRESA_SAIDA); -} - -TESTE(supervisor_timeout_conta_desde_a_partida_no_estouro_do_millis) { - Supervisor s; - const uint32_t ini = 0xFFFFF000UL; - supervisorIniciar(s, paramPadrao(), ini); - LeituraJuncao j = livre(); - supervisorAtualizar(s, ini + 500, false, j); - VERIFICA(supervisorAtualizar(s, ini + 12499, false, j) == SUP_SEGUE); - VERIFICA(supervisorAtualizar(s, ini + 12500, false, j) == SUP_FALHA_TIMEOUT); -} -``` - -- [ ] **Passo 2: Rodar e ver falhar** - -Rode (na raiz do repositório, Git Bash): - -```bash -MSYS_NO_PATHCONV=1 docker run --rm -v "$(cygpath -w "$PWD"):/w" -w /w gcc:14 make -C test/firmware_uno test -``` - -Esperado: erro de compilação (`supervisor_entrega.h` não existe). - -- [ ] **Passo 3: Implementar** - -Crie `arduino/data_flow_inventory/src/logica/supervisor_entrega.h` com exatamente este conteúdo: - -```cpp -// ============================================================ -// DATA FLOW INVENTORY — Supervisor de entrega por marcos (lógica pura) -// ------------------------------------------------------------ -// M1 Partida : o sensor de topo fica livre em até prazoPartidaMs. -// M2 Trânsito: a junção registra uma passagem válida (pulso ≥ -// pulsoMinJuncaoMs) em até timeoutMs desde a partida. -// M3 Saída : depois da confirmação, o motor segue saidaMs; no fim a -// junção precisa estar livre. -// Todos os prazos contam com aritmética sem sinal (estouro de millis). -// C++ puro (testado no PC). -// ============================================================ -#pragma once -#include - -struct ParamEsteira { - uint8_t pwmRegime; // PWM de regime (0-255) - uint16_t kickMs; // duração do kick-start em PWM 255 - uint16_t prazoPartidaMs; // M1 - uint16_t timeoutMs; // M2 (desde a partida) - uint16_t saidaMs; // M3 - uint16_t pulsoMinJuncaoMs; // largura mínima de uma passagem válida -}; - -// Leitura de um sensor de junção, montada pela camada de hardware a -// partir das bordas capturadas por interrupção. -struct LeituraJuncao { - bool ocupada; // nível atual (true = peça sobre o sensor) - uint32_t ocupouEm; // instante em que ficou ocupada (vale se ocupada) - uint16_t maiorPulsoMs; // maior pulso COMPLETO desde o último zerar - uint8_t bordas; // mudanças de nível desde o último zerar -}; - -enum FaseEntrega : uint8_t { FASE_PARTIDA, FASE_TRANSITO, FASE_SAIDA, FASE_FIM }; - -enum ResultadoSupervisor : uint8_t { - SUP_SEGUE = 0, - SUP_CONFIRMOU, // passagem válida na junção (debitar agora) - SUP_SAIDA_OK, // fim da saída com a junção livre - SUP_FALHA_SEM_AVANCO, // M1 - SUP_FALHA_TIMEOUT, // M2 - SUP_FALHA_PRESA_SAIDA // M3 -}; - -struct Supervisor { - ParamEsteira p; - FaseEntrega fase; - uint32_t inicioMs; - uint32_t confirmouEm; -}; - -void supervisorIniciar(Supervisor& s, const ParamEsteira& p, uint32_t agora); -ResultadoSupervisor supervisorAtualizar(Supervisor& s, uint32_t agora, - bool topoOcupado, const LeituraJuncao& j); -// Passagem válida: pulso completo largo o bastante ou ocupada há tempo suficiente. -bool passagemValida(const LeituraJuncao& j, uint32_t agora, uint16_t pulsoMinMs); -``` - -Crie `arduino/data_flow_inventory/src/logica/supervisor_entrega.cpp` com exatamente este conteúdo: - -```cpp -#include "supervisor_entrega.h" - -void supervisorIniciar(Supervisor& s, const ParamEsteira& p, uint32_t agora) { - s.p = p; - s.fase = FASE_PARTIDA; - s.inicioMs = agora; - s.confirmouEm = agora; -} - -bool passagemValida(const LeituraJuncao& j, uint32_t agora, uint16_t pulsoMinMs) { - if (j.maiorPulsoMs >= pulsoMinMs) return true; - return j.ocupada && (uint32_t)(agora - j.ocupouEm) >= pulsoMinMs; -} - -ResultadoSupervisor supervisorAtualizar(Supervisor& s, uint32_t agora, - bool topoOcupado, const LeituraJuncao& j) { - const uint32_t decorrido = (uint32_t)(agora - s.inicioMs); - - if (s.fase == FASE_PARTIDA || s.fase == FASE_TRANSITO) { - // Uma passagem válida prova o avanço mesmo antes de o filtro do topo liberar. - if (passagemValida(j, agora, s.p.pulsoMinJuncaoMs)) { - s.fase = FASE_SAIDA; - s.confirmouEm = agora; - return SUP_CONFIRMOU; - } - if (s.fase == FASE_PARTIDA) { - if (!topoOcupado) { - s.fase = FASE_TRANSITO; - } else if (decorrido >= s.p.prazoPartidaMs) { - s.fase = FASE_FIM; - return SUP_FALHA_SEM_AVANCO; - } - } - if (s.fase == FASE_TRANSITO && decorrido >= s.p.timeoutMs) { - s.fase = FASE_FIM; - return SUP_FALHA_TIMEOUT; - } - return SUP_SEGUE; - } - - if (s.fase == FASE_SAIDA) { - if ((uint32_t)(agora - s.confirmouEm) >= s.p.saidaMs) { - s.fase = FASE_FIM; - // "Presa" = ainda ocupada e há tempo suficiente para não ser ruído. - bool presa = j.ocupada && (uint32_t)(agora - j.ocupouEm) >= s.p.pulsoMinJuncaoMs; - return presa ? SUP_FALHA_PRESA_SAIDA : SUP_SAIDA_OK; - } - return SUP_SEGUE; - } - return SUP_SEGUE; -} -``` - -- [ ] **Passo 4: Rodar e ver passar** - -Rode (na raiz do repositório, Git Bash): - -```bash -MSYS_NO_PATHCONV=1 docker run --rm -v "$(cygpath -w "$PWD"):/w" -w /w gcc:14 make -C test/firmware_uno test -``` - -Esperado: `27 testes, ... 0 falhas`. - -- [ ] **Passo 5: Commit** - -```bash -git add test/firmware_uno arduino/data_flow_inventory/src/logica -git commit -m "feat(firmware): supervisor de entrega por marcos (partida, transito, saida) - -Co-Authored-By: Claude Opus 5.5 " -``` - ---- - -### Tarefa 4: Máquina de estados - -**Arquivos:** -- Criar: `arduino/data_flow_inventory/src/logica/fsm.h/.cpp` -- Teste: `test/firmware_uno/teste_fsm.cpp` - -**Interfaces:** -- Consome: `protocolo.h` (Tarefa 1), `supervisor_entrega.h` (Tarefa 3), `TXT()`. -- Produz: `enum Estado`, `enum TipoErro` (ERR_*), `enum FaseErro` (FERR_*), `nomeEstado/nomeErro/nomeFaseErro` (TXT; `nomeFaseErro(FERR_NENHUMA)` = `nullptr`), `enum TelaLcd`, `enum TipoEvento`, `struct Evento`, `struct EntradasFsm`, `struct SaidasFsm` (até 4 eventos + `zerarJuncao`), `struct Fsm`, `fsmIniciar(f, params[3], estoqueInicial)`, `fsmPasso(f, in, out)`, `bool fsmMotorLigado(f, idx)`, `char letraPeca(uint8_t)`. - -**Regras (spec §4):** checagem prévia (estoque → topo → junção) vai para ERRO sem ligar motor; débito só em `SUP_CONFIRMOU`; em ERRO nenhum motor fica ligado; `ocupado` traz a peça pedida; RESET só vale em ERRO e republica estado e estoque; sequência de eventos igual à do v2.x (pedido → status ACIONANDO → esteiras ...). - -- [ ] **Passo 1: Escrever os testes (falham)** - -Crie `test/firmware_uno/teste_fsm.cpp` com exatamente este conteúdo: - -```cpp -// Cenários da FSM com tempo simulado (casos de bancada P1–P5 inclusos). -#include "fsm.h" -#include "teste.h" - -namespace { - -ParamEsteira param() { - ParamEsteira p; - p.pwmRegime = 200; - p.kickMs = 150; - p.prazoPartidaMs = 3000; - p.timeoutMs = 12500; - p.saidaMs = 3000; - p.pulsoMinJuncaoMs = 20; - return p; -} - -// Bancada simulada: guarda as entradas e registra o que a FSM publicou. -struct Bancada { - Fsm f; - EntradasFsm in; - SaidasFsm out; - char log[2048]; // eventos publicados, um por linha - int debitos; // vezes em que o estoque diminuiu - int confirmacoes; // vezes em que houve EV_ENTREGA - - Bancada() { - ParamEsteira p[3] = {param(), param(), param()}; - fsmIniciar(f, p, 15); - memset(&in, 0, sizeof in); - for (int i = 0; i < 3; i++) in.topo[i] = true; // uma peça em cada topo - log[0] = 0; - debitos = 0; - confirmacoes = 0; - } - - void anota(const char* s) { - strncat(log, s, sizeof log - strlen(log) - 1); - strncat(log, "\n", sizeof log - strlen(log) - 1); - } - - // Um ciclo do loop no instante t (ms). - void passo(uint32_t t, ComandoTipo cmd = CMD_NENHUM, char peca = 0) { - in.agora = t; - in.cmd = cmd; - in.cmdPeca = peca; - int antes = f.estoque[0] + f.estoque[1] + f.estoque[2]; - fsmPasso(f, in, out); - int depois = f.estoque[0] + f.estoque[1] + f.estoque[2]; - debitos += antes - depois; - for (uint8_t i = 0; i < out.nEventos; i++) { - const Evento& e = out.eventos[i]; - char linha[64]; - switch (e.tipo) { - case EV_PEDIDO: snprintf(linha, sizeof linha, "pedido %c", e.peca); break; - case EV_ENTREGA: snprintf(linha, sizeof linha, "entrega %c", e.peca); confirmacoes++; break; - case EV_ESTADO: snprintf(linha, sizeof linha, "estado %s", nomeEstado(f.estado)); break; - case EV_ESTOQUE: snprintf(linha, sizeof linha, "estoque %d %d %d", f.estoque[0], f.estoque[1], f.estoque[2]); break; - case EV_ESTEIRAS: snprintf(linha, sizeof linha, "esteiras %d%d%d", fsmMotorLigado(f, 0), fsmMotorLigado(f, 1), fsmMotorLigado(f, 2)); break; - default: - snprintf(linha, sizeof linha, "erro %s %c %s", nomeErro(e.erro), e.peca ? e.peca : '-', - nomeFaseErro(e.fase) ? nomeFaseErro(e.fase) : "-"); - break; - } - anota(linha); - } - if (out.zerarJuncao >= 0) { - LeituraJuncao& j = in.juncao[out.zerarJuncao]; - j.maiorPulsoMs = 0; - j.bordas = 0; - } - } - - // Avança de t0 a t1 em passos de 5 ms. - void ate(uint32_t t0, uint32_t t1) { - for (uint32_t t = t0; t <= t1; t += 5) passo(t); - } - - bool contem(const char* trecho) const { return strstr(log, trecho) != nullptr; } - bool algumMotor() const { return fsmMotorLigado(f, 0) || fsmMotorLigado(f, 1) || fsmMotorLigado(f, 2); } -}; - -} // namespace - -TESTE(entrega_normal_debita_na_confirmacao_e_volta_a_aguardar) { - for (int idx = 0; idx < 3; idx++) { - Bancada b; - const char letra = (char)('A' + idx); - b.passo(0, CMD_PECA, letra); // pedido + verificação - VERIFICA(b.f.estado == ACIONANDO_ESTEIRA); - b.passo(5); // liga a esteira - VERIFICA(b.f.estado == ENTREGANDO_PECA); - VERIFICA(fsmMotorLigado(b.f, (uint8_t)idx)); - b.in.topo[idx] = false; // a peça saiu do topo - b.ate(10, 4000); - VERIFICA(b.f.estoque[idx] == 15); // ainda não confirmou - b.in.juncao[idx].ocupada = true; // peça sobre a junção - b.in.juncao[idx].ocupouEm = 4005; - b.ate(4005, 4030); - VERIFICA(b.f.estoque[idx] == 14); // debitou na confirmação - VERIFICA(fsmMotorLigado(b.f, (uint8_t)idx)); // motor segue na saída - b.in.juncao[idx].ocupada = false; // a peça caiu na principal - b.ate(4035, 7025); - VERIFICA(!b.algumMotor()); // saída de 3 s terminou - VERIFICA(b.f.estado == ENTREGANDO_PECA && b.f.emPausa); - b.ate(7030, 8600); - VERIFICA(b.f.estado == AGUARDANDO_PEDIDO); - VERIFICA(b.debitos == 1 && b.confirmacoes == 1); - char esperado[48]; - snprintf(esperado, sizeof esperado, "pedido %c\nestado ACIONANDO_ESTEIRA", letra); - VERIFICA(b.contem(esperado)); - VERIFICA(b.contem("estado AGUARDANDO_PEDIDO")); - } -} - -TESTE(checagem_previa_vai_para_erro_sem_ligar_motor) { - { // estoque zerado - Bancada b; - b.f.estoque[0] = 0; - b.passo(0, CMD_PECA, 'A'); - VERIFICA(b.f.estado == ERRO && !b.algumMotor()); - VERIFICA(b.contem("erro sem_estoque A verificacao\nestado ERRO\nesteiras 000")); - } - { // contador > 0 mas sem peça no topo - Bancada b; - b.in.topo[1] = false; - b.passo(0, CMD_PECA, 'B'); - VERIFICA(b.f.estado == ERRO && !b.algumMotor()); - VERIFICA(b.contem("erro sem_peca_topo B verificacao")); - } - { // junção já ocupada antes de ligar (sensor obstruído ou peça esquecida) - Bancada b; - b.in.juncao[2].ocupada = true; - b.passo(0, CMD_PECA, 'C'); - VERIFICA(b.f.estado == ERRO && !b.algumMotor()); - VERIFICA(b.contem("erro juncao_obstruida C verificacao")); - } -} - -TESTE(caso_p2_esteira_c_travada_para_em_3s_sem_debito) { - Bancada b; - b.passo(0, CMD_PECA, 'C'); - b.passo(5); - b.ate(10, 3000); // topo continua ocupado: motor não avança - VERIFICA(b.f.estado == ENTREGANDO_PECA); - b.ate(3005, 3010); - VERIFICA(b.f.estado == ERRO); - VERIFICA(!b.algumMotor()); - VERIFICA(b.contem("erro motor_sem_avanco C partida")); - VERIFICA(b.debitos == 0); -} - -TESTE(caso_p1_juncao_b_nao_registra_e_termina_em_timeout_sem_debito) { - Bancada b; - b.passo(0, CMD_PECA, 'B'); - b.passo(5); - b.in.topo[1] = false; // saiu do topo... - b.ate(10, 12500); // ...mas J2 nunca mudou - VERIFICA(b.f.estado == ENTREGANDO_PECA); - b.ate(12505, 12510); - VERIFICA(b.f.estado == ERRO && !b.algumMotor()); - VERIFICA(b.contem("erro timeout B transito")); - VERIFICA(b.debitos == 0); -} - -TESTE(ruido_na_juncao_nao_debita) { - Bancada b; - b.passo(0, CMD_PECA, 'A'); - b.passo(5); - b.in.topo[0] = false; - b.ate(10, 2000); - b.in.juncao[0].maiorPulsoMs = 8; // pico de 8 ms capturado pela interrupção - b.in.juncao[0].bordas = 2; - b.ate(2005, 3000); - VERIFICA(b.debitos == 0); - VERIFICA(b.f.estado == ENTREGANDO_PECA); -} - -TESTE(peca_presa_na_saida_ja_debitou_e_vai_para_erro) { - Bancada b; - b.passo(0, CMD_PECA, 'A'); - b.passo(5); - b.in.topo[0] = false; - b.in.juncao[0].ocupada = true; - b.in.juncao[0].ocupouEm = 1000; - b.ate(1000, 1030); - VERIFICA(b.debitos == 1); - b.ate(1035, 4100); // junção continua ocupada - VERIFICA(b.f.estado == ERRO && !b.algumMotor()); - VERIFICA(b.contem("erro peca_presa_saida A saida")); - VERIFICA(b.debitos == 1); -} - -TESTE(comandos_rejeitados_nao_mudam_estado) { - Bancada b; - b.passo(0, CMD_PECA, 'A'); - b.passo(5); - b.passo(10, CMD_PECA, 'B'); // ocupado, com a peça PEDIDA - VERIFICA(b.contem("erro ocupado B -")); - b.passo(15, CMD_PECA_INVALIDA, 0); - VERIFICA(b.contem("erro peca_invalida - -")); - b.passo(20, CMD_DESCONHECIDO, 0); - VERIFICA(b.contem("erro comando_desconhecido - -")); - b.passo(25, CMD_RESET, 0); // RESET fora de ERRO: ignorado - VERIFICA(b.f.estado == ENTREGANDO_PECA); - VERIFICA(fsmMotorLigado(b.f, 0)); -} - -TESTE(reset_em_erro_volta_a_aguardar_e_republica) { - Bancada b; - b.f.estoque[0] = 0; - b.passo(0, CMD_PECA, 'A'); - VERIFICA(b.f.estado == ERRO); - b.log[0] = 0; - b.passo(5, CMD_RESET, 0); - VERIFICA(b.f.estado == AGUARDANDO_PEDIDO); - VERIFICA(b.contem("estado AGUARDANDO_PEDIDO\nestoque 0 15 15")); - b.passo(10, CMD_PECA, 'B'); // aceita novo pedido - VERIFICA(b.contem("pedido B")); -} - -TESTE(saida_segura_em_erro_mantem_motores_desligados) { - Bancada b; - b.passo(0, CMD_PECA, 'C'); - b.passo(5); - b.ate(10, 3010); // motor_sem_avanco - VERIFICA(b.f.estado == ERRO); - for (uint32_t t = 3015; t < 20000; t += 50) { - b.in.topo[2] = (t / 50) % 2; // sensores mudando não religam nada - b.in.juncao[2].ocupada = (t / 100) % 2; - b.passo(t); - VERIFICA(!b.algumMotor()); - } -} - -TESTE(propriedade_nenhum_debito_sem_confirmacao) { - // Varre combinações de comportamento do topo e da junção e confere que - // o número de débitos é sempre igual ao de entregas publicadas. - for (int topoSai = 0; topoSai < 2; topoSai++) { - for (int pulso = 0; pulso < 40; pulso += 7) { - Bancada b; - b.passo(0, CMD_PECA, 'B'); - b.passo(5); - if (topoSai) b.in.topo[1] = false; - b.ate(10, 2000); - b.in.juncao[1].maiorPulsoMs = (uint16_t)pulso; - b.ate(2005, 16000); - VERIFICA(b.debitos == b.confirmacoes); - VERIFICA(b.debitos <= 1); - } - } -} -``` - -- [ ] **Passo 2: Rodar e ver falhar** - -Rode (na raiz do repositório, Git Bash): - -```bash -MSYS_NO_PATHCONV=1 docker run --rm -v "$(cygpath -w "$PWD"):/w" -w /w gcc:14 make -C test/firmware_uno test -``` - -Esperado: erro de compilação (`fsm.h` não existe). - -- [ ] **Passo 3: Implementar** - -Crie `arduino/data_flow_inventory/src/logica/fsm.h` com exatamente este conteúdo: - -```cpp -// ============================================================ -// DATA FLOW INVENTORY — Máquina de estados do Uno (lógica pura) -// ------------------------------------------------------------ -// 5 estados (fluxograma oficial) + supervisão da entrega por marcos. -// A FSM não toca em hardware: recebe as entradas de um ciclo e devolve -// os eventos a publicar. Motores e LCD são derivados do estado -// (fsmMotorLigado / tela): em ERRO, nenhum motor fica ligado. -// Invariante: o estoque só diminui em SUP_CONFIRMOU (passagem válida -// na junção da esteira pedida). -// C++ puro (testado no PC). -// ============================================================ -#pragma once -#include - -#include "protocolo.h" -#include "supervisor_entrega.h" - -enum Estado : uint8_t { - AGUARDANDO_PEDIDO = 0, - VERIFICANDO_ESTOQUE, - ACIONANDO_ESTEIRA, - ENTREGANDO_PECA, - ERRO -}; - -enum TipoErro : uint8_t { - ERR_NENHUM = 0, - ERR_SEM_ESTOQUE, // contador zerado → ERRO - ERR_SEM_PECA_TOPO, // contador > 0, topo vazio → ERRO - ERR_JUNCAO_OBSTRUIDA, // junção ocupada antes de ligar → ERRO - ERR_MOTOR_SEM_AVANCO, // M1 → ERRO - ERR_TIMEOUT, // M2 → ERRO - ERR_PECA_PRESA_SAIDA, // M3 → ERRO - ERR_OCUPADO, // rejeição (não muda o estado) - ERR_PECA_INVALIDA, // rejeição - ERR_COMANDO_DESCONHECIDO // rejeição -}; - -enum FaseErro : uint8_t { FERR_NENHUMA = 0, FERR_VERIFICACAO, FERR_PARTIDA, FERR_TRANSITO, FERR_SAIDA }; - -// Nomes publicados no JSON (TXT: flash no AVR). -const char* nomeEstado(Estado e); -const char* nomeErro(TipoErro e); -const char* nomeFaseErro(FaseErro f); // nullptr para FERR_NENHUMA - -enum TelaLcd : uint8_t { TELA_ESTOQUE = 0, TELA_ENTREGANDO, TELA_SAINDO, TELA_ENTREGUE, TELA_ERRO }; - -enum TipoEvento : uint8_t { EV_PEDIDO = 0, EV_ERRO, EV_ENTREGA, EV_ESTADO, EV_ESTOQUE, EV_ESTEIRAS }; - -struct Evento { - TipoEvento tipo; - char peca; // 'A'..'C' ou 0 - TipoErro erro; // EV_ERRO - FaseErro fase; // EV_ERRO - long tMs; // EV_ERRO: tempo desde a partida (ou -1) - long bordas; // EV_ERRO: bordas na junção (ou -1) -}; - -struct EntradasFsm { - uint32_t agora; - bool topo[3]; // sensores de topo FILTRADOS (true = peça presente) - LeituraJuncao juncao[3]; // junções (capturadas por interrupção) - ComandoTipo cmd; // comando recebido neste ciclo (CMD_NENHUM se nenhum) - char cmdPeca; // peça do CMD_PECA -}; - -struct SaidasFsm { - static const uint8_t MAX_EVENTOS = 4; - Evento eventos[MAX_EVENTOS]; - uint8_t nEventos; - int8_t zerarJuncao; // -1 = nada; 0..2 = zerar a captura dessa junção -}; - -struct Fsm { - Estado estado; - uint8_t peca; // 0 = nenhuma; 1..3 = A..C - int estoque[3]; - ParamEsteira params[3]; - Supervisor sup; - bool emPausa; // pausa pós-entrega (motor parado) - uint32_t pausaDesde; - TelaLcd tela; - TipoErro erroAtual; // último erro que levou a ERRO (para o LCD) -}; - -static const uint16_t PAUSA_POS_ENTREGA_MS = 1500; - -void fsmIniciar(Fsm& f, const ParamEsteira params[3], int estoqueInicial); -void fsmPasso(Fsm& f, const EntradasFsm& in, SaidasFsm& out); -// Esteira idx (0..2) deve estar ligada agora? -bool fsmMotorLigado(const Fsm& f, uint8_t idx); -char letraPeca(uint8_t peca); // 1..3 → 'A'..'C'; outro → 0 -``` - -Crie `arduino/data_flow_inventory/src/logica/fsm.cpp` com exatamente este conteúdo: - -```cpp -#include "fsm.h" - -#include "texto_flash.h" - -const char* nomeEstado(Estado e) { - switch (e) { - case AGUARDANDO_PEDIDO: return TXT("AGUARDANDO_PEDIDO"); - case VERIFICANDO_ESTOQUE: return TXT("VERIFICANDO_ESTOQUE"); - case ACIONANDO_ESTEIRA: return TXT("ACIONANDO_ESTEIRA"); - case ENTREGANDO_PECA: return TXT("ENTREGANDO_PECA"); - default: return TXT("ERRO"); - } -} - -const char* nomeErro(TipoErro e) { - switch (e) { - case ERR_SEM_ESTOQUE: return TXT("sem_estoque"); - case ERR_SEM_PECA_TOPO: return TXT("sem_peca_topo"); - case ERR_JUNCAO_OBSTRUIDA: return TXT("juncao_obstruida"); - case ERR_MOTOR_SEM_AVANCO: return TXT("motor_sem_avanco"); - case ERR_TIMEOUT: return TXT("timeout"); - case ERR_PECA_PRESA_SAIDA: return TXT("peca_presa_saida"); - case ERR_OCUPADO: return TXT("ocupado"); - case ERR_PECA_INVALIDA: return TXT("peca_invalida"); - case ERR_COMANDO_DESCONHECIDO: return TXT("comando_desconhecido"); - default: return TXT("desconhecido"); - } -} - -const char* nomeFaseErro(FaseErro f) { - switch (f) { - case FERR_VERIFICACAO: return TXT("verificacao"); - case FERR_PARTIDA: return TXT("partida"); - case FERR_TRANSITO: return TXT("transito"); - case FERR_SAIDA: return TXT("saida"); - default: return nullptr; - } -} - -char letraPeca(uint8_t peca) { - return (peca >= 1 && peca <= 3) ? (char)('A' + peca - 1) : 0; -} - -static void emitir(SaidasFsm& out, TipoEvento tipo, char peca = 0, - TipoErro erro = ERR_NENHUM, FaseErro fase = FERR_NENHUMA, - long tMs = -1, long bordas = -1) { - if (out.nEventos >= SaidasFsm::MAX_EVENTOS) return; - Evento& e = out.eventos[out.nEventos++]; - e.tipo = tipo; - e.peca = peca; - e.erro = erro; - e.fase = fase; - e.tMs = tMs; - e.bordas = bordas; -} - -// Vai para ERRO: motores param (fsmMotorLigado passa a ser falso), o erro é -// publicado com o diagnóstico e o dashboard recebe o novo estado e as esteiras. -static void falhar(Fsm& f, SaidasFsm& out, TipoErro erro, FaseErro fase, - long tMs, long bordas) { - f.estado = ERRO; - f.emPausa = false; - f.tela = TELA_ERRO; - f.erroAtual = erro; - emitir(out, EV_ERRO, letraPeca(f.peca), erro, fase, tMs, bordas); - emitir(out, EV_ESTADO); - emitir(out, EV_ESTEIRAS); -} - -void fsmIniciar(Fsm& f, const ParamEsteira params[3], int estoqueInicial) { - f.estado = AGUARDANDO_PEDIDO; - f.peca = 0; - for (uint8_t i = 0; i < 3; i++) { - f.estoque[i] = estoqueInicial; - f.params[i] = params[i]; - } - supervisorIniciar(f.sup, params[0], 0); - f.sup.fase = FASE_FIM; - f.emPausa = false; - f.pausaDesde = 0; - f.tela = TELA_ESTOQUE; - f.erroAtual = ERR_NENHUM; -} - -bool fsmMotorLigado(const Fsm& f, uint8_t idx) { - if (f.peca != idx + 1) return false; - if (f.estado != ENTREGANDO_PECA) return false; - if (f.emPausa) return false; - return f.sup.fase != FASE_FIM; -} - -static void tratarComando(Fsm& f, const EntradasFsm& in, SaidasFsm& out) { - switch (in.cmd) { - case CMD_PECA: - if (f.estado != AGUARDANDO_PEDIDO) { - emitir(out, EV_ERRO, in.cmdPeca, ERR_OCUPADO); - return; - } - f.peca = (uint8_t)(in.cmdPeca - 'A' + 1); - emitir(out, EV_PEDIDO, in.cmdPeca); - f.estado = VERIFICANDO_ESTOQUE; - return; - case CMD_PECA_INVALIDA: - emitir(out, EV_ERRO, 0, ERR_PECA_INVALIDA); - return; - case CMD_DESCONHECIDO: - emitir(out, EV_ERRO, 0, ERR_COMANDO_DESCONHECIDO); - return; - case CMD_RESET: - if (f.estado == ERRO) { - f.estado = AGUARDANDO_PEDIDO; - f.peca = 0; - f.emPausa = false; - f.sup.fase = FASE_FIM; - f.tela = TELA_ESTOQUE; - f.erroAtual = ERR_NENHUM; - emitir(out, EV_ESTADO); - emitir(out, EV_ESTOQUE); - } - // RESET fora de ERRO: ignorado de propósito (sistema já estável). - return; - default: - return; - } -} - -void fsmPasso(Fsm& f, const EntradasFsm& in, SaidasFsm& out) { - out.nEventos = 0; - out.zerarJuncao = -1; - - tratarComando(f, in, out); - const uint8_t idx = (f.peca >= 1 && f.peca <= 3) ? (uint8_t)(f.peca - 1) : 0; - - switch (f.estado) { - case VERIFICANDO_ESTOQUE: - // Checagem prévia: qualquer falha vai para ERRO SEM ligar o motor. - if (f.estoque[idx] <= 0) { - falhar(f, out, ERR_SEM_ESTOQUE, FERR_VERIFICACAO, -1, -1); - } else if (!in.topo[idx]) { - falhar(f, out, ERR_SEM_PECA_TOPO, FERR_VERIFICACAO, -1, -1); - } else if (in.juncao[idx].ocupada) { - falhar(f, out, ERR_JUNCAO_OBSTRUIDA, FERR_VERIFICACAO, -1, -1); - } else { - f.estado = ACIONANDO_ESTEIRA; - f.tela = TELA_ENTREGANDO; - emitir(out, EV_ESTADO); - } - break; - - case ACIONANDO_ESTEIRA: - out.zerarJuncao = (int8_t)idx; - supervisorIniciar(f.sup, f.params[idx], in.agora); - f.estado = ENTREGANDO_PECA; - emitir(out, EV_ESTEIRAS); - break; - - case ENTREGANDO_PECA: { - if (f.emPausa) { - if ((uint32_t)(in.agora - f.pausaDesde) >= PAUSA_POS_ENTREGA_MS) { - f.emPausa = false; - f.peca = 0; - f.estado = AGUARDANDO_PEDIDO; - f.tela = TELA_ESTOQUE; - emitir(out, EV_ESTADO); - emitir(out, EV_ESTEIRAS); - } - break; - } - const LeituraJuncao& j = in.juncao[idx]; - const long tMs = (long)(uint32_t)(in.agora - f.sup.inicioMs); - switch (supervisorAtualizar(f.sup, in.agora, in.topo[idx], j)) { - case SUP_CONFIRMOU: - f.estoque[idx]--; - f.tela = TELA_SAINDO; - emitir(out, EV_ENTREGA, letraPeca(f.peca)); - emitir(out, EV_ESTOQUE); - break; - case SUP_SAIDA_OK: - f.emPausa = true; - f.pausaDesde = in.agora; - f.tela = TELA_ENTREGUE; - emitir(out, EV_ESTEIRAS); - break; - case SUP_FALHA_SEM_AVANCO: - falhar(f, out, ERR_MOTOR_SEM_AVANCO, FERR_PARTIDA, tMs, j.bordas); - break; - case SUP_FALHA_TIMEOUT: - falhar(f, out, ERR_TIMEOUT, FERR_TRANSITO, tMs, j.bordas); - break; - case SUP_FALHA_PRESA_SAIDA: - falhar(f, out, ERR_PECA_PRESA_SAIDA, FERR_SAIDA, tMs, j.bordas); - break; - default: - break; - } - break; - } - - default: // AGUARDANDO_PEDIDO e ERRO: nada a fazer (motores desligados) - break; - } -} -``` - -- [ ] **Passo 4: Rodar e ver passar** - -Rode (na raiz do repositório, Git Bash): - -```bash -MSYS_NO_PATHCONV=1 docker run --rm -v "$(cygpath -w "$PWD"):/w" -w /w gcc:14 make -C test/firmware_uno test -``` - -Esperado: `37 testes, ... 0 falhas` (inclui os casos P1 e P2 e a propriedade "nenhum débito sem confirmação"). - -- [ ] **Passo 5: Commit** - -```bash -git add test/firmware_uno arduino/data_flow_inventory/src/logica -git commit -m "feat(firmware): maquina de estados com checagem previa e debito so na confirmacao - -Co-Authored-By: Claude Opus 5.5 " -``` - ---- - -### Tarefa 5: Textos do LCD - -**Arquivos:** -- Criar: `arduino/data_flow_inventory/src/logica/telas.h/.cpp` -- Teste: `test/firmware_uno/teste_telas.cpp` - -**Interfaces:** -- Consome: `fsm.h` (Tarefa 4), `TXT()`. -- Produz: `LCD_COLUNAS` (16) e `void textoTela(const Fsm& f, char* l1, char* l2)` (linhas com no máximo 16 caracteres, em RAM). - -- [ ] **Passo 1: Escrever os testes (falham)** - -Crie `test/firmware_uno/teste_telas.cpp` com exatamente este conteúdo: - -```cpp -// Textos do LCD (16 colunas). -#include "fsm.h" -#include "telas.h" -#include "teste.h" - -static ParamEsteira paramPadrao() { - ParamEsteira p; - p.pwmRegime = 200; - p.kickMs = 150; - p.prazoPartidaMs = 3000; - p.timeoutMs = 12500; - p.saidaMs = 3000; - p.pulsoMinJuncaoMs = 20; - return p; -} - -// ---- Telas --------------------------------------------------------------------- - -static Fsm fsmNova() { - Fsm f; - ParamEsteira p[3] = {paramPadrao(), paramPadrao(), paramPadrao()}; - fsmIniciar(f, p, 15); - return f; -} - -TESTE(telas_cabem_em_16_colunas) { - char l1[17], l2[17]; - Fsm f = fsmNova(); - textoTela(f, l1, l2); - VERIFICA_TEXTO(l1, "Estoque:"); - VERIFICA_TEXTO(l2, "A:15 B:15 C:15"); - - f.peca = 2; - f.tela = TELA_ENTREGANDO; - textoTela(f, l1, l2); - VERIFICA_TEXTO(l1, "Entregando B"); - - f.tela = TELA_ENTREGUE; - f.estoque[1] = 14; - textoTela(f, l1, l2); - VERIFICA_TEXTO(l2, "Entregue! B:14"); - - const TipoErro erros[] = {ERR_MOTOR_SEM_AVANCO, ERR_TIMEOUT, ERR_JUNCAO_OBSTRUIDA, ERR_SEM_PECA_TOPO, - ERR_SEM_ESTOQUE, ERR_PECA_PRESA_SAIDA, ERR_COMANDO_DESCONHECIDO}; - const char* esperado1[] = {"ERRO: Motor B", "ERRO: Timeout B", "ERRO: Juncao J2", "ERRO: Topo B", - "ERRO: Estoque B", "ERRO: Saida B", "ERRO"}; - const char* esperado2[] = {"Sem avanco", "Retire a peca", "Obstruida", "Sem peca", - "Zerado", "Peca presa", "comando_desconhe"}; - f.tela = TELA_ERRO; - for (int i = 0; i < 7; i++) { - f.erroAtual = erros[i]; - textoTela(f, l1, l2); - VERIFICA_TEXTO(l1, esperado1[i]); - VERIFICA_TEXTO(l2, esperado2[i]); - VERIFICA(strlen(l1) <= 16 && strlen(l2) <= 16); - } -} -``` - -- [ ] **Passo 2: Rodar e ver falhar** - -Rode (na raiz do repositório, Git Bash): - -```bash -MSYS_NO_PATHCONV=1 docker run --rm -v "$(cygpath -w "$PWD"):/w" -w /w gcc:14 make -C test/firmware_uno test -``` - -Esperado: erro de compilação (`telas.h` não existe). - -- [ ] **Passo 3: Implementar** - -Crie `arduino/data_flow_inventory/src/logica/telas.h` com exatamente este conteúdo: - -```cpp -// ============================================================ -// DATA FLOW INVENTORY — Textos do LCD 16x2 (lógica pura) -// ------------------------------------------------------------ -// Cada tela vira duas linhas de no máximo 16 caracteres. A camada de -// hardware só redesenha quando o texto muda. C++ puro (testado no PC). -// ============================================================ -#pragma once -#include "fsm.h" - -static const uint8_t LCD_COLUNAS = 16; - -// l1 e l2 precisam ter espaço para LCD_COLUNAS + 1 caracteres. -void textoTela(const Fsm& f, char* l1, char* l2); -``` - -Crie `arduino/data_flow_inventory/src/logica/telas.cpp` com exatamente este conteúdo: - -```cpp -#include "telas.h" - -#include - -#include "texto_flash.h" - -namespace { -// Acrescenta texto fixo (TXT) respeitando o limite de 16 colunas. -void anexaTxt(char* dst, const char* txt) { - uint8_t n = (uint8_t)strlen(dst); - for (char c = lerTxt(txt); c != 0 && n < LCD_COLUNAS; c = lerTxt(++txt)) dst[n++] = c; - dst[n] = 0; -} - -// Acrescenta texto em RAM respeitando o limite de 16 colunas. -void anexaRam(char* dst, const char* s) { - uint8_t n = (uint8_t)strlen(dst); - while (*s && n < LCD_COLUNAS) dst[n++] = *s++; - dst[n] = 0; -} - -void anexaNum(char* dst, int v) { - char tmp[7]; - uint8_t i = 0; - bool neg = v < 0; - unsigned u = neg ? (unsigned)(-v) : (unsigned)v; - do { tmp[i++] = (char)('0' + (u % 10)); u /= 10; } while (u > 0 && i < 6); - char s[8]; - uint8_t k = 0; - if (neg) s[k++] = '-'; - while (i > 0) s[k++] = tmp[--i]; - s[k] = 0; - anexaRam(dst, s); -} - -void anexaLetra(char* dst, char c) { - char s[2] = {c, 0}; - anexaRam(dst, s); -} - -void telaErro(const Fsm& f, char* l1, char* l2) { - const char p = letraPeca(f.peca); - switch (f.erroAtual) { - case ERR_MOTOR_SEM_AVANCO: - anexaTxt(l1, TXT("ERRO: Motor ")); anexaLetra(l1, p); anexaTxt(l2, TXT("Sem avanco")); - break; - case ERR_TIMEOUT: - anexaTxt(l1, TXT("ERRO: Timeout ")); anexaLetra(l1, p); anexaTxt(l2, TXT("Retire a peca")); - break; - case ERR_JUNCAO_OBSTRUIDA: - anexaTxt(l1, TXT("ERRO: Juncao J")); anexaNum(l1, f.peca); anexaTxt(l2, TXT("Obstruida")); - break; - case ERR_SEM_PECA_TOPO: - anexaTxt(l1, TXT("ERRO: Topo ")); anexaLetra(l1, p); anexaTxt(l2, TXT("Sem peca")); - break; - case ERR_SEM_ESTOQUE: - anexaTxt(l1, TXT("ERRO: Estoque ")); anexaLetra(l1, p); anexaTxt(l2, TXT("Zerado")); - break; - case ERR_PECA_PRESA_SAIDA: - anexaTxt(l1, TXT("ERRO: Saida ")); anexaLetra(l1, p); anexaTxt(l2, TXT("Peca presa")); - break; - default: - anexaTxt(l1, TXT("ERRO")); - anexaTxt(l2, nomeErro(f.erroAtual)); - break; - } -} -} // namespace - -void textoTela(const Fsm& f, char* l1, char* l2) { - l1[0] = 0; - l2[0] = 0; - const char p = letraPeca(f.peca); - switch (f.tela) { - case TELA_ENTREGANDO: - anexaTxt(l1, TXT("Entregando ")); anexaLetra(l1, p); - anexaTxt(l2, TXT("Aguarda juncao")); - break; - case TELA_SAINDO: - anexaTxt(l1, TXT("Saindo da")); - anexaTxt(l2, TXT("esteira...")); - break; - case TELA_ENTREGUE: - anexaTxt(l1, TXT("Entrega OK!")); - anexaTxt(l2, TXT("Entregue! ")); anexaLetra(l2, p); anexaTxt(l2, TXT(":")); - anexaNum(l2, (f.peca >= 1 && f.peca <= 3) ? f.estoque[f.peca - 1] : 0); - break; - case TELA_ERRO: - telaErro(f, l1, l2); - break; - default: // TELA_ESTOQUE - anexaTxt(l1, TXT("Estoque:")); - anexaTxt(l2, TXT("A:")); anexaNum(l2, f.estoque[0]); - anexaTxt(l2, TXT(" B:")); anexaNum(l2, f.estoque[1]); - anexaTxt(l2, TXT(" C:")); anexaNum(l2, f.estoque[2]); - break; - } -} -``` - -- [ ] **Passo 4: Rodar e ver passar** - -Rode (na raiz do repositório, Git Bash): - -```bash -MSYS_NO_PATHCONV=1 docker run --rm -v "$(cygpath -w "$PWD"):/w" -w /w gcc:14 make -C test/firmware_uno test -``` - -Esperado: `38 testes, 582 verificacoes, 0 falhas`. - -- [ ] **Passo 5: Commit** - -```bash -git add test/firmware_uno arduino/data_flow_inventory/src/logica -git commit -m "feat(firmware): textos do LCD 16x2 por situacao - -Co-Authored-By: Claude Opus 5.5 " -``` - ---- - -### Tarefa 6: Camada de hardware, troca do `.ino` e portão de RAM no CI - -**Arquivos:** -- Criar: `arduino/data_flow_inventory/config.h` -- Criar: `arduino/data_flow_inventory/src/hw/motores.h/.cpp`, `sensores.h/.cpp`, `comunicacao.h/.cpp`, `lcd.h/.cpp`, `diagnostico.h/.cpp` -- Modificar (substituir por inteiro): `arduino/data_flow_inventory/data_flow_inventory.ino` -- Modificar: `.github/workflows/lint-and-security.yaml` (passo "Compilar Arduino Uno (principal)") - -**Interfaces:** -- Consome: todos os módulos de `src/logica/` (Tarefas 1–5). -- Produz: firmware v3.0 completo. - -**Contexto:** esta é a tarefa que troca o firmware gravado. A ordem de mensagens no boot continua a do v2.x (estoque e depois `inicio`). Rollback: o firmware v2.x é o commit `fddde08` (`git checkout fddde08 -- arduino/data_flow_inventory`). O código comentado de botões e separador do v2.x não volta (fica no histórico). - -- [ ] **Passo 1: Configuração** - -Crie `arduino/data_flow_inventory/config.h` com exatamente este conteúdo: - -```cpp -// ============================================================ -// DATA FLOW INVENTORY — Configuração do firmware do Uno -// ------------------------------------------------------------ -// Pinos (iguais ao firmware v2.x e ao diagrama elétrico) e parâmetros -// por esteira. Para calibrar uma esteira, mude só a linha dela. -// ============================================================ -#pragma once -#include "src/logica/supervisor_entrega.h" - -#define VERSAO_FIRMWARE "3.0" - -// ---- Serial (deve ser IGUAL ao Serial2 do ESP32) -------------------------- -#define BAUD_SERIAL 9600 - -// ---- Motores DC (IRF520: 1 pino PWM por motor) ---------------------------- -#define MOTOR_A 9 // Timer1 -#define MOTOR_B 10 // Timer1 -#define MOTOR_C 11 // Timer2 - -// ---- Sensores TCRT5000 (LOW = peça detectada; INPUT_PULLUP) --------------- -#define SENSOR_TOPO_A A0 -#define SENSOR_TOPO_B A1 -#define SENSOR_TOPO_C A2 -#define SENSOR_JUNCAO_J1 A3 // PCINT11 (porta C) -#define SENSOR_JUNCAO_J2 2 // PCINT18 (porta D) -#define SENSOR_JUNCAO_J3 4 // PCINT20 (porta D) - -// ---- LCD I2C 16x2 (endereços procurados no boot) --------------------------- -#define LCD_ENDERECO_1 0x27 -#define LCD_ENDERECO_2 0x3F - -// ---- Sistema ----------------------------------------------------------------- -#define ESTOQUE_INICIAL 15 // peças de cada tipo ao ligar (não persiste) -#define INTERVALO_AMOSTRA_MS 5 // amostragem dos sensores -#define FILTRO_TOPO_MS 20 // estabilidade exigida no sensor de topo -#define INTERVALO_PUBLICACAO 1000 // ciclo das mensagens periódicas -#define PASSOS_PUBLICACAO 4 // status, estoque, sensores, esteiras -#define TELA_ABERTURA_MS 2000 // "Data Flow / Inventory v3.0" no boot - -// ---- Parâmetros por esteira (A, B, C) ---------------------------------------- -// pwmRegime : PWM depois do kick (bancada: mínimo ~150 para mover com peça) -// kickMs : partida em PWM 255 para vencer o atrito estático -// prazoPartidaMs : M1 — o topo precisa ficar livre neste prazo -// timeoutMs : M2 — a junção precisa confirmar a passagem neste prazo -// saidaMs : M3 — motor ligado depois da confirmação -// pulsoMinJuncaoMs : pulso mínimo na junção para valer como passagem -// pwm kick partida timeout saida pulso -#define PARAM_ESTEIRA_A { 200, 150, 3000, 12500, 3000, 20 } -#define PARAM_ESTEIRA_B { 200, 150, 3000, 12500, 3000, 20 } -#define PARAM_ESTEIRA_C { 200, 150, 3000, 12500, 3000, 20 } -``` - -- [ ] **Passo 2: Motores, sensores, serial, LCD e diagnóstico** - -Crie `arduino/data_flow_inventory/src/hw/motores.h` com exatamente este conteúdo: - -```cpp -// ============================================================ -// DATA FLOW INVENTORY — Motores das esteiras secundárias (hardware) -// ------------------------------------------------------------ -// IRF520: 1 pino PWM por motor. Partida com kick-start (PWM 255 por -// kickMs) e depois o PWM de regime da esteira. O estado desejado vem -// da FSM a cada ciclo; o que não está desejado fica em 0. -// ============================================================ -#pragma once -#include - -#include "../logica/supervisor_entrega.h" - -// Configura os 3 pinos como saída em 0. Chamar ANTES de qualquer outra -// inicialização (o gate do IRF520 não pode ficar flutuando). -void motoresIniciar(); - -// desejado[i] = esteira i deve estar ligada agora. -void motoresAplicar(const bool desejado[3], const ParamEsteira params[3], uint32_t agora); -``` - -Crie `arduino/data_flow_inventory/src/hw/motores.cpp` com exatamente este conteúdo: - -```cpp -#include "motores.h" - -#include - -#include "../../config.h" - -namespace { -const uint8_t PINOS[3] = {MOTOR_A, MOTOR_B, MOTOR_C}; - -struct EstadoMotor { - bool ligado; - bool emRegime; - uint32_t ligouEm; -}; -EstadoMotor motor[3]; -} // namespace - -void motoresIniciar() { - for (uint8_t i = 0; i < 3; i++) { - pinMode(PINOS[i], OUTPUT); - analogWrite(PINOS[i], 0); - motor[i].ligado = false; - motor[i].emRegime = false; - motor[i].ligouEm = 0; - } -} - -void motoresAplicar(const bool desejado[3], const ParamEsteira params[3], uint32_t agora) { - for (uint8_t i = 0; i < 3; i++) { - EstadoMotor& m = motor[i]; - if (!desejado[i]) { - if (m.ligado) { - analogWrite(PINOS[i], 0); - m.ligado = false; - m.emRegime = false; - } - continue; - } - if (!m.ligado) { - m.ligado = true; - m.ligouEm = agora; - if (params[i].kickMs > 0) { - analogWrite(PINOS[i], 255); // kick-start - m.emRegime = false; - } else { - analogWrite(PINOS[i], params[i].pwmRegime); - m.emRegime = true; - } - } else if (!m.emRegime && (uint32_t)(agora - m.ligouEm) >= params[i].kickMs) { - analogWrite(PINOS[i], params[i].pwmRegime); - m.emRegime = true; - } - } -} -``` - -Crie `arduino/data_flow_inventory/src/hw/sensores.h` com exatamente este conteúdo: - -```cpp -// ============================================================ -// DATA FLOW INVENTORY — Sensores TCRT5000 (hardware) -// ------------------------------------------------------------ -// Topo (A0–A2): amostrados a cada INTERVALO_AMOSTRA_MS e filtrados. -// Junção (J1=A3, J2=D2, J3=D4): mudanças capturadas por interrupção de -// mudança de pino (PCINT), para que um pulso curto não se perca quando -// o loop está ocupado (serial, LCD). LOW no pino = peça detectada. -// ============================================================ -#pragma once -#include - -#include "../logica/supervisor_entrega.h" - -void sensoresIniciar(uint32_t agora); -void sensoresAmostrar(uint32_t agora); - -bool sensorTopo(uint8_t i); // filtrado (true = peça presente) -bool sensorJuncaoNivel(uint8_t i); // nível atual da junção -LeituraJuncao sensorJuncao(uint8_t i); // cópia atômica da captura -void sensorJuncaoZerar(uint8_t i); // zera pulsos/bordas (início da entrega) -``` - -Crie `arduino/data_flow_inventory/src/hw/sensores.cpp` com exatamente este conteúdo: - -```cpp -#include "sensores.h" - -#include - -#include "../../config.h" -#include "../logica/filtro_entrada.h" - -namespace { -const uint8_t PINOS_TOPO[3] = {SENSOR_TOPO_A, SENSOR_TOPO_B, SENSOR_TOPO_C}; -FiltroEntrada topo[3]; - -struct CapturaJuncao { - volatile bool ocupada; - volatile uint32_t ocupouEm; - volatile uint16_t maiorPulsoMs; - volatile uint8_t bordas; -}; -CapturaJuncao juncao[3]; - -// Leitura direta das portas (rápida, dentro da ISR). LOW = ocupada. -inline bool lerJ1() { return (PINC & _BV(PC3)) == 0; } // A3 -inline bool lerJ2() { return (PIND & _BV(PD2)) == 0; } // D2 -inline bool lerJ3() { return (PIND & _BV(PD4)) == 0; } // D4 - -// Chamada só com interrupções desabilitadas (dentro da ISR). -inline void registrar(uint8_t i, bool ocupada, uint32_t agora) { - CapturaJuncao& c = juncao[i]; - if (ocupada == c.ocupada) return; - c.ocupada = ocupada; - if (c.bordas < 255) c.bordas = c.bordas + 1; - if (ocupada) { - c.ocupouEm = agora; - } else { - uint32_t largura = agora - c.ocupouEm; - uint16_t l = largura > 65535UL ? 65535 : (uint16_t)largura; - if (l > c.maiorPulsoMs) c.maiorPulsoMs = l; - } -} -} // namespace - -ISR(PCINT1_vect) { // porta C: A3 (J1) - registrar(0, lerJ1(), millis()); -} - -ISR(PCINT2_vect) { // porta D: D2 (J2) e D4 (J3) - const uint32_t agora = millis(); - registrar(1, lerJ2(), agora); - registrar(2, lerJ3(), agora); -} - -void sensoresIniciar(uint32_t agora) { - for (uint8_t i = 0; i < 3; i++) { - pinMode(PINOS_TOPO[i], INPUT_PULLUP); - filtroIniciar(topo[i], digitalRead(PINOS_TOPO[i]) == LOW, agora, FILTRO_TOPO_MS); - } - pinMode(SENSOR_JUNCAO_J1, INPUT_PULLUP); - pinMode(SENSOR_JUNCAO_J2, INPUT_PULLUP); - pinMode(SENSOR_JUNCAO_J3, INPUT_PULLUP); - - noInterrupts(); - const bool inicial[3] = {lerJ1(), lerJ2(), lerJ3()}; - for (uint8_t i = 0; i < 3; i++) { - juncao[i].ocupada = inicial[i]; - juncao[i].ocupouEm = agora; - juncao[i].maiorPulsoMs = 0; - juncao[i].bordas = 0; - } - PCMSK1 |= _BV(PCINT11); // A3 - PCMSK2 |= _BV(PCINT18) | _BV(PCINT20); // D2, D4 - PCIFR = _BV(PCIF1) | _BV(PCIF2); // descarta pendências antigas - PCICR |= _BV(PCIE1) | _BV(PCIE2); - interrupts(); -} - -void sensoresAmostrar(uint32_t agora) { - for (uint8_t i = 0; i < 3; i++) { - filtroAtualizar(topo[i], digitalRead(PINOS_TOPO[i]) == LOW, agora); - } -} - -bool sensorTopo(uint8_t i) { return topo[i].estavel; } - -bool sensorJuncaoNivel(uint8_t i) { return juncao[i].ocupada; } - -LeituraJuncao sensorJuncao(uint8_t i) { - LeituraJuncao l; - noInterrupts(); - l.ocupada = juncao[i].ocupada; - l.ocupouEm = juncao[i].ocupouEm; - l.maiorPulsoMs = juncao[i].maiorPulsoMs; - l.bordas = juncao[i].bordas; - interrupts(); - return l; -} - -void sensorJuncaoZerar(uint8_t i) { - noInterrupts(); - juncao[i].maiorPulsoMs = 0; - juncao[i].bordas = 0; - interrupts(); -} -``` - -Crie `arduino/data_flow_inventory/src/hw/comunicacao.h` com exatamente este conteúdo: - -```cpp -// ============================================================ -// DATA FLOW INVENTORY — Serial Uno ↔ ESP32 (hardware) -// ------------------------------------------------------------ -// Lê comandos sem bloquear e publica as mensagens JSON (formatadas por -// src/logica/protocolo, idênticas ao firmware v2.x + campos opcionais). -// Cada mensagem é uma linha terminada em "\r\n" (como Serial.println). -// ============================================================ -#pragma once -#include - -#include "../logica/fsm.h" - -void comunicacaoIniciar(); - -// Consome bytes disponíveis até completar UMA linha; devolve o comando -// (CMD_NENHUM se ainda não há linha completa). Bytes restantes ficam -// para o próximo ciclo. -ComandoTipo comunicacaoLer(char* peca); - -void publicarEvento(const Evento& e, const Fsm& f, uint32_t agora); -void publicarEstado(const Fsm& f, uint32_t agora); -void publicarEstoque(const Fsm& f); -void publicarSensores(); -void publicarEsteiras(const Fsm& f); -void publicarInicio(const char* versao, const char* reset, bool lcd, long ramLivre); -``` - -Crie `arduino/data_flow_inventory/src/hw/comunicacao.cpp` com exatamente este conteúdo: - -```cpp -#include "comunicacao.h" - -#include - -#include "../../config.h" -#include "../logica/protocolo.h" -#include "diagnostico.h" -#include "sensores.h" - -namespace { -LeitorLinha leitor; -char buf[160]; // buffer único de formatação (maior mensagem ~140 B) - -void enviar(size_t n) { - if (n == 0) return; // não coube: não envia linha quebrada - Serial.write(reinterpret_cast(buf), n); - Serial.write('\r'); - Serial.write('\n'); -} -} // namespace - -void comunicacaoIniciar() { leitorIniciar(leitor); } - -ComandoTipo comunicacaoLer(char* peca) { - *peca = 0; - while (Serial.available() > 0) { - if (leitorAlimentar(leitor, (char)Serial.read())) { - ComandoTipo c = interpretarComando(leitor.buf, peca); - if (c != CMD_NENHUM) return c; - } - } - return CMD_NENHUM; -} - -void publicarEstado(const Fsm& f, uint32_t agora) { - enviar(formatarStatus(buf, sizeof buf, nomeEstado(f.estado), f.peca, agora / 1000UL, ramLivre())); -} - -void publicarEstoque(const Fsm& f) { enviar(formatarEstoque(buf, sizeof buf, f.estoque)); } - -void publicarSensores() { - bool topo[3], jun[3]; - for (uint8_t i = 0; i < 3; i++) { - topo[i] = sensorTopo(i); - jun[i] = sensorJuncaoNivel(i); - } - enviar(formatarSensores(buf, sizeof buf, topo, jun)); -} - -void publicarEsteiras(const Fsm& f) { - bool sec[3]; - for (uint8_t i = 0; i < 3; i++) sec[i] = fsmMotorLigado(f, i); - enviar(formatarEsteiras(buf, sizeof buf, sec)); -} - -void publicarInicio(const char* versao, const char* reset, bool lcd, long ram) { - enviar(formatarInicio(buf, sizeof buf, versao, reset, lcd ? 1 : 0, ram)); -} - -void publicarEvento(const Evento& e, const Fsm& f, uint32_t agora) { - switch (e.tipo) { - case EV_PEDIDO: - enviar(formatarPedido(buf, sizeof buf, e.peca)); - break; - case EV_ENTREGA: - enviar(formatarEntrega(buf, sizeof buf, e.peca, f.estoque)); - break; - case EV_ERRO: - enviar(formatarErro(buf, sizeof buf, nomeErro(e.erro), e.peca, nomeFaseErro(e.fase), e.tMs, e.bordas)); - break; - case EV_ESTADO: - publicarEstado(f, agora); - break; - case EV_ESTOQUE: - publicarEstoque(f); - break; - case EV_ESTEIRAS: - publicarEsteiras(f); - break; - } -} -``` - -Crie `arduino/data_flow_inventory/src/hw/lcd.h` com exatamente este conteúdo: - -```cpp -// ============================================================ -// DATA FLOW INVENTORY — LCD I2C 16x2 (hardware) -// ------------------------------------------------------------ -// Procura o módulo em LCD_ENDERECO_1 e LCD_ENDERECO_2. Se nenhum -// responder, o sistema segue SEM LCD (nada trava). Redesenha só quando -// o texto muda, sobrescrevendo com espaços (sem lcd.clear(), sem piscar). -// ============================================================ -#pragma once -#include - -bool lcdIniciar(); // true se encontrou o LCD -void lcdAtualizar(const char* l1, const char* l2); // no-op sem LCD -``` - -Crie `arduino/data_flow_inventory/src/hw/lcd.cpp` com exatamente este conteúdo: - -```cpp -#include "lcd.h" - -#include -#include -#include -#include - -#include "../../config.h" - -namespace { -LiquidCrystal_I2C lcd1(LCD_ENDERECO_1, 16, 2); -LiquidCrystal_I2C lcd2(LCD_ENDERECO_2, 16, 2); -LiquidCrystal_I2C* ativo = nullptr; -char linha1[17] = ""; -char linha2[17] = ""; - -bool responde(uint8_t endereco) { - Wire.beginTransmission(endereco); - return Wire.endTransmission() == 0; -} - -void escreverLinha(uint8_t linha, const char* texto) { - ativo->setCursor(0, linha); - uint8_t i = 0; - for (; texto[i] && i < 16; i++) ativo->write(texto[i]); - for (; i < 16; i++) ativo->write(' '); -} -} // namespace - -bool lcdIniciar() { - Wire.begin(); - // Um barramento I2C preso (ruído do motor) não pode travar o Uno. - Wire.setWireTimeout(25000, true); - if (responde(LCD_ENDERECO_1)) ativo = &lcd1; - else if (responde(LCD_ENDERECO_2)) ativo = &lcd2; - if (ativo == nullptr) return false; - ativo->init(); - ativo->backlight(); - ativo->clear(); - linha1[0] = 0; - linha2[0] = 0; - return true; -} - -void lcdAtualizar(const char* l1, const char* l2) { - if (ativo == nullptr) return; - if (strncmp(l1, linha1, 16) != 0) { - escreverLinha(0, l1); - strncpy(linha1, l1, 16); - linha1[16] = 0; - } - if (strncmp(l2, linha2, 16) != 0) { - escreverLinha(1, l2); - strncpy(linha2, l2, 16); - linha2[16] = 0; - } -} -``` - -Crie `arduino/data_flow_inventory/src/hw/diagnostico.h` com exatamente este conteúdo: - -```cpp -// ============================================================ -// DATA FLOW INVENTORY — Diagnóstico: causa do reset, RAM, watchdog -// ------------------------------------------------------------ -// O watchdog (2 s, modo interrupção + reset) reinicia o Uno se o loop -// travar; a interrupção grava a marca "watchdog" em RAM .noinit antes -// do reset. O watchdog é desligado logo no boot (.init3), senão um -// reset por watchdog deixaria o Uno reiniciando em laço. -// ============================================================ -#pragma once -#include - -void diagnosticoIniciar(); // calcula a causa do reset e arma o watchdog -const char* diagnosticoCausaReset(); // "energia", "reinicio", "watchdog" ou "brownout" -long ramLivre(); // bytes entre o heap e a pilha -void watchdogAlimentar(); // chamar uma vez por ciclo do loop -``` - -Crie `arduino/data_flow_inventory/src/hw/diagnostico.cpp` com exatamente este conteúdo: - -```cpp -#include "diagnostico.h" - -#include -#include - -#include "../logica/causa_reset.h" - -namespace { -const uint32_t ASSINATURA = 0xDF1A3B5CUL; -const uint8_t MARCA_WATCHDOG = 0xA5; -const char* causa = "energia"; -} // namespace - -// Variáveis que sobrevivem a resets com energia (não são zeradas no boot). -uint8_t dfiMcusrNoBoot __attribute__((section(".noinit"))); -uint32_t dfiAssinatura __attribute__((section(".noinit"))); -volatile uint8_t dfiMarcaWatchdog __attribute__((section(".noinit"))); - -// Roda antes de main(): guarda o MCUSR e desliga o watchdog (padrão avr-libc). -extern "C" void dfiCapturarMcusr(void) __attribute__((naked, used, section(".init3"))); -extern "C" void dfiCapturarMcusr(void) { - dfiMcusrNoBoot = MCUSR; - MCUSR = 0; - wdt_disable(); -} - -ISR(WDT_vect) { - // O loop não alimentou o watchdog por 2 s: marca e deixa o próximo - // estouro reiniciar o Uno. - dfiMarcaWatchdog = MARCA_WATCHDOG; -} - -static void armarWatchdog() { - noInterrupts(); - wdt_reset(); - WDTCSR = _BV(WDCE) | _BV(WDE); - WDTCSR = _BV(WDIE) | _BV(WDE) | _BV(WDP2) | _BV(WDP1) | _BV(WDP0); // 2 s - interrupts(); -} - -void diagnosticoIniciar() { - const bool assinaturaValida = dfiAssinatura == ASSINATURA; - const bool marcaWd = dfiMarcaWatchdog == MARCA_WATCHDOG; - causa = causaReset(dfiMcusrNoBoot, assinaturaValida, marcaWd); - dfiAssinatura = ASSINATURA; - dfiMarcaWatchdog = 0; - armarWatchdog(); -} - -const char* diagnosticoCausaReset() { return causa; } - -extern int __heap_start; -extern int* __brkval; - -long ramLivre() { - int v; - return (long)((int)&v - (__brkval == 0 ? (int)&__heap_start : (int)__brkval)); -} - -void watchdogAlimentar() { - wdt_reset(); - if (dfiMarcaWatchdog == MARCA_WATCHDOG) { - // O loop voltou depois de um aviso do watchdog: limpa a marca e - // rearma o modo interrupção + reset. - dfiMarcaWatchdog = 0; - armarWatchdog(); - } -} -``` - -- [ ] **Passo 3: Substituir o `.ino`** - -Substitua **todo** o conteúdo de `arduino/data_flow_inventory/data_flow_inventory.ino` por: - -```cpp -// ============================================================ -// DATA FLOW INVENTORY — Sistema de Intralogística Automatizada -// SENAI São Caetano do Sul — Engenharia de Controle e Automação -// Autores: Henrique Moni, Matheus Nespolo, Murilo Tolardo, Vitor Marcolongo -// Ano: 2026 — Firmware do Arduino Uno v3.0 -// ============================================================ -// Máquina de estados com 5 etapas (fluxograma oficial): -// AGUARDANDO_PEDIDO → VERIFICANDO_ESTOQUE → ACIONANDO_ESTEIRA -// → ENTREGANDO_PECA → ERRO -// A entrega é supervisionada por marcos (src/logica/supervisor_entrega): -// M1 partida — o topo fica livre em 3 s (senão motor_sem_avanco) -// M2 trânsito — a junção confirma a passagem em 12,5 s (senão timeout) -// M3 saída — motor 3 s depois da confirmação; a junção precisa liberar -// O estoque só diminui na confirmação da junção da esteira pedida. -// -// Organização: -// config.h pinos e parâmetros por esteira -// src/logica/ regras puras (FSM, marcos, filtro, protocolo, telas), -// testadas no PC em test/firmware_uno -// src/hw/ motores, sensores, serial, LCD e diagnóstico -// -// Comunicação com o ESP32 (Serial 9600): mensagens JSON idênticas às do -// firmware v2.x, com campos opcionais novos. Comandos: CMD:PECA:X, CMD:RESET. -// Botões físicos e separador (motor de passo) seguem desabilitados; o código -// antigo deles está no histórico do Git (firmware v2.1). -// ============================================================ - -#include "config.h" -#include "src/hw/comunicacao.h" -#include "src/hw/diagnostico.h" -#include "src/hw/lcd.h" -#include "src/hw/motores.h" -#include "src/hw/sensores.h" -#include "src/logica/fsm.h" -#include "src/logica/telas.h" -#include "src/logica/texto_flash.h" - -static const ParamEsteira PARAMS[3] = {PARAM_ESTEIRA_A, PARAM_ESTEIRA_B, PARAM_ESTEIRA_C}; - -static Fsm fsm; -static bool lcdPresente = false; -static uint32_t inicioMs = 0; -static uint32_t ultimaAmostra = 0; -static uint32_t ultimaPublicacao = 0; -static uint8_t passoPublicacao = 0; - -void setup() { - motoresIniciar(); // 1º: gates do IRF520 em 0 antes de qualquer coisa - diagnosticoIniciar(); // causa do reset + watchdog de 2 s - Serial.begin(BAUD_SERIAL); - comunicacaoIniciar(); - - inicioMs = millis(); - sensoresIniciar(inicioMs); - lcdPresente = lcdIniciar(); - lcdAtualizar("Data Flow", "Inventory v" VERSAO_FIRMWARE); - - fsmIniciar(fsm, PARAMS, ESTOQUE_INICIAL); - - // Mesma ordem do firmware v2.x: estoque inicial e depois o evento de início. - publicarEstoque(fsm); - publicarInicio(TXT(VERSAO_FIRMWARE), diagnosticoCausaReset(), lcdPresente, ramLivre()); -} - -void loop() { - const uint32_t agora = millis(); - - if ((uint32_t)(agora - ultimaAmostra) >= INTERVALO_AMOSTRA_MS) { - ultimaAmostra = agora; - sensoresAmostrar(agora); - } - - // Entradas do ciclo → FSM. - EntradasFsm in; - in.agora = agora; - for (uint8_t i = 0; i < 3; i++) { - in.topo[i] = sensorTopo(i); - in.juncao[i] = sensorJuncao(i); - } - in.cmd = comunicacaoLer(&in.cmdPeca); - - SaidasFsm out; - fsmPasso(fsm, in, out); - if (out.zerarJuncao >= 0) sensorJuncaoZerar((uint8_t)out.zerarJuncao); - - // Motores derivados do estado: em ERRO nenhum fica ligado. - bool desejado[3]; - for (uint8_t i = 0; i < 3; i++) desejado[i] = fsmMotorLigado(fsm, i); - motoresAplicar(desejado, PARAMS, agora); - - for (uint8_t i = 0; i < out.nEventos; i++) publicarEvento(out.eventos[i], fsm, agora); - - // Publicação periódica escalonada: uma mensagem a cada 250 ms, cada uma - // 1x por segundo (evita prender o loop na serial de 9600 baud). - if ((uint32_t)(agora - ultimaPublicacao) >= INTERVALO_PUBLICACAO / PASSOS_PUBLICACAO) { - ultimaPublicacao = agora; - switch (passoPublicacao) { - case 0: publicarEstado(fsm, agora); break; - case 1: publicarEstoque(fsm); break; - case 2: publicarSensores(); break; - default: publicarEsteiras(fsm); break; - } - passoPublicacao = (uint8_t)((passoPublicacao + 1) % PASSOS_PUBLICACAO); - } - - // LCD: depois da tela de abertura, mostra a tela da FSM (redesenha só se mudou). - if (lcdPresente && (uint32_t)(agora - inicioMs) >= TELA_ABERTURA_MS) { - char l1[LCD_COLUNAS + 1], l2[LCD_COLUNAS + 1]; - textoTela(fsm, l1, l2); - lcdAtualizar(l1, l2); - } - - watchdogAlimentar(); -} -``` - -- [ ] **Passo 4: Compilar e conferir RAM e avisos** - -```bash -CLI="/c/Users/matheusn/AppData/Local/Programs/Arduino IDE/resources/app/lib/backend/resources/arduino-cli.exe" -"$CLI" compile --fqbn arduino:avr:uno --warnings all --clean arduino/data_flow_inventory 2>&1 | tee "$TEMP/uno.log" | grep -E "Sketch uses|Global variables" -grep -iE "warning|error" "$TEMP/uno.log" | grep -vE "LiquidCrystal|cores[\\/]arduino" || echo "sem avisos no código do projeto" -``` - -Esperado: compila; `Global variables use ~943 bytes (46%)` (limite: 65%); "sem avisos no código do projeto". Rode também os testes da lógica (devem continuar `38 testes, ... 0 falhas`). - -- [ ] **Passo 5: Portão de RAM no CI** - -No job `arduino-compile` de `.github/workflows/lint-and-security.yaml`, substitua o passo: - -```yaml - - name: Compilar Arduino Uno (principal) - run: arduino-cli compile --fqbn arduino:avr:uno --warnings all arduino/data_flow_inventory/data_flow_inventory.ino -``` - -por: - -```yaml - - name: Compilar Arduino Uno (principal) e conferir RAM (máx. 65%) - run: | - set -o pipefail - arduino-cli compile --fqbn arduino:avr:uno --warnings all arduino/data_flow_inventory/data_flow_inventory.ino 2>&1 | tee uno.log - ram=$(grep -oP 'Global variables use \d+ bytes \(\K\d+' uno.log) - echo "RAM global do Uno: ${ram}%" - test "$ram" -le 65 -``` - -- [ ] **Passo 6: Commit** - -```bash -git add arduino/data_flow_inventory .github/workflows/lint-and-security.yaml -git commit -m "feat(firmware): Uno v3.0 modular com marcos, watchdog e diagnostico - -Co-Authored-By: Claude Opus 5.5 " -``` - ---- - -### Tarefa 7: Documentação e roteiro de bancada - -**Arquivos:** -- Modificar: `docs/ARCHITECTURE.md` (seção 3), `README.md` (tabela da FSM), `docs/CHANGELOG.md`, `docs/CI-CD.md` -- Criar: `docs/testes/roteiros/firmware_uno_v3_validacao.md` - -- [ ] **Passo 1: `docs/ARCHITECTURE.md`** - -Na seção `## 3. Máquina de Estados (Arduino)`, substitua o trecho que começa em `**5 estados:**` e termina na linha antes de `**Diagramas:**` (inclui as listas "Erros que levam a ERRO" e "Rejeições") por: - -````markdown -**5 estados** (firmware v3.0 — código em `arduino/data_flow_inventory/src/logica/fsm.cpp`): -1. **AGUARDANDO_PEDIDO** → recebe comando -2. **VERIFICANDO_ESTOQUE** → checagem prévia, sem ligar o motor: contador > 0, peça no sensor do topo, junção livre -3. **ACIONANDO_ESTEIRA** → liga o motor da esteira escolhida (kick-start em PWM 255 por 150 ms, depois o PWM de regime da esteira) -4. **ENTREGANDO_PECA** → supervisão por marcos (abaixo); o estoque é debitado na confirmação da junção -5. **ERRO** → motores desligados a cada ciclo; só sai com `CMD:RESET` - -**Supervisão da entrega por marcos** (prazos por esteira em `arduino/data_flow_inventory/config.h`): - -| Marco | Prova | Prazo padrão | Falha → erro | Estoque | -|---|---|---|---|---| -| M1 Partida | sensor de topo fica livre | 3 s após ligar | `motor_sem_avanco` (motor parado na hora) | não debita | -| M2 Trânsito | junção registra uma passagem (pulso ≥ 20 ms, capturado por interrupção) | 12,5 s após ligar | `timeout` (retirar a peça da esteira) | não debita | -| Confirmação | — | — | — | **debita** e publica a entrega | -| M3 Saída | motor segue 3 s; no fim a junção precisa estar livre | 3 s | `peca_presa_saida` | já debitado | - -```mermaid -flowchart LR - P[Pedido] --> V{Checagem prévia} - V -- falha --> E[ERRO] - V -- ok --> M1{M1: topo livre em 3 s?} - M1 -- não --> E - M1 -- sim --> M2{M2: junção em 12,5 s?} - M2 -- não --> E - M2 -- sim --> D[Debita estoque e publica entrega] - D --> M3{M3: junção livre após 3 s?} - M3 -- não --> E - M3 -- sim --> A[AGUARDANDO_PEDIDO] - E -- CMD:RESET --> A -``` - -**Erros que levam a ERRO (exigem CMD:RESET):** -- `sem_estoque` — contador da peça zerado -- `sem_peca_topo` — contador > 0, mas sem peça no sensor do topo -- `juncao_obstruida` — sensor de junção já ocupado antes de ligar o motor -- `motor_sem_avanco` — M1: o topo não liberou no prazo (motor travado, fraco ou peça presa no início) -- `timeout` — M2: a peça saiu do topo e não chegou à junção no prazo -- `peca_presa_saida` — M3: a junção continuou ocupada no fim da saída - -**Rejeições (não mudam o estado):** -- `peca_invalida` — formato diferente de `CMD:PECA:` + A, B ou C -- `ocupado` — pedido com a FSM fora de AGUARDANDO_PEDIDO (o campo `peca` é a peça pedida) -- `comando_desconhecido` — linha não reconhecida - -**Campos opcionais (v3.0)** — as mensagens da v2.x continuam idênticas; os campos abaixo só são acrescentados no fim: -- `evento: inicio` → `reset` (`energia`, `reinicio`, `watchdog` ou `brownout`), `lcd` (`true`/`false`), `ram_livre` -- `type: status` → `ram_livre` -- `evento: erro` → `fase` (`verificacao`, `partida`, `transito`, `saida`), `t_ms` (desde a partida do motor), `bordas_juncao` - -**Robustez:** watchdog de 2 s (o Uno reinicia sozinho se o loop travar); o LCD é procurado em 0x27 e 0x3F e, se não responder, o sistema segue sem ele; o estoque não é persistido (volta a 15 no boot) e o campo `reset` do `inicio` diz por que o Uno reiniciou. Abrir a Serial Monitor reinicia o Uno (auto-reset da USB), o que aparece como `reset: reinicio`. -```` - -- [ ] **Passo 2: `README.md`** - -Na tabela de estados (seção Funcionamento), substitua as três linhas: - -```markdown -| 3 | **ACIONANDO_ESTEIRA** | Liga motor da esteira secundária correspondente | -| 4 | **ENTREGANDO_PECA** | Monitora o sensor da junção (timeout de 12,5 s); só debita o estoque após a confirmação e mantém o motor 3 s depois dela | -| 5 | **ERRO** | Sinaliza falha no LCD, aguarda reset manual | -``` - -por: - -```markdown -| 3 | **ACIONANDO_ESTEIRA** | Liga o motor da esteira escolhida (kick-start e depois o PWM de regime) | -| 4 | **ENTREGANDO_PECA** | Supervisão por marcos: topo livre em 3 s, junção em 12,5 s (débito na confirmação) e 3 s de saída | -| 5 | **ERRO** | Motores desligados; LCD e dashboard mostram onde falhou (ver `docs/ARCHITECTURE.md` §3); aguarda reset | -``` - -- [ ] **Passo 3: `docs/CHANGELOG.md`** - -Logo abaixo de `## [Não publicado]` → `### Alterado`, insira como **primeiro** item: - -```markdown -- **Firmware do Uno v3.0 — supervisão por marcos e robustez (03/10/2026)** - - Firmware reestruturado em módulos: `src/logica/` (C++ puro: protocolo, filtro, supervisor, FSM, telas, causa do reset) testado no PC (`test/firmware_uno`, 38 testes, job `firmware-logica` no CI) e `src/hw/` (motores, sensores, serial, LCD, diagnóstico) - - **Compatibilidade:** mensagens JSON idênticas às da v2.x (testes de contrato byte a byte); ESP32 e servidor não mudaram. Novos campos opcionais (`reset`, `lcd`, `ram_livre`, `fase`, `t_ms`, `bordas_juncao`) e novos erros (`sem_peca_topo`, `juncao_obstruida`, `motor_sem_avanco`, `peca_presa_saida`) - - **Entrega por marcos:** checagem prévia sem ligar o motor; M1 topo livre em 3 s (motor travado para na hora); M2 junção em 12,5 s; débito na confirmação; M3 junção livre após 3 s de saída. Junções capturadas por interrupção, com pulso mínimo de 20 ms - - **Robustez:** kick-start dos motores, watchdog de 2 s, causa do reset no `inicio`, LCD procurado em 0x27/0x3F e opcional, textos na flash (RAM global de 55% para 46%), sem ArduinoJson no Uno - - **Rollback:** firmware v2.x no commit `fddde08`. **Validação em bancada pendente:** `docs/testes/roteiros/firmware_uno_v3_validacao.md` -``` - -- [ ] **Passo 4: `docs/CI-CD.md`** - -Na seção `#### 4. Arduino Compilation (arduino-cli)`, troque a linha `- **Falha em:** Erros de compilação` por `- **Falha em:** Erros de compilação ou RAM global do Uno acima de 65%`. Depois da linha `- Para repetir o job na sua máquina, veja [Compilar o firmware localmente](#compilar-o-firmware-localmente-sem-hardware)`, acrescente: - -```markdown - -#### 5. Lógica do firmware do Uno (`firmware-logica`) -- **O quê:** compila `arduino/data_flow_inventory/src/logica/` com g++ e roda `test/firmware_uno` (FSM, marcos, filtro, protocolo, telas e causa do reset, com tempo simulado) -- **Contrato:** os testes `*_igual_v2` garantem que as mensagens JSON continuam idênticas às da v2.x -- **Local:** `docker run --rm -v ":/w" -w /w gcc:14 make -C test/firmware_uno test` (ou `make -C test/firmware_uno test` com g++ instalado) -``` - -- [ ] **Passo 5: Roteiro de bancada** - -Crie `docs/testes/roteiros/firmware_uno_v3_validacao.md` com exatamente este conteúdo: - -```markdown -# Roteiro — Validação em bancada do firmware do Uno v3.0 - -> Objetivo: confirmar na bancada o que os testes automatizados não alcançam (sensores, motores, alimentação). Execute na ordem; anote o resultado de cada passo. Firmware: `arduino/data_flow_inventory/` (v3.0). Desconecte o ESP32 dos pinos 0/1 durante o upload no Uno. - -Ferramentas: Serial Monitor (9600 baud) **ou** `test/mqtt_probe` (com o ESP32 conectado) e o dashboard. - -> Abrir a Serial Monitor reinicia o Uno (auto-reset da USB) e o estoque volta a 15. Isso é esperado e aparece como `"reset":"reinicio"`. - -## 1. Boot - -1. Ligue o Uno. -2. **Esperado:** a linha `{"type":"evento","evento":"inicio",...,"versao":"3.0",...,"reset":"energia","lcd":true|false,"ram_livre":N}`. -3. Anote `lcd` (se `false`, o LCD não respondeu em 0x27 nem 0x3F: confira 5 V, GND, SDA=A4, SCL=A5) e `ram_livre`. - -## 2. Sensores (antes de qualquer entrega) - -1. Com as três esteiras **vazias**, leia a mensagem `"type":"sensores"`: esperado `"juncao":{"J1":0,"J2":0,"J3":0}`. -2. Passe uma peça sobre cada sensor de junção, um por vez: o valor correspondente deve ir a 1. -3. Repita para os sensores de topo (`"topo"`), com e sem peça. -4. **Se algum sensor ficar sempre em 0 ou sempre em 1**, ajuste o potenciômetro do módulo TCRT5000 (ou a fiação) antes de seguir. Este passo resolve a causa provável da "entrega da B sem débito". - -## 3. Uma entrega por esteira (A, B, C) - -1. Coloque uma peça no topo da esteira e peça pelo dashboard. -2. **Esperado:** `pedido` → status `ACIONANDO_ESTEIRA` → esteira liga → `entrega` com o estoque daquela peça diminuído em 1 → após ~3 s a esteira para → status `AGUARDANDO_PEDIDO`. -3. Confira o débito no dashboard e no LCD. - -## 4. Motor travado (esteira C) - -1. Segure a esteira C com a mão (ou trave a peça no topo) e peça uma peça C. -2. **Esperado:** em ~3 s, `erro` com `"tipo":"motor_sem_avanco","fase":"partida"`, motor parado, estado `ERRO`, **sem reset do Uno** (nenhum novo `inicio`). -3. Envie Reiniciar no dashboard: volta a `AGUARDANDO_PEDIDO`. - -## 5. Timeout no trânsito - -1. Peça uma peça e, depois que ela sair do topo, retire-a da esteira antes da junção. -2. **Esperado:** em 12,5 s desde a partida, `erro` com `"tipo":"timeout","fase":"transito"`, motor parado, estoque **intacto**. - -## 6. Peça presa na saída - -1. Peça uma peça e, quando ela chegar à junção, segure-a sobre o sensor. -2. **Esperado:** `entrega` (débito feito) e, 3 s depois, `erro` com `"tipo":"peca_presa_saida","fase":"saida"`. - -## 7. Junção obstruída - -1. Deixe uma peça parada sobre o sensor de junção e peça uma peça daquela esteira. -2. **Esperado:** `erro` com `"tipo":"juncao_obstruida","fase":"verificacao"`, **sem ligar o motor**. - -## 8. Resistência - -1. Faça 10 entregas seguidas (alternando A, B e C). -2. **Esperado:** nenhum `inicio` novo no meio (sem resets) e `ram_livre` do status estável. - -## Registro - -| Passo | Resultado | Observações | -|---|---|---| -| 1 Boot | | | -| 2 Sensores | | | -| 3 Entregas A/B/C | | | -| 4 Motor travado | | | -| 5 Timeout | | | -| 6 Peça presa | | | -| 7 Junção obstruída | | | -| 8 Resistência | | | -``` - -- [ ] **Passo 6: Verificar links e commit** - -Confira que os links relativos novos existem (`docs/ARCHITECTURE.md` cita `arduino/data_flow_inventory/config.h` e `src/logica/fsm.cpp` só como texto; o roteiro não tem links). Depois: - -```bash -git add docs README.md -git commit -m "docs(firmware): marcos, novos erros, campos opcionais, CI e roteiro de bancada da v3.0 - -Co-Authored-By: Claude Opus 5.5 " -``` - ---- - -### Tarefa 8: Verificação final (nada quebrou) - -Sem arquivos novos. Rode e registre a saída de cada item: - -- [ ] **Passo 1: Lógica do firmware** - -Rode (na raiz do repositório, Git Bash): - -```bash -MSYS_NO_PATHCONV=1 docker run --rm -v "$(cygpath -w "$PWD"):/w" -w /w gcc:14 make -C test/firmware_uno test -``` - -Esperado: `38 testes, 582 verificacoes, 0 falhas`. - -- [ ] **Passo 2: Compilação das placas que o CI compila** - -```bash -CLI="/c/Users/matheusn/AppData/Local/Programs/Arduino IDE/resources/app/lib/backend/resources/arduino-cli.exe" -"$CLI" compile --fqbn arduino:avr:uno --warnings all arduino/data_flow_inventory | grep -E "Sketch uses|Global variables" -"$CLI" compile --fqbn esp32:esp32:esp32 esp32/gateway_mqtt/gateway_mqtt.ino | grep -E "Sketch uses" -"$CLI" compile --fqbn arduino:avr:uno test/esteira_peca_b/arduino_esteiras_ab/arduino_esteiras_ab.ino | grep -E "Sketch uses" -``` - -Esperado: as três compilam; RAM do Uno principal ≤ 65%. (O ESP32 compila só se existir `esp32/gateway_mqtt/secrets.h` local; no CI ele é copiado do `.example`.) - -- [ ] **Passo 3: Fronteiras (ESP32, servidor, frontend e simulador intocados)** - -```bash -git diff --name-only fddde08 -- esp32 server frontend simulator || true -``` - -Esperado: nenhuma linha. - -- [ ] **Passo 4: Suítes existentes como rede de segurança** - -```bash -node --test test/frontend/*.test.mjs 2>&1 | grep -E "^# (pass|fail)" -(cd test/server_metrics && npm ci >/dev/null 2>&1; npm test 2>&1 | grep -E "^# (tests|pass|fail)") -``` - -Esperado: 0 falhas. - -- [ ] **Passo 5: Árvore limpa** - -```bash -git status --short -``` - -Esperado: vazio (o binário `test/firmware_uno/testes` está no `.gitignore`). diff --git a/docs/superpowers/specs/2026-10-03-firmware-uno-v3-design.md b/docs/superpowers/specs/2026-10-03-firmware-uno-v3-design.md deleted file mode 100644 index ffc0e25..0000000 --- a/docs/superpowers/specs/2026-10-03-firmware-uno-v3-design.md +++ /dev/null @@ -1,196 +0,0 @@ -# Firmware Arduino Uno v3.0 — Supervisão por marcos e robustez — Design - -Data: 03/10/2026 · Branch: `feat/firmware-uno-v3` · Base: `fddde08` (PR #42) · Idioma: português do Brasil. - -## 1. Objetivo - -Tornar o firmware do Arduino Uno (`arduino/data_flow_inventory/`) robusto como produto e resolver os problemas observados em bancada em 02–03/10/2026, **sem mudar a conexão com o ESP32 nem com o servidor**. - -Problemas de bancada que este design precisa resolver ou tornar diagnosticáveis: - -| # | Observado | Causa provável | -|---|---|---| -| P1 | Esteira B entregou a peça, mas o estoque não foi debitado | o sensor de junção J2 não registrou a passagem (sempre em LOW/HIGH, mal calibrado) ou o pulso foi perdido; o firmware terminou em `timeout` sem débito | -| P2 | Esteira C mal move a peça e dá timeout | PWM baixo, atrito estático, motor travando; o motor ficava 12,5 s puxando corrente de rotor bloqueado | -| P3 | Uno travou / reiniciou junto com a Serial Monitor | queda ou ruído de alimentação com o motor travado (hardware) e/ou auto-reset ao abrir a Serial Monitor (DTR) | -| P4 | Estoque voltou a 15 sem aviso | reset do Uno; o estoque não é persistido | -| P5 | LCD apagado | hardware (alimentação/fiação/módulo); o firmware não pode travar por causa dele | - -## 2. Decisões tomadas com a equipe - -| # | Decisão | -|---|---| -| D1 | Escopo de **robustez de produto** (reestruturar o firmware), não só remendos. | -| D2 | **Não mexer na conexão com o ESP32 nem com o servidor.** As mensagens JSON atuais continuam idênticas; só entram **campos opcionais novos** e **novos valores de `tipo`** em eventos de erro. Os comandos aceitos continuam `CMD:PECA:X` e `CMD:RESET`. | -| D3 | **Estoque não é persistido**: volta a `ESTOQUE_INICIAL` (15) no boot, mas o evento `inicio` informa a causa do reset. | -| D4 | **Uma peça por vez** sobre o sensor de topo: o topo serve de indicador de avanço. | -| D5 | Falha de entrega supervisionada **por marcos** (partida → trânsito → saída), com prazos por esteira. | -| D6 | **Arquitetura modular (abordagem A)**: lógica pura testável no PC + camada de hardware fina. | -| D7 | **Débito na confirmação da junção** (momento mostrado no fluxograma oficial), seguido de 3 s de saída. | -| D8 | Sem nova tentativa automática após falha: o operador confere e envia RESET. | - -Fora do escopo: ESP32, servidor, dashboard, persistência em EEPROM, mudança de pinos, separador (motor de passo). - -## 3. Arquitetura - -### 3.1 Arquivos - -``` -arduino/data_flow_inventory/ -├── data_flow_inventory.ino # fino: setup() e loop() chamam os módulos -├── config.h # pinos, parâmetros por esteira, constantes -└── src/ - ├── logica/ # C++ puro, SEM Arduino.h → testável no PC - │ ├── filtro_entrada.h/.cpp # debounce por tempo + bordas - │ ├── supervisor_entrega.h/.cpp # marcos M1/M2/M3 - │ └── fsm.h/.cpp # 5 estados, comandos, estoque, saídas - └── hw/ # depende do Arduino - ├── motores.h/.cpp # kick-start, PWM de regime, parada segura - ├── sensores.h/.cpp # amostragem dos 6 TCRT5000 + captura de bordas da junção por PCINT - ├── comunicacao.h/.cpp # parser de comando (char[]) + publicação JSON - ├── lcd.h/.cpp # detecção 0x27/0x3F, redesenho só quando muda - └── diagnostico.h/.cpp # causa do reset, RAM livre, watchdog -``` - -A Arduino IDE e o `arduino-cli` compilam recursivamente a pasta `src/` do sketch. Os arquivos de `src/logica/` não incluem `Arduino.h` nem usam tipos do Arduino (só ``), para compilar também no PC. - -### 3.2 Ciclo do `loop()` (não bloqueante) - -1. A cada 5 ms: amostrar os sensores e atualizar os filtros. -2. Ler os bytes disponíveis da serial e montar comandos (sem bloquear). -3. Entregar à FSM o tempo atual (`agora_ms`), os sensores filtrados e o comando recebido; a FSM devolve as **saídas** (motor a ligar/parar, eventos a publicar, mudança de LCD). -4. Aplicar as saídas nos motores; em ERRO, a "saída segura" força todos os motores em 0 a cada ciclo. -5. Publicar uma mensagem periódica escalonada (status, estoque, sensores, esteiras: uma a cada 250 ms, cada uma 1x por segundo — comportamento atual). -6. Atualizar o LCD se o conteúdo mudou. -7. `wdt_reset()`. - -### 3.3 Memória (2 KB de RAM) - -- Nenhuma `String` no caminho normal; buffers fixos (`char` de 32 B para comando; documentos JSON pequenos). -- Textos fixos em flash quando possível (`F()`/`PROGMEM`). -- Orçamento: variáveis globais ≤ **65%** da RAM (hoje 55%), verificado na compilação. -- O código precisa compilar com **ArduinoJson 6** (versão fixada no CI: 6.21.5) **e 7** (instalada na máquina da equipe). - -## 4. Supervisão da entrega e regras da FSM - -### 4.1 Estados - -1. **AGUARDANDO_PEDIDO**: aceita `CMD:PECA:X`. `CMD:RESET` é ignorado. -2. **VERIFICANDO_ESTOQUE** (instantâneo). Em ordem; qualquer falha vai para **ERRO sem ligar o motor** (fase `verificacao`): - 1. contador da peça > 0, senão `sem_estoque`; - 2. sensor de topo ocupado, senão `sem_peca_topo`; - 3. sensor de junção livre, senão `juncao_obstruida`. -3. **ACIONANDO_ESTEIRA**: liga o motor com kick-start; inicia o supervisor. -4. **ENTREGANDO_PECA**: marcos (prazos contados a partir da partida do motor): - - **M1 Partida** — topo livre (filtrado) em até `prazo_partida_ms` (3000). Falha → `motor_sem_avanco` (fase `partida`), motor parado, **sem débito**. - - **M2 Trânsito** — borda válida na junção (pulso ≥ `pulso_min_juncao_ms`, 20 ms) em até `timeout_ms` (12500). Falha → `timeout` (fase `transito`), motor parado, **sem débito**; LCD pede para retirar a peça. - - **Confirmação** — **debita na hora** e publica o evento `entrega` e o estoque. - - **M3 Saída** — motor ligado por `saida_ms` (3000); no fim, junção livre. Falha → `peca_presa_saida` (fase `saida`), motor parado; o débito **já foi feito** (a peça saiu do estoque). - - Sucesso: motor parado, pausa de 1500 ms (LCD "Entregue"), volta a AGUARDANDO. -5. **ERRO**: motores sempre em 0; só sai com `CMD:RESET` (volta a AGUARDANDO, limpa o supervisor, republica estado e estoque). - -Rejeições que **não** mudam o estado: `ocupado` (pedido fora de AGUARDANDO; `peca` = peça pedida), `peca_invalida`, `comando_desconhecido`. Formato exato dos comandos: `CMD:PECA:` + 1 letra (A/B/C) e `CMD:RESET`. - -**Invariante:** o estoque só é decrementado na confirmação válida da junção da esteira da peça pedida. - -### 4.2 Captura da junção - -- As bordas dos três sensores de junção (J1 = A3/PCINT11, J2 = D2/PCINT18, J3 = D4/PCINT20) são registradas por **interrupção de mudança de pino**: a ISR só anota o instante da mudança, sem lógica. Um pulso curto não se perde mesmo com o loop ocupado (serial, LCD). -- A lógica valida a largura do pulso (≥ 20 ms) e conta as bordas observadas durante a entrega (`bordas_juncao`), usado no diagnóstico. -- Sensores de topo continuam amostrados a cada 5 ms com o mesmo filtro por tempo. - -### 4.3 Motores - -- Partida com **kick-start**: PWM 255 por `kick_ms` (150 ms) e depois `pwm_regime` da esteira (200). -- Parada imediata em qualquer erro e no fim da saída. -- No `setup()`, os pinos dos motores são os primeiros configurados (saída em 0), antes de qualquer outra inicialização. - -### 4.4 Parâmetros por esteira (`config.h`) - -| Parâmetro | Padrão (A, B e C) | -|---|---| -| `pwm_regime` | 200 | -| `kick_ms` | 150 | -| `prazo_partida_ms` | 3000 | -| `timeout_ms` | 12500 | -| `saida_ms` | 3000 | -| `pulso_min_juncao_ms` | 20 | - -## 5. Diagnóstico e recuperação - -### 5.1 Watchdog - -- Watchdog de **2 s**, alimentado no fim de cada `loop()`; nenhuma operação do laço bloqueia mais que algumas dezenas de ms (a tela de abertura deixa de usar `delay(2000)`). -- Modo interrupção-e-reset: a ISR do watchdog grava a marca `watchdog` em uma variável `.noinit` antes do reset. -- O watchdog é **desligado logo no início do boot** (seção `.init3`), porque após um reset por watchdog ele continua ativo com o prazo mínimo e reiniciaria o Uno em laço. - -### 5.2 Causa do reset - -O bootloader do Uno (Optiboot) costuma zerar o `MCUSR` antes do sketch. Combinação usada: - -1. `MCUSR` lido no `.init3`, quando ainda vier preenchido (pode indicar `brownout`); -2. assinatura em `.noinit`: ausente → `energia` (RAM apagada); presente → `reinicio` (botão, queda breve, auto-reset ao abrir a porta serial); -3. marca gravada pela ISR do watchdog → `watchdog`. - -Publicado no `inicio` como `reset: energia | reinicio | watchdog | brownout`. - -### 5.3 Periféricos - -- **LCD:** no boot, procura o módulo em 0x27 e 0x3F; se nenhum responder, segue sem LCD (`lcd: false` no `inicio`) e não tenta escrever de novo. `Wire.setWireTimeout` continua ativo. -- **RAM livre:** medida e publicada no `inicio` e no `status`. - -### 5.4 Campos novos (todos opcionais) - -| Mensagem | Campos novos | -|---|---| -| `evento: inicio` | `versao: "3.0"`, `reset`, `lcd`, `ram_livre` (os atuais `msg`, `driver` continuam) | -| `type: status` | `ram_livre` | -| `evento: erro` | `fase`, `t_ms`, `bordas_juncao` | - -Novos valores de `tipo` em `evento: erro`: `sem_peca_topo`, `juncao_obstruida`, `motor_sem_avanco`, `peca_presa_saida`. Valores atuais mantidos: `sem_estoque`, `timeout`, `ocupado`, `peca_invalida`, `comando_desconhecido`. - -Compatibilidade conferida no código: o ESP32 repassa a linha JSON inteira (parse em 512 B, MQTT em 1024 B); o servidor só usa `evento` e `peca` (valor desconhecido nas métricas vira `outro`); o dashboard exibe `tipo` como texto e ignora campos desconhecidos. Cada mensagem do Uno deve continuar abaixo de 200 B. - -### 5.5 LCD por situação (16 colunas) - -| Situação | Linha 1 | Linha 2 | -|---|---|---| -| Repouso | `Estoque:` | `A:15 B:15 C:15` | -| Entregando | `Entregando B` | `Aguarda juncao` | -| `motor_sem_avanco` | `ERRO: Motor C` | `Sem avanco` | -| `timeout` | `ERRO: Timeout B` | `Retire a peca` | -| `juncao_obstruida` | `ERRO: Juncao J2` | `Obstruida` | -| `sem_peca_topo` | `ERRO: Topo B` | `Sem peca` | -| `sem_estoque` | `ERRO: Estoque B` | `Zerado` | -| `peca_presa_saida` | `ERRO: Saida B` | `Peca presa` | - -### 5.6 Recuperação após reset - -O sistema volta em AGUARDANDO, motores em 0, estoque em 15. Uma peça esquecida no meio da esteira é pega pela checagem prévia (`juncao_obstruida`) ou pelo M1; nada é debitado às cegas. - -## 6. Testes e validação - -1. **Lógica no PC** (`test/firmware_uno/`): mini-framework de asserts próprio (sem dependência externa), tempo simulado (inclui estouro de `millis()`), compilado com g++ — localmente via Docker (`gcc:14`), no CI em um job novo `firmware-logica` (os jobs atuais não mudam). Cenários obrigatórios, escritos antes do código: - - entrega normal A/B/C (débito na confirmação, saída de 3 s, volta a AGUARDANDO); - - `sem_estoque`, `sem_peca_topo`, `juncao_obstruida` sem ligar o motor; - - M1 → `motor_sem_avanco` sem débito (caso P2); - - M2 com `bordas_juncao: 0` → `timeout` sem débito (caso P1); - - pulso < 20 ms ignorado; ≥ 20 ms confirma; - - M3 → `peca_presa_saida` com débito feito; - - comandos: `ocupado` com a peça pedida, `peca_invalida`, `comando_desconhecido`, RESET em e fora de ERRO, formato exato; - - saída segura em ERRO; - - propriedade: nenhum débito sem confirmação em todos os cenários. -2. **Compilação real**: Uno com `arduino-cli` (local e CI), orçamento de RAM ≤ 65%, ArduinoJson 6 e 7. Suítes atuais do servidor e do frontend como rede de segurança. -3. **Bancada**: roteiro novo em `docs/testes/roteiros/` (boot e causa do reset; leitura de J1–J3 vazias e com peça, calibrando a J2 antes de tudo; uma entrega por esteira; C travada à mão → `motor_sem_avanco` em ~3 s sem reset; timeout; peça presa na saída; junção obstruída; 10 entregas seguidas com `ram_livre` estável). -4. **Documentação**: `docs/ARCHITECTURE.md` (erros, marcos, campos), tabela da FSM no `README.md`, `docs/CHANGELOG.md` e um diagrama Mermaid dos marcos. - -## 7. Riscos - -| Risco | Mitigação | -|---|---| -| RAM insuficiente com módulos e JSON | sem `String`, textos em flash, orçamento de 65% checado na compilação | -| Diferenças entre ArduinoJson 6 e 7 | usar só a API comum às duas (`StaticJsonDocument`, `serializeJson`); compilar com as duas | -| Prazo do M1 curto demais para a esteira real | parâmetro por esteira; calibrar na bancada (passo do roteiro) | -| Causa de reset imprecisa no Optiboot | documentado; o campo distingue ao menos energia, reinício e watchdog | -| Watchdog em laço após reset | desligado no `.init3` | -| Hardware (fiação, IRF520, fonte) segue com defeito | o firmware para o motor rápido e diz onde falhou; a correção física é da equipe |