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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -242,10 +242,10 @@ O Arduino opera com **5 estados**:

| # | Estado | Descrição |
|---|--------|-----------|
| 1 | **AGUARDANDO_PEDIDO** | Sistema em repouso, monitora botões e comandos remotos |
| 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 sensor da junção com timeout de 9s |
| 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 |

> 📊 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).
Expand Down
162 changes: 110 additions & 52 deletions arduino/data_flow_inventory/data_flow_inventory.ino
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
// Máquina de estados com 5 etapas:
// 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.
//
Expand Down Expand Up @@ -83,16 +85,24 @@ LiquidCrystal_I2C lcd(0x27, 16, 2);
// ============================================================
#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). A e B usam o padrão; a C tem
// motor mais forte, então usa um PWM menor para igualar o tempo de entrega.
// Valor da C é ponto de partida: calibrar em bancada (depois da troca do motor).
// 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 140
#define TIMEOUT_ENTREGA 9000 // ms (medição 25/08: peça leva ~5s até o sensor,
// mas precisa de folga extra para sair da esteira secundária)
#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 — intervalo para publicar status periódico
#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
Expand All @@ -118,10 +128,11 @@ const char* nomesEstados[] = {
// ============================================================
Estado estadoAtual = AGUARDANDO_PEDIDO;
int pecaSolicitada = 0; // 1 = A, 2 = B, 3 = C
int estoque[4] = {0, 5, 5, 5}; // índice 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;

Expand All @@ -133,12 +144,20 @@ 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 — a soma (tempo até o sensor + este valor) deve caber no
// TIMEOUT_ENTREGA. Medição 25/08: ~5 s até o sensor.
// 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;
Expand Down Expand Up @@ -271,7 +290,6 @@ void publicarEstado() {
doc["uptime"] = millis() / 1000;
serializeJson(doc, Serial);
Serial.println();
Serial.flush();
}

void publicarEstoque() {
Expand All @@ -282,7 +300,6 @@ void publicarEstoque() {
doc["pecaC"] = estoque[3];
serializeJson(doc, Serial);
Serial.println();
Serial.flush();
}

void publicarSensores() {
Expand Down Expand Up @@ -312,13 +329,17 @@ void publicarEntrega(char peca) {
Serial.println();
}

void publicarErro(const char* tipo) {
// "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 (pecaSolicitada > 0) {
doc["peca"] = String((char)('A' + pecaSolicitada - 1));
if (peca != 0) {
doc["peca"] = String(peca);
}
serializeJson(doc, Serial);
Serial.println();
Expand All @@ -333,16 +354,21 @@ void publicarPedido(char peca) {
Serial.println();
}

// 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);
}

void publicarStatusEsteiras() {
StaticJsonDocument<200> doc;
doc["type"] = "esteiras";
doc["principal"] = 1; // sempre ligada
doc["secA"] = (estadoAtual == ACIONANDO_ESTEIRA && pecaSolicitada == 1) ||
(estadoAtual == ENTREGANDO_PECA && pecaSolicitada == 1) ? 1 : 0;
doc["secB"] = (estadoAtual == ACIONANDO_ESTEIRA && pecaSolicitada == 2) ||
(estadoAtual == ENTREGANDO_PECA && pecaSolicitada == 2) ? 1 : 0;
doc["secC"] = (estadoAtual == ACIONANDO_ESTEIRA && pecaSolicitada == 3) ||
(estadoAtual == ENTREGANDO_PECA && pecaSolicitada == 3) ? 1 : 0;
doc["secA"] = esteiraLigada(1) ? 1 : 0;
doc["secB"] = esteiraLigada(2) ? 1 : 0;
doc["secC"] = esteiraLigada(3) ? 1 : 0;
serializeJson(doc, Serial);
Serial.println();
}
Expand All @@ -363,41 +389,43 @@ void interpretarComando(String linha) {
if (linha.length() == 0) return;

if (linha.startsWith("CMD:PECA:")) {
char pecaChar = linha.charAt(9); // A, B ou C
// 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");
publicarErro("peca_invalida", 0);
return;
}
if (estadoAtual != AGUARDANDO_PEDIDO) {
publicarErro("ocupado");
publicarErro("ocupado", pecaChar);
return;
}
pecaSolicitada = peca;
publicarPedido(pecaChar);
estadoAtual = VERIFICANDO_ESTOQUE;

} else if (linha.startsWith("CMD:RESET")) {
} 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
estadoAtual = AGUARDANDO_PEDIDO;
}
// RESET fora de ERRO: ignorado de propósito (sistema já estável)

} else {
publicarErro("comando_desconhecido");
publicarErro("comando_desconhecido", 0);
}
}

Expand All @@ -421,6 +449,18 @@ void processarComando() {
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
}

// Evita que um barramento I2C travado (ruído do motor) prenda o Uno inteiro.
Wire.setWireTimeout(25000, true);

