/*
* Marco 3 - AmparoAzul
* PRODUTOR - le o sensor PIR e publica eventos de presenca no broker MQTT.
*
* Este e um dos DOIS projetos Wokwi que compoem a fronteira de comunicacao
* (dois ESP32 diferentes, duas simulacoes independentes):
*
* [ESTE PROJETO: PRODUTOR] sensor PIR --publish--> broker EMQX Cloud (TLS)
* |
* [OUTRO PROJETO: CONSUMIDOR] LED <--deliver-- (subscribe)
*
* O produtor NUNCA aciona o LED: ele so publica no broker. Quem aciona o
* LED e o outro projeto (consumidor), ao receber a mensagem que volta do
* broker EMQX Cloud (TLS 8883, usuario/senha por dispositivo, ACL por topico), na internet - nao ha nenhuma chamada local entre eles.
*
* Projeto CONSUMIDOR: https://wokwi.com/projects/475979967154638849
*/
#include <WiFi.h>
#include <WiFiClientSecure.h> // Marco 3: TLS no canal
#include "segredos.h" // Marco 3: host e credenciais (fora do Git)
#include <PubSubClient.h>
// --- Componente PRODUTOR: sensor de presenca ---
const int PINO_PIR = 27;
// Parametros de tempo (ajustaveis para teste adversarial conforme matricula)
const unsigned long JANELA_CONFIRMACAO_MS = 2000;
const unsigned long EXPIRACAO_MS = 5000;
const unsigned long DEBOUNCE_MS = 50;
// --- Ponto 7 (alteracao ao vivo): tempo em que o evento e lido/produzido ---
// Duas alavancas independentes, cada uma de uma linha, para reproduzir um
// cenario alternativo sem mexer em mais nada:
// INTERVALO_LEITURA_SENSOR_MS -> a cada quanto tempo o PRODUTOR le o sensor
// ATRASO_PRODUCAO_MS -> atraso proposital entre "evento detectado"
// e "evento publicado no broker" (simula um
// produtor lento / rede congestionada)
const unsigned long INTERVALO_LEITURA_SENSOR_MS = 20;
const unsigned long ATRASO_PRODUCAO_MS = 0;
// --- Rede / Broker MQTT ---
const char* WIFI_SSID = "Wokwi-GUEST";
const char* WIFI_PASSWORD = "";
const char* MQTT_BROKER = MQTT_HOST; // definido em segredos.h
const int MQTT_PORT = 8883; // MQTT sobre TLS
const char* MQTT_TOPIC = "amparoazul/marco2/luquetti-202302608/presenca";
const char* MQTT_CLIENT_ID = "esp32-amparoazul-luquetti-202302608-produtor";
WiFiClientSecure wifiClient; // Marco 3: canal criptografado (TLS)
PubSubClient mqttClient(wifiClient);
enum EstadoPresenca {
SEM_PRESENCA,
PENDENTE,
CONFIRMADA
};
EstadoPresenca estadoAtual = SEM_PRESENCA;
int ultimaLeituraEstavel = LOW;
int ultimaLeituraBruta = LOW;
unsigned long ultimoTempoMudancaBruta = 0;
unsigned long inicioDeteccaoPendente = 0;
unsigned long ultimaEvidenciaMs = 0;
unsigned long sequenciaEvento = 0;
const char* DEVICE_ID = "esp32-consultorio-01";
const char* ENTITY_ID = "presenca-sala";
// --- Versionamento do contrato ---
// O produtor publica com esta versao; o consumidor (outro projeto) valida
// contra a dele. Mudar so esta demonstra, ao vivo, a rejeicao por versao.
const char* CONTRACT_VERSION_PUBLICADA = "1.0";
// ---------------------------------------------------------------------------
// Infraestrutura de rede
// ---------------------------------------------------------------------------
void conectarWiFi() {
Serial.print("Conectando ao WiFi ");
Serial.print(WIFI_SSID);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD, 6); // canal 6: conexao mais rapida no Wokwi
unsigned long inicio = millis();
while (WiFi.status() != WL_CONNECTED && (millis() - inicio) < 15000) {
delay(300);
Serial.print(".");
}
if (WiFi.status() == WL_CONNECTED) {
Serial.println(" conectado!");
Serial.print("IP: ");
Serial.println(WiFi.localIP());
} else {
Serial.println(" falhou (vai tentar de novo no loop).");
}
}
// Marco 3: traduz o codigo de retorno do PubSubClient (evidencia de autenticacao)
const char* descreverRc(int rc) {
switch (rc) {
case -4: return " (timeout)";
case -2: return " (falha de rede/TLS)";
case 4: return " (credenciais invalidas - RECUSADO pelo broker)";
case 5: return " (nao autorizado - RECUSADO pelo broker)";
default: return "";
}
}
void reconectarMQTT() {
if (WiFi.status() != WL_CONNECTED) return;
int tentativas = 0;
while (!mqttClient.connected() && tentativas < 5) {
Serial.print("Conectando ao broker MQTT ");
Serial.print(MQTT_BROKER);
Serial.print(":");
Serial.print(MQTT_PORT);
Serial.print(" (TLS) como ");
Serial.print(MQTT_USER);
Serial.print(" ... ");
wifiClient.stop(); // Marco 3: descarta a sessao TLS antiga antes de reconectar
if (mqttClient.connect(MQTT_CLIENT_ID, MQTT_USER, MQTT_PASS)) {
Serial.println("conectado!");
} else {
tentativas++;
Serial.print("falhou, rc=");
Serial.print(mqttClient.state());
Serial.print(descreverRc(mqttClient.state()));
Serial.print(" tentativa ");
Serial.print(tentativas);
Serial.println("/5");
delay(1000 * tentativas); // backoff exponencial (falha/retry - ponto 5)
}
}
}
// ---------------------------------------------------------------------------
// PRODUTOR: le o sensor e publica no broker
// ---------------------------------------------------------------------------
void publicarEvento(const char* tipoEvento, const char* estado, int valor) {
sequenciaEvento++;
char payload[300];
snprintf(payload, sizeof(payload),
"{\"contractVersion\":\"%s\",\"eventId\":\"%s-%lu\",\"eventType\":\"%s\","
"\"deviceId\":\"%s\",\"entityId\":\"%s\",\"eventTimeMs\":%lu,"
"\"sequence\":%lu,\"value\":%d,\"unit\":\"bool\",\"state\":\"%s\"}",
CONTRACT_VERSION_PUBLICADA, DEVICE_ID, sequenciaEvento, tipoEvento,
DEVICE_ID, ENTITY_ID, millis(),
sequenciaEvento, valor, estado);
Serial.print("[PRODUTOR] publicando no broker: ");
Serial.println(payload);
if (mqttClient.connected()) {
bool ok = mqttClient.publish(MQTT_TOPIC, payload);
if (!ok) {
Serial.println("[PRODUTOR] FALHA ao publicar; tentando reconectar e reenviar...");
reconectarMQTT();
ok = mqttClient.publish(MQTT_TOPIC, payload); // retry do mesmo evento (mesmo eventId)
Serial.println(ok ? "[PRODUTOR] reenvio OK." : "[PRODUTOR] reenvio falhou, evento perdido.");
}
} else {
Serial.println("[PRODUTOR] broker indisponivel; tentando reconectar para entregar...");
reconectarMQTT();
if (mqttClient.connected()) {
bool ok = mqttClient.publish(MQTT_TOPIC, payload);
Serial.println(ok ? "[PRODUTOR] entregue apos reconexao." : "[PRODUTOR] falhou apos reconexao.");
} else {
Serial.println("[PRODUTOR] segue sem broker, evento NAO publicado.");
}
}
}
int lerPirComDebounce() {
int leituraBruta = digitalRead(PINO_PIR);
if (leituraBruta != ultimaLeituraBruta) {
ultimaLeituraBruta = leituraBruta;
ultimoTempoMudancaBruta = millis();
}
if ((millis() - ultimoTempoMudancaBruta) > DEBOUNCE_MS) {
if (leituraBruta != ultimaLeituraEstavel) {
ultimaLeituraEstavel = leituraBruta;
}
}
return ultimaLeituraEstavel;
}
// --- Qualidade de sequencia: valida repetido / fora de ordem / salto (teste adversarial mat. final 8) ---
bool sequenciaEmOrdem(unsigned long recebida, unsigned long ultimaAceita) {
if (recebida <= ultimaAceita) return false;
if (recebida != ultimaAceita + 1) return false;
return true;
}
void rodarSelfTestQualidadeSequencia() {
unsigned long ultimaAceita = 3;
struct { unsigned long seq; const char* rotulo; } casos[] = {
{ 3, "repetido" },
{ 2, "fora_de_ordem" },
{ 7, "salto" },
{ 4, "valido" }
};
Serial.println("--- self-test qualidade de sequencia (adversarial mat. final 8) ---");
for (int i = 0; i < 4; i++) {
bool ok = sequenciaEmOrdem(casos[i].seq, ultimaAceita);
Serial.print("{\"eventType\":\"qualidade.sequencia\",");
Serial.print("\"deviceId\":\""); Serial.print(DEVICE_ID); Serial.print("\",");
Serial.print("\"entityId\":\""); Serial.print(ENTITY_ID); Serial.print("\",");
Serial.print("\"eventTimeMs\":"); Serial.print(millis()); Serial.print(",");
Serial.print("\"caso\":\""); Serial.print(casos[i].rotulo); Serial.print("\",");
Serial.print("\"sequenceRecebida\":"); Serial.print(casos[i].seq); Serial.print(",");
Serial.print("\"ultimaAceita\":"); Serial.print(ultimaAceita); Serial.print(",");
Serial.print("\"state\":\""); Serial.print(ok ? "ACEITO" : "REJEITADO"); Serial.print("\"}");
Serial.println();
if (ok) ultimaAceita = casos[i].seq;
}
Serial.println("--- fim do self-test ---");
}
void setup() {
Serial.begin(115200);
pinMode(PINO_PIR, INPUT);
Serial.println("[PRODUTOR] iniciando (sensoriamento e qualidade)");
rodarSelfTestQualidadeSequencia();
conectarWiFi();
wifiClient.setInsecure(); // TLS sem verificar certificado (limitacao declarada no README)
mqttClient.setServer(MQTT_BROKER, MQTT_PORT);
mqttClient.setBufferSize(512);
mqttClient.setKeepAlive(60); // Marco 3: tolera pausas do Wokwi sem o broker derrubar a sessao
mqttClient.setSocketTimeout(30); // Marco 3: handshake TLS e mais lento
reconectarMQTT();
}
void loop() {
if (WiFi.status() != WL_CONNECTED) {
conectarWiFi();
}
if (!mqttClient.connected()) {
reconectarMQTT();
}
mqttClient.loop();
int leitura = lerPirComDebounce();
unsigned long agora = millis();
if (leitura == HIGH) {
ultimaEvidenciaMs = agora;
}
switch (estadoAtual) {
case SEM_PRESENCA:
if (leitura == HIGH) {
estadoAtual = PENDENTE;
inicioDeteccaoPendente = agora;
}
break;
case PENDENTE:
if (leitura == LOW) {
estadoAtual = SEM_PRESENCA;
} else if ((agora - inicioDeteccaoPendente) >= JANELA_CONFIRMACAO_MS) {
estadoAtual = CONFIRMADA;
Serial.print("[PRODUTOR] evento detectado em t=");
Serial.print(agora);
Serial.println("ms");
if (ATRASO_PRODUCAO_MS > 0) {
Serial.print("[PRODUTOR] atraso de producao configurado: ");
Serial.print(ATRASO_PRODUCAO_MS);
Serial.println("ms (aguardando antes de publicar...)");
delay(ATRASO_PRODUCAO_MS);
}
Serial.print("[PRODUTOR] publicando em t=");
Serial.print(millis());
Serial.println("ms");
publicarEvento("presenca.confirmada", "CONFIRMADA", 1);
}
break;
case CONFIRMADA:
if ((agora - ultimaEvidenciaMs) >= EXPIRACAO_MS) {
estadoAtual = SEM_PRESENCA;
publicarEvento("presenca.invalidada", "SEM_PRESENCA", 0);
}
break;
}
delay(INTERVALO_LEITURA_SENSOR_MS);
}