/*
* PI Cidades Inteligentes e IoT
* Maratona: Anti-trapaça + Desempenho dos corredores
*
* ESP32 = UM PONTO DE LEITURA (marca o início/fim de uma "sessão").
* No mundo real: 1 ESP32 + 1 leitor RFID por ponto do circuito.
* No Wokwi (não existe RFID UHF): os botões simulam a leitura das 2 tags.
*
* Potenciômetro ........ escolhe o corredor (1..10) que está passando
* Botão verde (Tag A) .. leitura da tag A do corredor
* Botão azul (Tag B) .. leitura da tag B do corredor (redundância)
* Botão amarelo ........ troca o ponto do circuito (só no simulador)
* Botão vermelho ....... liga/desliga uma FALHA no sensor
* DHT22 ................ temperatura/umidade (clima da tela pública)
*
* Tudo é publicado via MQTT em JSON (ver GUIA.md).
*/
#include <WiFi.h>
#include <PubSubClient.h>
#include <ArduinoJson.h>
#include <LiquidCrystal_I2C.h>
#include <DHT.h>
#include <sys/time.h>
// ===================== CONFIGURAÇÃO =====================
const char* WIFI_SSID = "Wokwi-GUEST";
const char* WIFI_PASS = "";
const char* MQTT_HOST = "broker.hivemq.com";
const uint16_t MQTT_PORT = 1883;
// TROQUE por algo único do seu grupo (o broker público é aberto a todos!)
const char* BASE = "maratona-pi/CHANGE-ME-12345";
const char* DEVICE_ID = "leitor-sim-01";
const int NUM_CORREDORES = 10;
struct Ponto { const char* nome; float km; };
const Ponto PONTOS[] = {
{"Largada", 0.0f}, {"KM 2.5", 2.5f}, {"KM 5.0", 5.0f},
{"KM 7.5", 7.5f}, {"Chegada", 10.0f}
};
const int NUM_PONTOS = sizeof(PONTOS) / sizeof(PONTOS[0]);
const uint32_t JANELA_MS = 2000; // tempo p/ a 2ª tag aparecer
const uint32_t COOLDOWN_MS = 5000; // ignora releitura do mesmo corredor/ponto
const uint32_t HEARTBEAT_MS = 5000; // "estou vivo" + sinal
const uint32_t CLIMA_MS = 10000;
// ===================== PINOS =====================
#define PIN_POT 34
#define PIN_BTN_A 25
#define PIN_BTN_B 26
#define PIN_BTN_PONTO 27
#define PIN_BTN_FALHA 32
#define PIN_DHT 13
#define PIN_LED_OK 15 // verde
#define PIN_LED_ALERTA 2 // vermelho
#define PIN_LED_REDE 4 // azul
#define PIN_BUZZER 18
// ===================== OBJETOS =====================
WiFiClient wifi;
PubSubClient mqtt(wifi);
LiquidCrystal_I2C lcd(0x27, 16, 2);
DHT dht(PIN_DHT, DHT22);
// ===================== ESTADO =====================
struct Botao { uint8_t pin; bool solto; uint32_t t; };
Botao bA{PIN_BTN_A, true, 0}, bB{PIN_BTN_B, true, 0},
bPonto{PIN_BTN_PONTO, true, 0}, bFalha{PIN_BTN_FALHA, true, 0};
struct Passagem { bool ativa; uint32_t t0; uint64_t ts; bool a, b; int ponto; };
Passagem pas[NUM_CORREDORES + 1];
uint32_t ultimoPubMs[NUM_CORREDORES + 1];
int ultimoPontoPub[NUM_CORREDORES + 1];
int pontoAtual = 0;
bool sensorFalha = false;
uint32_t totalLeituras = 0, falhaTag[2] = {0, 0};
char msgLcd[17] = "";
uint32_t msgAte = 0, okAte = 0, alertaAte = 0;
uint32_t tHeartbeat = 0, tClima = 0, tLcd = 0;
// ===================== UTILIDADES =====================
uint64_t epochMs() {
struct timeval tv;
gettimeofday(&tv, nullptr);
return (uint64_t)tv.tv_sec * 1000ULL + tv.tv_usec / 1000;
}
int corredorSel() {
int v = analogRead(PIN_POT); // 0..4095
int c = 1 + (v * NUM_CORREDORES) / 4096;
return constrain(c, 1, NUM_CORREDORES);
}
bool clicou(Botao &b) {
bool solto = digitalRead(b.pin); // pull-up: HIGH = solto
uint32_t agora = millis();
if (b.solto && !solto && agora - b.t > 50) { b.solto = false; b.t = agora; return true; }
if (!b.solto && solto && agora - b.t > 50) { b.solto = true; b.t = agora; }
return false;
}
void msg(const char *fmt, ...) {
va_list ap;
va_start(ap, fmt);
vsnprintf(msgLcd, sizeof(msgLcd), fmt, ap);
va_end(ap);
msgAte = millis() + 2000;
}
void lcdLinha(uint8_t l, const char *s) {
char b[17];
snprintf(b, sizeof(b), "%-16s", s);
lcd.setCursor(0, l);
lcd.print(b);
}
void pub(const char *sub, JsonDocument &d, bool retain = false) {
if (!mqtt.connected()) return; // (evolução: fila offline)
String t = String(BASE) + "/" + sub;
String out;
serializeJson(d, out);
mqtt.publish(t.c_str(), out.c_str(), retain);
}
// ===================== PUBLICAÇÕES =====================
void enviarStatus(const char *estado) {
JsonDocument d;
d["dispositivo"] = DEVICE_ID;
d["ponto"] = pontoAtual;
d["estado"] = estado;
d["rssi"] = WiFi.RSSI();
d["uptime_s"] = millis() / 1000;
d["leituras"] = totalLeituras;
d["falhas_tagA"] = falhaTag[0];
d["falhas_tagB"] = falhaTag[1];
d["ts"] = epochMs();
String sub = String("status/") + DEVICE_ID;
pub(sub.c_str(), d, true);
}
void alerta(const char *tipo, int corredor, const char *texto) {
JsonDocument d;
d["tipo"] = tipo;
d["corredor"] = corredor;
d["ponto"] = pontoAtual;
d["dispositivo"] = DEVICE_ID;
d["msg"] = texto;
d["ts"] = epochMs();
pub("alertas", d);
alertaAte = millis() + 800;
tone(PIN_BUZZER, 2000, 300);
}
void publicarPassagem(int c) {
Passagem &p = pas[c];
bool ok = p.a && p.b;
JsonDocument d;
d["corredor"] = c;
d["ponto"] = p.ponto;
d["km"] = PONTOS[p.ponto].km;
d["ts"] = p.ts; // instante da 1ª leitura (epoch ms, UTC)
d["tagA"] = p.a;
d["tagB"] = p.b;
d["redundancia_ok"] = ok;
d["dispositivo"] = DEVICE_ID;
pub("leituras", d);
totalLeituras++;
ultimoPubMs[c] = millis();
ultimoPontoPub[c] = p.ponto;
p.ativa = false;
if (ok) {
okAte = millis() + 500;
msg("OK corredor %02d", c);
} else {
falhaTag[p.a ? 1 : 0]++; // contabiliza qual tag falhou
char t[48];
snprintf(t, sizeof(t), "Somente tag %c lida (corredor %d)", p.a ? 'A' : 'B', c);
alerta("TAG_UNICA", c, t);
msg("So tag %c lida", p.a ? 'A' : 'B');
}
}
// ===================== LEITURA DAS TAGS =====================
void lerTag(char tag) {
if (sensorFalha) { msg("SENSOR OFFLINE"); return; }
int c = corredorSel();
uint32_t agora = millis();
if (ultimoPubMs[c] && ultimoPontoPub[c] == pontoAtual &&
agora - ultimoPubMs[c] < COOLDOWN_MS) {
msg("Duplicada ignor.");
return;
}
Passagem &p = pas[c];
if (!p.ativa) {
p.ativa = true; p.t0 = agora; p.ts = epochMs();
p.a = p.b = false; p.ponto = pontoAtual;
}
if (tag == 'A') p.a = true; else p.b = true;
msg("Tag %c c/ %02d", tag, c);
if (p.a && p.b) publicarPassagem(c); // as duas leram: publica já
}
void fecharJanelas() {
for (int c = 1; c <= NUM_CORREDORES; c++)
if (pas[c].ativa && millis() - pas[c].t0 >= JANELA_MS) publicarPassagem(c);
}
// ===================== REDE =====================
bool conectarMqtt() {
String cid = String("pi-") + DEVICE_ID + "-" + String((uint32_t)esp_random(), HEX);
String lwtT = String(BASE) + "/status/" + DEVICE_ID;
String lwtM = String("{\"dispositivo\":\"") + DEVICE_ID + "\",\"estado\":\"offline\"}";
bool ok = mqtt.connect(cid.c_str(), nullptr, nullptr, lwtT.c_str(), 1, true, lwtM.c_str());
if (ok) enviarStatus("ok");
return ok;
}
void garantirRede() {
static uint32_t ultima = 0;
uint32_t agora = millis();
if (WiFi.status() != WL_CONNECTED) {
if (agora - ultima > 5000) { ultima = agora; WiFi.disconnect(); WiFi.begin(WIFI_SSID, WIFI_PASS, 6); }
return;
}
if (!mqtt.connected() && agora - ultima > 3000) { ultima = agora; conectarMqtt(); }
}
// ===================== TELA / LEDs =====================
void atualizarLcd() {
char l0[17], l1[17];
snprintf(l0, sizeof(l0), "P%d %-7s C#%02d", pontoAtual, PONTOS[pontoAtual].nome, corredorSel());
if (sensorFalha) snprintf(l1, sizeof(l1), "SENSOR OFFLINE");
else if (millis() < msgAte) snprintf(l1, sizeof(l1), "%s", msgLcd);
else snprintf(l1, sizeof(l1), "WiFi:%s MQTT:%s",
WiFi.status() == WL_CONNECTED ? "ok" : "--", mqtt.connected() ? "ok" : "--");
lcdLinha(0, l0);
lcdLinha(1, l1);
}
void atualizarLeds() {
digitalWrite(PIN_LED_OK, millis() < okAte);
digitalWrite(PIN_LED_ALERTA, sensorFalha || millis() < alertaAte);
digitalWrite(PIN_LED_REDE, mqtt.connected());
}
// ===================== SETUP / LOOP =====================
void setup() {
Serial.begin(115200);
pinMode(PIN_BTN_A, INPUT_PULLUP);
pinMode(PIN_BTN_B, INPUT_PULLUP);
pinMode(PIN_BTN_PONTO, INPUT_PULLUP);
pinMode(PIN_BTN_FALHA, INPUT_PULLUP);
pinMode(PIN_LED_OK, OUTPUT);
pinMode(PIN_LED_ALERTA, OUTPUT);
pinMode(PIN_LED_REDE, OUTPUT);
pinMode(PIN_BUZZER, OUTPUT);
lcd.init();
lcd.backlight();
dht.begin();
lcdLinha(0, "PI Maratona IoT");
lcdLinha(1, "Conectando WiFi");
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASS, 6);
uint32_t t0 = millis();
while (WiFi.status() != WL_CONNECTED && millis() - t0 < 15000) delay(250);
lcdLinha(1, "Sincronizando...");
configTime(0, 0, "pool.ntp.org", "time.google.com");
t0 = millis();
while (time(nullptr) < 1700000000 && millis() - t0 < 10000) delay(250);
mqtt.setServer(MQTT_HOST, MQTT_PORT);
mqtt.setBufferSize(512);
mqtt.setKeepAlive(15);
}
void loop() {
garantirRede();
mqtt.loop();
if (clicou(bA)) lerTag('A');
if (clicou(bB)) lerTag('B');
if (clicou(bPonto)) {
pontoAtual = (pontoAtual + 1) % NUM_PONTOS;
msg("Ponto: %s", PONTOS[pontoAtual].nome);
}
if (clicou(bFalha)) {
sensorFalha = !sensorFalha;
if (sensorFalha) {
enviarStatus("falha");
alerta("SENSOR_FALHA", 0, "Leitor sem resposta");
} else {
enviarStatus("ok");
msg("Sensor normal");
}
}
fecharJanelas();
uint32_t agora = millis();
if (!sensorFalha && agora - tHeartbeat >= HEARTBEAT_MS) { tHeartbeat = agora; enviarStatus("ok"); }
if (agora - tClima >= CLIMA_MS) {
tClima = agora;
float t = dht.readTemperature(), u = dht.readHumidity();
if (!isnan(t) && !isnan(u)) {
JsonDocument d;
d["temp_c"] = round(t * 10) / 10.0;
d["umidade"] = round(u * 10) / 10.0;
d["ts"] = epochMs();
pub("clima", d, true);
}
}
if (agora - tLcd >= 250) { tLcd = agora; atualizarLcd(); }
atualizarLeds();
}