/*
* ViraVaga / FutureCall - GS 2026.2 - Edge Computing & Computer Systems
* Arthur Sgarbi Sousa - RM 569774
* Bruno Bastos - RM 569434
* Pedro Oliveira - RM 572468
* Posto e vaga ficticios; valores simulados; limites exclusivamente didaticos.
*/
#include <Arduino.h>
#include <DHTesp.h>
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <HTTPClient.h>
#include <time.h>
#include "edge_logic.h"
#include "cloud_config.h"
constexpr int DHT_PIN = 15, GREEN_PIN = 18, YELLOW_PIN = 19, RED_PIN = 21;
constexpr int BUZZER_PIN = 23, FAULT_PIN = 32, OFFLINE_PIN = 33;
constexpr uint32_t READ_MS = 2500, UPLOAD_MS = 20000, RETRY_MS = 10000;
DHTesp dht;
EdgeLogic logic;
uint32_t lastRead = 0, stateSince = 0;
State previousState = State::Fault;
bool previousFault = false;
int lastFrequency = 0;
struct Sample { float t, h; uint8_t state; uint32_t capturedMs; };
QueueHandle_t latestSample;
void updateOutputs(uint32_t now) {
uint32_t elapsed = now - stateSince;
bool fault = logic.state == State::Fault;
bool blink = (elapsed / 300) % 2 == 0;
digitalWrite(GREEN_PIN, logic.state == State::Normal);
digitalWrite(YELLOW_PIN, logic.state == State::Attention || (fault && !blink));
digitalWrite(RED_PIN, logic.state == State::Critical || (fault && blink));
// Critico: bip de 250 ms/segundo; falha: dois bipes curtos a cada 2 segundos.
uint32_t phase = elapsed % 2000;
int frequency = logic.state == State::Critical && elapsed % 1000 < 250 ? 1800 :
fault && (phase < 100 || (phase >= 200 && phase < 300)) ? 900 : 0;
if (frequency != lastFrequency) {
if (frequency) tone(BUZZER_PIN, frequency); else noTone(BUZZER_PIN);
lastFrequency = frequency;
}
}
void publishLocalSnapshot(uint32_t now) {
if (logic.state != previousState) {
stateSince = now;
Serial.printf("TRANSICAO %s -> %s\n", stateName(previousState), stateName(logic.state));
previousState = logic.state;
}
Sample sample{logic.temperature, logic.humidity, static_cast<uint8_t>(logic.state), now};
xQueueOverwrite(latestSample, &sample);
Serial.printf("t=%lu ms | media T=%.2f C UR=%.2f%% | n=%u | %s | offline=%s\n",
static_cast<unsigned long>(now), logic.temperature, logic.humidity, logic.count,
stateName(logic.state), digitalRead(OFFLINE_PIN) == LOW ? "SIM" : "NAO");
}
// Toda operacao de rede fica nesta tarefa; HTTP/DNS nao suspendem o loop local.
void cloudTask(void*) {
WiFi.mode(WIFI_STA);
WiFi.setAutoReconnect(false);
configTime(0, 0, "pool.ntp.org"); // sincronizacao assincrona para validacao TLS
uint32_t lastAttempt = millis() - RETRY_MS, lastUpload = millis();
bool wasOffline = false;
for (;;) {
uint32_t now = millis();
bool offline = digitalRead(OFFLINE_PIN) == LOW;
if (offline) {
if (!wasOffline) { WiFi.disconnect(); Serial.println("REDE: offline forcado; controle local continua."); }
} else {
if (WiFi.status() != WL_CONNECTED && now - lastAttempt >= RETRY_MS) {
lastAttempt = now;
WiFi.begin("Wokwi-GUEST", "", 6);
Serial.println("REDE: tentativa de conexao.");
}
if (CLOUD_ENABLED && WiFi.status() == WL_CONNECTED && now - lastUpload >= UPLOAD_MS) {
lastUpload = now;
Sample s;
if (xQueuePeek(latestSample, &s, 0) == pdTRUE && now - s.capturedMs <= READ_MS * 2) {
if (!strlen(THINGSPEAK_WRITE_KEY) || !strlen(THINGSPEAK_ROOT_CA)) {
Serial.println("NUVEM: configuracao incompleta; nenhum envio.");
} else {
WiFiClientSecure client;
client.setCACert(THINGSPEAK_ROOT_CA);
client.setHandshakeTimeout(5);
HTTPClient http;
http.setConnectTimeout(3000);
http.setTimeout(3000);
if (http.begin(client, "https://api.thingspeak.com/update")) {
http.addHeader("Content-Type", "application/x-www-form-urlencoded");
String body = "api_key=" + String(THINGSPEAK_WRITE_KEY) + "&field3=" + String(s.state);
// Campos ausentes em falha: nao publicar zeros como se fossem medidas.
if (s.state != static_cast<uint8_t>(State::Fault))
body += "&field1=" + String(s.t, 2) + "&field2=" + String(s.h, 2);
int code = http.POST(body);
bool accepted = code == 200 && http.getString().toInt() > 0;
Serial.printf("NUVEM: HTTP %d | %s\n", code, accepted ? "aceito" : "nao confirmado");
http.end();
} else Serial.println("NUVEM: falha ao iniciar HTTPS.");
}
}
}
}
wasOffline = offline;
vTaskDelay(pdMS_TO_TICKS(100)); // cede CPU apenas da tarefa de rede
}
}
void setup() {
Serial.begin(115200);
pinMode(GREEN_PIN, OUTPUT); pinMode(YELLOW_PIN, OUTPUT); pinMode(RED_PIN, OUTPUT);
pinMode(BUZZER_PIN, OUTPUT);
pinMode(FAULT_PIN, INPUT_PULLUP); pinMode(OFFLINE_PIN, INPUT_PULLUP);
dht.setup(DHT_PIN, DHTesp::DHT22);
latestSample = xQueueCreate(1, sizeof(Sample));
Serial.println("ViraVaga | SIMULACAO | aquisicao inicial: FALHA ate primeira leitura valida.");
Serial.println(CLOUD_ENABLED ? "NUVEM habilitada (verificar configuracao)." : "NUVEM desativada: demonstracao local.");
if (!latestSample) { Serial.println("ERRO: sem memoria para fila."); abort(); }
if (xTaskCreatePinnedToCore(cloudTask, "cloud", 8192, nullptr, 1, nullptr, 0) != pdPASS)
Serial.println("REDE indisponivel: tarefa nao criada; controle local continua.");
}
void loop() {
uint32_t now = millis();
bool fault = digitalRead(FAULT_PIN) == LOW;
if (fault && !previousFault) {
logic.accept(NAN, NAN); // injecao explicita de leitura invalida, mesma validacao
Serial.println("TESTE: falha de leitura injetada pela chave.");
publishLocalSnapshot(now);
}
previousFault = fault;
if (now - lastRead >= READ_MS) {
lastRead = now;
TempAndHumidity raw = dht.getTempAndHumidity();
logic.accept(fault ? NAN : raw.temperature, fault ? NAN : raw.humidity);
Serial.printf("BRUTO T=%.2f UR=%.2f | falha_injetada=%s\n", raw.temperature, raw.humidity, fault ? "SIM" : "NAO");
publishLocalSnapshot(now);
}
updateOutputs(now);
}
Falha sensor: esquerda ON
Sem Wi-Fi: esquerda ON