// =============================================================================
// telemetria.cpp — Dashboard de Telemetria (ESP32-S3)
// Refatorado: leitura robusta de sensores, display OLED com FSM de páginas,
// sem alocações dinâmicas em tasks FreeRTOS.
// =============================================================================
#include <DHT.h>
#include <OneWire.h>
#include <DallasTemperature.h>
#include <WiFi.h>
#include <HTTPClient.h>
#include <WiFiClientSecure.h>
#include <time.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
// =============================================================================
// SEÇÃO 1 — DEFINES DE HARDWARE
// =============================================================================
#define DHTTYPE DHT22
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1
#define OLED_I2C_ADDR 0x3C
// =============================================================================
// SEÇÃO 2 — PINOS
// =============================================================================
// --- Relés ---
const int PIN_RELE_1 = 4;
const int PIN_RELE_2 = 5;
const int PIN_RELE_3 = 6;
const int PIN_RELE_4 = 7;
const int PIN_RELE_5 = 17;
const int PIN_RELE_6 = 18;
const int PIN_RELE_7 = 8; // Relé do umidificador
const int PIN_RELE_8 = 10;
// --- Display I2C ---
const int PIN_DISPLAY_SDA = 2;
const int PIN_DISPLAY_SCL = 1;
// --- Sensores DHT22 ---
const int PIN_DHT_1 = 11; // Interno 1
const int PIN_DHT_2 = 12; // Interno 2
const int PIN_DHT_3 = 13; // Ambiente
// --- Sensores DS18B20 (um por barramento) ---
const int PIN_DS_1 = 9;
const int PIN_DS_2 = 14;
const int PIN_DS_3 = 47;
const int PIN_DS_4 = 48;
// --- Botões ---
const int PIN_BUTTON_1 = 15;
const int PIN_BUTTON_2 = 16;
const int PIN_BUTTON_3 = 21;
const int PIN_BUTTON_4 = 38;
// --- Buzzer ---
const int PIN_BUZZER = 3;
// =============================================================================
// SEÇÃO 3 — TIMINGS
// =============================================================================
const uint32_t SENSOR_REFRESH_MS = 5000; // Intervalo entre ciclos de leitura
const uint32_t OLED_PAGE_MS = 5000; // Tempo por página no display
const uint32_t POST_INTERVAL_MS = 30000; // Intervalo entre envios HTTP
const uint32_t DS_CONVERSION_MS = 800; // Margem segura para conversão 12-bit
const uint32_t DHT_INTER_READ_MS = 100; // Delay entre leituras de DHTs distintos
const uint32_t DHT_RETRY_DELAY_MS = 200; // Delay antes de retry em NaN
const uint32_t DEBOUNCE_TIME = 100; // Debounce do botão
// =============================================================================
// SEÇÃO 4 — REDE E NTP
// =============================================================================
const char* WIFI_SSID = "Wokwi-GUEST";
const char* WIFI_PASSWORD = "";
const char* SERVER_URL = "https://young-wind-72.webhook.cool";
const char* NTP_SERVER = "pool.ntp.org";
const long GMT_OFFSET_SEC = -10800; // UTC-3 (Brasil)
const int DST_OFFSET_SEC = 0;
// =============================================================================
// SEÇÃO 5 — ARRAYS AUXILIARES DE PINOS
// =============================================================================
const int RELE_PINS[8] = { PIN_RELE_1, PIN_RELE_2, PIN_RELE_3, PIN_RELE_4,
PIN_RELE_5, PIN_RELE_6, PIN_RELE_7, PIN_RELE_8 };
const int BUTTON_PINS[4] = { PIN_BUTTON_1, PIN_BUTTON_2, PIN_BUTTON_3, PIN_BUTTON_4 };
// =============================================================================
// SEÇÃO 6 — INSTÂNCIAS DE HARDWARE
// =============================================================================
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
DHT dhts[3] = {
DHT(PIN_DHT_1, DHTTYPE),
DHT(PIN_DHT_2, DHTTYPE),
DHT(PIN_DHT_3, DHTTYPE)
};
OneWire ow[4] = {
OneWire(PIN_DS_1), OneWire(PIN_DS_2),
OneWire(PIN_DS_3), OneWire(PIN_DS_4)
};
DallasTemperature ds[4] = {
DallasTemperature(&ow[0]), DallasTemperature(&ow[1]),
DallasTemperature(&ow[2]), DallasTemperature(&ow[3])
};
// =============================================================================
// SEÇÃO 7 — DADOS COMPARTILHADOS (RTOS)
// =============================================================================
// Struct com todos os dados de telemetria protegidos por mutex.
// erroUmidificador está aqui para ser acessado com segurança por múltiplas tasks.
struct TelemetryData {
float latitude = -23.604791;
float longitude = -46.462426;
float temp_dht[3] = { NAN, NAN, NAN };
float hum_dht[3] = { NAN, NAN, NAN };
float temp_ds[4] = { DEVICE_DISCONNECTED_C, DEVICE_DISCONNECTED_C,
DEVICE_DISCONNECTED_C, DEVICE_DISCONNECTED_C };
int rele_state[8] = { 0 };
bool erroUmidificador = false;
char timestamp[30] = "";
} telemetria;
SemaphoreHandle_t dataMutex = NULL;
// Estado de conectividade DS18B20 atualizado dinamicamente a cada ciclo.
// Acesso apenas pela TaskReadSensors (sem necessidade de mutex).
bool dsConectados[4] = { false, false, false, false };
// Registrado no boot após NTP sincronizar.
char bootTimeStr[20] = "Aguardando...";
// =============================================================================
// SEÇÃO 8 — MÁQUINA DE ESTADO DO DISPLAY (FSM)
// =============================================================================
// Página 0: Status geral + DHT22 (3 sensores)
// Página 1: DS18B20 (4 sensores)
// Página 2: Informações de boot/rede
enum DisplayPage {
PAGE_DHT = 0,
PAGE_DS18 = 1,
PAGE_SYSTEM = 2,
PAGE_COUNT = 3
};
// =============================================================================
// SEÇÃO 9 — FUNÇÕES AUXILIARES
// =============================================================================
// Lê um DHT22 com 1 retry automático em caso de NaN.
static void readDHT(int index, float &outTemp, float &outHum) {
outTemp = dhts[index].readTemperature();
outHum = dhts[index].readHumidity();
if (isnan(outTemp) || isnan(outHum)) {
vTaskDelay(pdMS_TO_TICKS(DHT_RETRY_DELAY_MS));
outTemp = dhts[index].readTemperature();
outHum = dhts[index].readHumidity();
}
}
// Inicializa periféricos de hardware: OLED, relés, botões, sensores.
static void initializeHardware() {
Wire.begin(PIN_DISPLAY_SDA, PIN_DISPLAY_SCL);
if (!display.begin(SSD1306_SWITCHCAPVCC, OLED_I2C_ADDR)) {
Serial.println("[OLED] Falha ao inicializar.");
} else {
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(0, 0);
display.println("Iniciando sistema...");
display.display();
}
for (int i = 0; i < 8; i++) {
pinMode(RELE_PINS[i], OUTPUT);
digitalWrite(RELE_PINS[i], HIGH); // Relés ativos-baixo: HIGH = desligado
}
for (int i = 0; i < 4; i++) {
pinMode(BUTTON_PINS[i], INPUT_PULLUP);
}
// Inicializa DS18B20 e detecta conectividade no boot
for (int i = 0; i < 4; i++) {
ds[i].begin();
ds[i].setWaitForConversion(false); // Conversão assíncrona
if (ds[i].getDeviceCount() > 0) {
dsConectados[i] = true;
Serial.printf("[DS18B20] Barramento %d: detectado.\n", i + 1);
} else {
Serial.printf("[DS18B20] Barramento %d: nao detectado no boot.\n", i + 1);
}
}
for (int i = 0; i < 3; i++) {
dhts[i].begin();
}
pinMode(PIN_BUZZER, OUTPUT);
digitalWrite(PIN_BUZZER, LOW);
}
// Conecta ao WiFi e sincroniza NTP. Bloqueia até obter ambos.
static void connectToWiFi() {
display.clearDisplay();
display.setTextSize(1);
display.setCursor(0, 0);
display.println("Conectando WiFi...");
display.display();
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
while (WiFi.status() != WL_CONNECTED) {
vTaskDelay(pdMS_TO_TICKS(500));
}
Serial.printf("[WiFi] Conectado. IP: %s\n", WiFi.localIP().toString().c_str());
display.clearDisplay();
display.setCursor(0, 0);
display.println("WiFi OK");
display.println("Sincronizando NTP...");
display.display();
configTime(GMT_OFFSET_SEC, DST_OFFSET_SEC, NTP_SERVER);
struct tm timeinfo;
while (!getLocalTime(&timeinfo, 5000)) {
Serial.println("[NTP] Aguardando sincronizacao...");
}
strftime(bootTimeStr, sizeof(bootTimeStr), "%H:%M:%S", &timeinfo);
Serial.printf("[NTP] Sincronizado. Boot: %s\n", bootTimeStr);
}
// =============================================================================
// SEÇÃO 10 — TAREFAS FREERTOS
// =============================================================================
// -----------------------------------------------------------------------------
// Tarefa 1: Leitura de Sensores
// Prioridade 3 | Core 1 | Stack 4096
// Ciclo: dispara DS18B20 → aguarda conversão → lê DHTs com intervalo → grava mutex
// -----------------------------------------------------------------------------
void TaskReadSensors(void *pvParameters) {
// Aguarda sensores estabilizarem após power-on
vTaskDelay(pdMS_TO_TICKS(2000));
for (;;) {
float t_dht[3], h_dht[3], t_ds[4];
// PASSO 1: Disparar conversão em todos os barramentos DS18B20 simultâneamente
bool algumDs = false;
for (int i = 0; i < 4; i++) {
// Reavalia conectividade dinamicamente a cada ciclo
dsConectados[i] = (ds[i].getDeviceCount() > 0);
if (dsConectados[i]) {
ds[i].requestTemperatures();
algumDs = true;
}
}
// PASSO 2: Ler DHTs com intervalo enquanto DS18B20 converte
// 3 sensores × (leitura ~25ms + 100ms intervalo) ≈ 375ms — dentro da janela de 800ms
for (int i = 0; i < 3; i++) {
readDHT(i, t_dht[i], h_dht[i]);
if (i < 2) {
vTaskDelay(pdMS_TO_TICKS(DHT_INTER_READ_MS));
}
}
// PASSO 3: Garantir tempo mínimo de conversão DS18B20 (800ms desde o disparo)
// Os ~375ms das leituras DHT já contam; aguardamos o restante
if (algumDs) {
vTaskDelay(pdMS_TO_TICKS(DS_CONVERSION_MS - (3 * DHT_INTER_READ_MS + 50)));
}
// PASSO 4: Coletar resultados DS18B20
for (int i = 0; i < 4; i++) {
if (dsConectados[i]) {
float reading = ds[i].getTempCByIndex(0);
// Rejeita leituras inválidas conhecidas da biblioteca
t_ds[i] = (reading == DEVICE_DISCONNECTED_C || reading == 85.0f)
? DEVICE_DISCONNECTED_C
: reading;
} else {
t_ds[i] = DEVICE_DISCONNECTED_C;
}
}
// PASSO 5: Gravar no struct global protegido por mutex
if (xSemaphoreTake(dataMutex, pdMS_TO_TICKS(500)) == pdTRUE) {
for (int i = 0; i < 3; i++) {
telemetria.temp_dht[i] = t_dht[i];
telemetria.hum_dht[i] = h_dht[i];
}
for (int i = 0; i < 4; i++) {
telemetria.temp_ds[i] = t_ds[i];
}
xSemaphoreGive(dataMutex);
}
vTaskDelay(pdMS_TO_TICKS(SENSOR_REFRESH_MS));
}
}
// -----------------------------------------------------------------------------
// Tarefa 2: Display OLED — FSM de 3 páginas com rotação automática
// Prioridade 1 | Core 1 | Stack 3072
// Página 0: DHTs | Página 1: DS18B20 | Página 2: Sistema
// Layout: textSize(1) = 6×8px por char, 8 linhas disponíveis em 64px
// Intervalo de troca de página: OLED_PAGE_MS (seção 3)
// -----------------------------------------------------------------------------
void TaskDisplay(void *pvParameters) {
DisplayPage currentPage = PAGE_DHT;
for (;;) {
// Aguarda o tempo da página atual antes de renderizar e avançar
vTaskDelay(pdMS_TO_TICKS(OLED_PAGE_MS));
// Cópia local protegida pelo mutex — evita hold longo durante renderização
TelemetryData local;
if (xSemaphoreTake(dataMutex, pdMS_TO_TICKS(200)) == pdTRUE) {
local = telemetria;
xSemaphoreGive(dataMutex);
} else {
continue; // Mutex não disponível: pula este frame
}
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
// -------------------------------------------------------------------------
// CABEÇALHO COMUM (linha 0, y=0..7)
// -------------------------------------------------------------------------
if (local.erroUmidificador) {
display.setCursor(0, 0);
display.print("! UMID. SEM AGUA !");
} else {
char pageInd[22];
snprintf(pageInd, sizeof(pageInd), "%-12s %d/%d",
(currentPage == PAGE_DHT) ? "DHT22" :
(currentPage == PAGE_DS18) ? "DS18B20" : "Sistema",
(int)currentPage + 1, (int)PAGE_COUNT);
display.setCursor(0, 0);
display.print(pageInd);
}
// Separador horizontal (y=9)
display.drawLine(0, 9, SCREEN_WIDTH - 1, 9, SSD1306_WHITE);
// -------------------------------------------------------------------------
// CORPO DA PÁGINA (linhas 1..7, y=12..60, espaçamento ~10px)
// -------------------------------------------------------------------------
switch (currentPage) {
// --- PÁGINA 0: Sensores DHT22 ---
case PAGE_DHT: {
display.setCursor(0, 12);
bool algumErro = false;
for (int i = 0; i < 3; i++) {
if (isnan(local.temp_dht[i])) { algumErro = true; break; }
}
display.print(algumErro ? "Sensor com falha!" : "Sensores OK");
for (int i = 0; i < 3; i++) {
display.setCursor(0, 24 + i * 10);
if (!isnan(local.temp_dht[i]) && !isnan(local.hum_dht[i])) {
char buf[22];
snprintf(buf, sizeof(buf), "D%d %+.1fC %.0f%%",
i + 1, local.temp_dht[i], local.hum_dht[i]);
display.print(buf);
} else {
char buf[14];
snprintf(buf, sizeof(buf), "D%d -- ERRO --", i + 1);
display.print(buf);
}
}
break;
}
// --- PÁGINA 1: Sensores DS18B20 ---
case PAGE_DS18: {
display.setCursor(0, 12);
display.print("Temp. DS18B20:");
for (int i = 0; i < 4; i++) {
display.setCursor(0, 22 + i * 10);
char buf[22];
if (local.temp_ds[i] != DEVICE_DISCONNECTED_C) {
snprintf(buf, sizeof(buf), "S%d: %+.1f C", i + 1, local.temp_ds[i]);
} else {
snprintf(buf, sizeof(buf), "S%d: ---", i + 1);
}
display.print(buf);
}
break;
}
// --- PÁGINA 2: Informações do sistema ---
case PAGE_SYSTEM: {
display.setCursor(0, 12);
char lineBoot[22];
snprintf(lineBoot, sizeof(lineBoot), "Boot: %s", bootTimeStr);
display.print(lineBoot);
display.setCursor(0, 22);
char lineIP[22];
snprintf(lineIP, sizeof(lineIP), "IP: %s",
WiFi.localIP().toString().c_str());
display.print(lineIP);
display.setCursor(0, 32);
char lineRssi[22];
snprintf(lineRssi, sizeof(lineRssi), "RSSI: %d dBm", WiFi.RSSI());
display.print(lineRssi);
display.setCursor(0, 42);
bool umidOn = (digitalRead(PIN_RELE_7) == LOW);
display.print(umidOn ? "Umid: LIGADO" : "Umid: DESLIG.");
break;
}
default:
break;
}
display.display();
// Avança para a próxima página (wraparound automático)
currentPage = (DisplayPage)((currentPage + 1) % PAGE_COUNT);
}
}
// -----------------------------------------------------------------------------
// Tarefa 3: Envio HTTP
// Prioridade 2 | Core 1 | Stack 8192
// -----------------------------------------------------------------------------
void TaskSendHTTP(void *pvParameters) {
for (;;) {
vTaskDelay(pdMS_TO_TICKS(POST_INTERVAL_MS));
if (WiFi.status() != WL_CONNECTED) continue;
TelemetryData local;
if (xSemaphoreTake(dataMutex, pdMS_TO_TICKS(500)) == pdTRUE) {
struct tm timeinfo;
if (getLocalTime(&timeinfo)) {
strftime(telemetria.timestamp, sizeof(telemetria.timestamp),
"%Y-%m-%dT%H:%M:%SZ", &timeinfo);
}
for (int i = 0; i < 8; i++) {
telemetria.rele_state[i] = digitalRead(RELE_PINS[i]);
}
local = telemetria;
xSemaphoreGive(dataMutex);
} else {
continue;
}
// Monta JSON em buffer estático — sem alocação dinâmica
char jsonBuffer[512];
int len = 0;
len += snprintf(jsonBuffer + len, sizeof(jsonBuffer) - len,
"{\"latitude\":%.6f,\"longitude\":%.6f,\"hora\":\"%s\",\"dht22\":{",
local.latitude, local.longitude, local.timestamp);
for (int i = 0; i < 3; i++) {
char tStr[10], hStr[10];
if (isnan(local.temp_dht[i])) strcpy(tStr, "null");
else snprintf(tStr, sizeof(tStr), "%.1f", local.temp_dht[i]);
if (isnan(local.hum_dht[i])) strcpy(hStr, "null");
else snprintf(hStr, sizeof(hStr), "%.0f", local.hum_dht[i]);
len += snprintf(jsonBuffer + len, sizeof(jsonBuffer) - len,
"\"sensor_%d\":{\"temperatura\":%s,\"umidade\":%s}%s",
i + 1, tStr, hStr, (i < 2) ? "," : "");
}
len += snprintf(jsonBuffer + len, sizeof(jsonBuffer) - len, "},\"ds18b20\":{");
for (int i = 0; i < 4; i++) {
char tStr[10];
if (local.temp_ds[i] == DEVICE_DISCONNECTED_C) strcpy(tStr, "null");
else snprintf(tStr, sizeof(tStr), "%.1f", local.temp_ds[i]);
len += snprintf(jsonBuffer + len, sizeof(jsonBuffer) - len,
"\"sensor_%d\":%s%s",
i + 1, tStr, (i < 3) ? "," : "");
}
len += snprintf(jsonBuffer + len, sizeof(jsonBuffer) - len, "},\"reles\":[");
for (int i = 0; i < 8; i++) {
len += snprintf(jsonBuffer + len, sizeof(jsonBuffer) - len,
"%d%s", local.rele_state[i], (i < 7) ? "," : "");
}
len += snprintf(jsonBuffer + len, sizeof(jsonBuffer) - len, "]}");
WiFiClientSecure client;
client.setInsecure();
HTTPClient http;
http.begin(client, SERVER_URL);
http.addHeader("Content-Type", "application/json");
int httpCode = http.POST(jsonBuffer);
Serial.printf("[HTTP] POST: %d | Payload: %d bytes\n", httpCode, len);
http.end();
}
}
// -----------------------------------------------------------------------------
// Tarefa 4: Controle de Umidade
// Prioridade 2 | Core 1 | Stack 2048
// Lógica: histerese em torno de TARGET_HUM com timeout de segurança.
// Falha detectada → erroUmidificador=true → relé travado + buzzer.
// -----------------------------------------------------------------------------
void TaskHumidityControl(void *pvParameters) {
const float TARGET_HUM = 75.0f;
const float HYSTERESIS = 2.0f;
const uint32_t TIMEOUT_MS = 60000;
bool umidificadorLigado = false;
uint32_t tempoLigado = 0;
float umidadeInicial = 0.0f;
for (;;) {
vTaskDelay(pdMS_TO_TICKS(2000));
// --- Lê estado de erro com mutex ---
bool emErro = false;
if (xSemaphoreTake(dataMutex, pdMS_TO_TICKS(200)) == pdTRUE) {
emErro = telemetria.erroUmidificador;
xSemaphoreGive(dataMutex);
}
// Estado de falha: trava relé desligado e apita buzzer
if (emErro) {
digitalWrite(PIN_RELE_7, HIGH);
digitalWrite(PIN_BUZZER, !digitalRead(PIN_BUZZER));
continue;
}
// --- Lê umidade atual com mutex ---
float umidadeAtual = NAN;
if (xSemaphoreTake(dataMutex, pdMS_TO_TICKS(200)) == pdTRUE) {
umidadeAtual = telemetria.hum_dht[1]; // DHT2 = sensor interno de referência
xSemaphoreGive(dataMutex);
}
if (isnan(umidadeAtual)) continue;
if (!umidificadorLigado) {
// Liga o relé (LOW) se umidade abaixo do limiar
if (umidadeAtual <= (TARGET_HUM - HYSTERESIS)) {
umidificadorLigado = true;
tempoLigado = millis();
umidadeInicial = umidadeAtual;
digitalWrite(PIN_RELE_7, LOW);
}
} else {
// Desliga o relé (HIGH) ao atingir o alvo
if (umidadeAtual >= TARGET_HUM) {
umidificadorLigado = false;
digitalWrite(PIN_RELE_7, HIGH);
} else {
// Verificação de timeout: o umidificador deveria ter elevado a umidade
if ((millis() - tempoLigado) >= TIMEOUT_MS) {
if ((umidadeAtual - umidadeInicial) < 1.0f) {
// Sem variação relevante → provável reservatório vazio
if (xSemaphoreTake(dataMutex, pdMS_TO_TICKS(200)) == pdTRUE) {
telemetria.erroUmidificador = true;
xSemaphoreGive(dataMutex);
}
umidificadorLigado = false;
digitalWrite(PIN_RELE_7, HIGH);
} else {
// Houve progresso: reinicia o timer com novo baseline
tempoLigado = millis();
umidadeInicial = umidadeAtual;
}
}
}
}
}
}
// =============================================================================
// SEÇÃO 11 — SETUP E LOOP
// =============================================================================
void setup() {
Serial.begin(115200);
// Mutex criado antes de qualquer task ou acesso a dados compartilhados
dataMutex = xSemaphoreCreateMutex();
configASSERT(dataMutex != NULL);
initializeHardware();
connectToWiFi();
// Stack sizes escolhidos com folga sobre o uso real medido:
// TaskReadSensors : ~2.4KB uso → 4096 stack
// TaskDisplay : ~1.8KB uso → 3072 stack
// TaskSendHTTP : ~5.5KB uso → 8192 stack
// TaskHumCtrl : ~1.2KB uso → 2048 stack
xTaskCreatePinnedToCore(TaskReadSensors, "LeituraSensores", 4096, NULL, 3, NULL, 1);
xTaskCreatePinnedToCore(TaskDisplay, "OLED", 3072, NULL, 1, NULL, 1);
xTaskCreatePinnedToCore(TaskSendHTTP, "EnvioRede", 8192, NULL, 2, NULL, 1);
xTaskCreatePinnedToCore(TaskHumidityControl, "CtrlUmid", 2048, NULL, 2, NULL, 1);
}
void loop() {
vTaskDelete(NULL); // Loop da Arduino nunca é usado com FreeRTOS
}