diff --git a/.github/workflows/lint-and-security.yaml b/.github/workflows/lint-and-security.yaml index 5fb798b..ed89b0a 100644 --- a/.github/workflows/lint-and-security.yaml +++ b/.github/workflows/lint-and-security.yaml @@ -114,13 +114,18 @@ 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 - - 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 @@ -192,3 +197,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/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/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..76a796c 100644 --- a/arduino/data_flow_inventory/data_flow_inventory.ino +++ b/arduino/data_flow_inventory/data_flow_inventory.ino @@ -2,704 +2,129 @@ // 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); - -// ============================================================ -// 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(); -} +// 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}; + +// 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; +static uint32_t aberturaMs = 0; +static uint32_t ultimaAmostra = 0; +static uint32_t ultimaPublicacao = 0; +static uint8_t passoPublicacao = 0; -// "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); + // 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 -// 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); + SaidasFsm out; + fsmPasso(fsm, in, out); + if (out.zerarJuncao >= 0) sensorJuncaoZerar((uint8_t)out.zerarJuncao); - // 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); + // 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); - // 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); + for (uint8_t i = 0; i < out.nEventos; i++) publicarEvento(out.eventos[i], fsm, agora); - exibirEstoque(); - - // 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; - } + // LCD: depois da tela de abertura, mostra a tela da FSM (redesenha só se mudou). + 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); + } - // ---------------------------------------------------------- - // 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). + // 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()); - // 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; - } - } -} \ 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..d5f445e --- /dev/null +++ b/arduino/data_flow_inventory/src/hw/diagnostico.cpp @@ -0,0 +1,70 @@ +#include "diagnostico.h" + +#include +#include + +#include "../logica/causa_reset.h" +#include "motores.h" + +namespace { +const uint32_t ASSINATURA = 0xDF1A3B5CUL; +const uint8_t MARCA_WATCHDOG = 0xA5; +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). +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; + motoresPararNaInterrupcao(); // não deixa o motor ligado até o reset +} + +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..eeec678 --- /dev/null +++ b/arduino/data_flow_inventory/src/hw/lcd.cpp @@ -0,0 +1,93 @@ +#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; +} + +// 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]); + if (Wire.getWireTimeoutFlag()) { + Wire.clearWireTimeoutFlag(); + return false; + } + } + for (; i < 16; i++) { + ativo->write(' '); + if (Wire.getWireTimeoutFlag()) { + Wire.clearWireTimeoutFlag(); + return false; + } + } + return true; +} +} // 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; + // 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(); + // 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; +} + +void lcdAtualizar(const char* l1, const char* l2) { + if (ativo == nullptr) return; + if (strncmp(l1, linha1, 16) != 0) { + 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) { + if (!escreverLinha(1, l2)) { + ativo = nullptr; + return; + } + 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..bdb50de --- /dev/null +++ b/arduino/data_flow_inventory/src/hw/motores.cpp @@ -0,0 +1,71 @@ +#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]; +volatile bool paradosPorEmergencia = false; +} // 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 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]) { + analogWrite(PINOS[i], 0); // saída segura a cada ciclo + 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..62f1780 --- /dev/null +++ b/arduino/data_flow_inventory/src/hw/motores.h @@ -0,0 +1,22 @@ +// ============================================================ +// 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(); + +// 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 new file mode 100644 index 0000000..23d99ef --- /dev/null +++ b/arduino/data_flow_inventory/src/hw/sensores.cpp @@ -0,0 +1,193 @@ +#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]; + +// 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]; + +// 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 + +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) + 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(); + // 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]; + 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(); +} + +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; } + +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; + // 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(); +} 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) 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..a83cf3e --- /dev/null +++ b/arduino/data_flow_inventory/src/logica/causa_reset.cpp @@ -0,0 +1,11 @@ +#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"); + // 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/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/arduino/data_flow_inventory/src/logica/fsm.cpp b/arduino/data_flow_inventory/src/logica/fsm.cpp new file mode 100644 index 0000000..a922b51 --- /dev/null +++ b/arduino/data_flow_inventory/src/logica/fsm.cpp @@ -0,0 +1,209 @@ +#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: + // 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; + 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..4831e99 --- /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 = 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 +}; + +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/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/supervisor_entrega.cpp b/arduino/data_flow_inventory/src/logica/supervisor_entrega.cpp new file mode 100644 index 0000000..b77d5c2 --- /dev/null +++ b/arduino/data_flow_inventory/src/logica/supervisor_entrega.cpp @@ -0,0 +1,65 @@ +#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; +} + +// 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 ocupadaHa(j, agora) >= 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. + // 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; + } + 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 = ocupadaHa(j, agora) >= 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/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/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/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 3273e85..b01c87f 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -105,24 +105,61 @@ 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 (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 **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:** +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/CHANGELOG.md b/docs/CHANGELOG.md index df92b3f..789b658 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`, 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 (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 - **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..3119827 100644 --- a/docs/CI-CD.md +++ b/docs/CI-CD.md @@ -99,10 +99,16 @@ 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 -- **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) +- **Falha em:** Erros de compilação ou RAM global do Uno acima de 65% +- **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`) +- **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) + --- @@ -132,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) @@ -248,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): @@ -268,7 +277,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 new file mode 100644 index 0000000..d398369 --- /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** (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) + +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 → 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. 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`. + +## 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. 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 + +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 | | | 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_filtro_reset.cpp b/test/firmware_uno/teste_filtro_reset.cpp new file mode 100644 index 0000000..30df6c0 --- /dev/null +++ b/test/firmware_uno/teste_filtro_reset.cpp @@ -0,0 +1,50 @@ +// 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"); + // 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"); + VERIFICA_TEXTO(causaReset(MCUSR_EXTRF, true, false), "reinicio"); + VERIFICA_TEXTO(causaReset(0, false, true), "energia"); // marca sem assinatura não vale +} diff --git a/test/firmware_uno/teste_fsm.cpp b/test/firmware_uno/teste_fsm.cpp new file mode 100644 index 0000000..7498e6a --- /dev/null +++ b/test/firmware_uno/teste_fsm.cpp @@ -0,0 +1,298 @@ +// 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(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'); + 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_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_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"); +} diff --git a/test/firmware_uno/teste_supervisor.cpp b/test/firmware_uno/teste_supervisor.cpp new file mode 100644 index 0000000..cc99584 --- /dev/null +++ b/test/firmware_uno/teste_supervisor.cpp @@ -0,0 +1,162 @@ +// 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); +} + +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 +} + +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; + 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); +} 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); + } +}