// Inicializa LCD
lcd.init();
lcd.backlight();
Expand All @@ -430,13 +470,6 @@ void setup() {
// Publica estoque inicial para sincronizar com o dashboard
publicarEstoque();

// Configura pinos dos motores (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
}

// Configura pinos dos sensores
int sensores[] = {
SENSOR_TOPO_A, SENSOR_TOPO_B, SENSOR_TOPO_C,
Expand Down Expand Up @@ -475,16 +508,24 @@ void loop() {
// Processa comandos recebidos do ESP32
processarComando();

// Publicação periódica de status.
// 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) {
if (millis() - ultimaPublicacao >= INTERVALO_PUBLICACAO / PASSOS_PUBLICACAO) {
ultimaPublicacao = millis();
publicarEstado();
publicarEstoque();
publicarSensores();
publicarStatusEsteiras();
switch (passoPublicacao) {
case 0: publicarEstado(); break;
case 1: publicarEstoque(); break;
case 2: publicarSensores(); break;
default: publicarStatusEsteiras(); break;
}
passoPublicacao = (passoPublicacao + 1) % PASSOS_PUBLICACAO;
}

switch (estadoAtual) {
Expand Down Expand Up @@ -521,13 +562,14 @@ void loop() {

if (temPeca && estoque[pecaSolicitada] > 0) {
atualizarLCD("Separando:", nomePeca);
publicarEstado();
estadoAtual = ACIONANDO_ESTEIRA;
publicarEstado();
} else {
atualizarLCD("ERRO: Sem estoque", nomePeca);
atualizarLCD("ERRO:Sem estoque", nomePeca); // 16 colunas
erroSemEstoque = true;
publicarErro("sem_estoque");
estadoAtual = ERRO;
publicarErro("sem_estoque", (char)('A' + pecaSolicitada - 1));
publicarEstado();
}
break;
}
Expand All @@ -541,6 +583,9 @@ void loop() {
if (pecaSolicitada == 3) ligarEsteiraC();

tempoInicio = millis();
juncaoLiberada = false;
juncaoUltima = false;
juncaoMudouEm = millis();
publicarStatusEsteiras();
estadoAtual = ENTREGANDO_PECA;
break;
Expand All @@ -557,10 +602,10 @@ void loop() {
if (millis() - tempoLimpezaEntrega >= PAUSA_POS_ENTREGA_MS) {
aguardandoLimpezaEntrega = false;
pecaSolicitada = 0;
estadoAtual = AGUARDANDO_PEDIDO;
exibirEstoque();
publicarEstado();
publicarStatusEsteiras();
estadoAtual = AGUARDANDO_PEDIDO;
}
break;
}
Expand Down Expand Up @@ -602,18 +647,33 @@ void loop() {
if (pecaSolicitada == 2) pecaChegou = sensorJuncaoJ2();
if (pecaSolicitada == 3) pecaChegou = sensorJuncaoJ3();

if (pecaChegou) {
// 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 nunca chegou ao sensor de junção
// 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;
publicarErro("timeout");
atualizarLCD("ERRO: Timeout", "Aguarde comando");
estadoAtual = ERRO;
publicarErro("timeout", (char)('A' + pecaSolicitada - 1));
publicarEstado();
publicarStatusEsteiras();
}
break;
}
Expand All @@ -622,11 +682,9 @@ void loop() {
// ESTADO 5: ERRO
// ----------------------------------------------------------
case ERRO: {
if (erroTimeout) {
atualizarLCD("ERRO: Timeout", "Aguarde comando");
} else if (erroSemEstoque) {
// Mantém mensagem de sem estoque
}
// A mensagem do LCD é desenhada UMA vez, na transição para ERRO (timeout ou
// sem estoque). Redesenhar a cada volta do loop (lcd.clear() + Strings)
// fazia o display piscar e fragmentava o heap do Uno (2 KB de RAM).

// Reset via botão físico: DESABILITADO
// Para reativar, descomente:
Expand Down
20 changes: 12 additions & 8 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,14 +108,18 @@ Linhas que não são JSON (logs de debug do Arduino) só aparecem no monitor ser
**5 estados:**
1. **AGUARDANDO_PEDIDO** → recebe comando
2. **VERIFICANDO_ESTOQUE** → verifica se há peça
3. **ACIONANDO_ESTEIRA** → liga motor, aguarda sensores
4. **ENTREGANDO_PECA** → pausa, decrementa, publica
5. **ERRO** → timeout ou rejeição (requer CMD:RESET)

**Rejeições explícitas:**
- `peca_indisponivel` — Peça não existe (ex.: "C" sem estoque)
- `ocupado` — Comando durante acionamento
- `comando_desconhecido` — Inválido
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)

**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)

**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
- `comando_desconhecido` — linha não reconhecida

**Diagramas:**

Expand Down
Loading
Loading