Este projeto tem como objetivo o desenvolvimento de uma estação de monitoramento de temperatura utilizando sensores DS18B20 e ESP32. A estação é capaz de ler múltiplas temperaturas simultaneamente e, com base em parâmetros configuráveis, acionar relés de forma automática. O controle e visualização são realizados por meio de uma interface web responsiva, hospedada no próprio ESP32, com comunicação em tempo real via WebSocket.
1x Módulo WiFi ESP32 Bluetooth com Display OLED – HW-724
2x Sensor de Temperatura DS18B20 a Prova D’água (CI MY18E20)
1x Módulo Relé de Estado Sólido SSR 2 Canais
1x Protoboard 400 Pontos
1x Conector Borne KRE 3 Vias
1x Resistor 4K7 1/4W (10 Unidades)
1x Jumpers – Macho/Macho – 65 Unidades
1x Kit com 140 Jumpers Rígidos em U para Protoboard
Opcional
1x Display OLED 128×64 0.96″ I2C – Pinos Soldados (para ESP32 sem display nativo)
Sistemas de automação baseados em temperatura são amplamente utilizados em aplicações domésticas e industriais, como em estufas, tanques de aquecimento, refrigeração e fermentação. A proposta deste projeto é fornecer uma solução personalizável e acessível, com interface amigável e de fácil configuração, que possa funcionar com ou sem conexão com a Internet.
Objetivo: Em estufas agrícolas, controlar a temperatura é essencial para manter o ambiente ideal para o crescimento de plantas.
Aplicação:
Se a temperatura cair abaixo do mínimo, o sistema pode ligar um aquecedor automaticamente.
Se a temperatura subir acima do máximo, pode acionar exaustores ou abrir venezianas para reduzir o calor.
Importância:
Mantém a faixa ideal para germinação, floração e frutificação.
Evita estresse térmico que pode prejudicar as plantas.
Objetivo: Em tanques que precisam de temperatura constante (como tanques de peixes, sistemas industriais ou boilers de água), o controle é fundamental.
Aplicação:
Quando a água esfria demais, um aquecedor pode ser ligado automaticamente.
Quando superaquece, o sistema pode desligar o aquecedor ou até ativar mecanismos de resfriamento.
Importância:
Em aquários, a temperatura errada pode matar os peixes.
Em processos industriais, pode comprometer a qualidade de produtos ou causar falhas no sistema.
Objetivo: Manter produtos perecíveis (como alimentos ou medicamentos) em temperaturas específicas.
Aplicação:
Se a temperatura subir acima do máximo, o sistema aciona um compressor ou aumenta a potência de refrigeração.
Se a temperatura cair abaixo do mínimo, pode desligar o compressor para evitar congelamento indevido.
Importância:
Mantém alimentos seguros, evita perdas financeiras.
Em laboratórios, preserva a eficácia de medicamentos e amostras biológicas.
Objetivo: Durante a fermentação, a temperatura influencia diretamente a qualidade do produto.
Aplicação:
Se a fermentação esquentar demais (o que pode acontecer naturalmente), um resfriamento pode ser acionado (tipo uma serpentina com água gelada).
Se esfriar além do ideal, aquecedores são ativados.
Importância:
Controla o tipo de aroma e sabor das bebidas fermentadas.
Evita a morte das leveduras ou produção de subprodutos indesejados.
A placa LoLin32 é uma variante do ESP32 com suporte a displays OLED 128×64 integrados. Isso facilita o desenvolvimento de aplicações embarcadas que precisam de uma saída visual local, como exibição de temperatura em tempo real, mensagens de status e alarmes.
Principais características:

Figura 1 – Pinagem do ESP32 LoLin32 OLED
O DS18B20 é um sensor digital de temperatura com protocolo de comunicação OneWire, ideal para medições em ambientes úmidos (versão à prova d’água). Suporta encadeamento de múltiplos sensores no mesmo barramento e tem resolução configurável de 9 a 12 bits, com precisão de até ±0.5 °C. Cada sensor tem um número serial único que pode ser obtido através de API e serve para diferenciar em caso de um barramento maior com múltiplos sensores.

Figura 2 – Sensor DS18B20 a Prova D’Água
{
"modo": "WiFi",
"alias": "termostato",
"ssid": "Brsky fibra Dom_2.4G",
"senha": "########",
"varredura": 5000,
"ntpServer": "pool.ntp.org",
"timezone": "<-03>3",
"resolucao": 12,
"user_OTA": "admin",
"pass_OTA": "#####",
"autoRebootOTA": true,
"sensores": [
{
"id": "#1",
"desc": "Sensor 1",
"min": 27,
"acaoMin": "Desligar",
"max": 30,
"acaoMax": "Ligar",
"rele": 1,
"hab": true
},
{
"id": "#2",
"desc": "Sensor 2",
"min": 20,
"acaoMin": "Ligar",
"max": 30,
"acaoMax": "Desligar",
"rele": 2,
"hab": true
},
{
"id": "",
"desc": "Sensor 1",
"min": 20,
"acaoMin": "Desligar",
"max": 30,
"acaoMax": "Ligar",
"rele": 1,
"hab": true
}
]
}//-------------------------------------------------------
// Estrutura para representar os parâmetros dos sensores
//-------------------------------------------------------
struct SensorConfig
{
String idSensor;
String descricao;
float minTemp;
String acaoMin; // "Ligar" ou "Desligar"
float maxTemp;
String acaoMax;
int releID;
bool habilitado;
};
//-------------------------------------------------------
// Estrutura para representar os parâmetros no SPIFFS
//-------------------------------------------------------
struct AppConfig
{
std::vector<SensorConfig> sensores;
unsigned long varredura;
String modo;
String alias;
int resolucao;
String ntpServer;
String timezone;
String ssid;
String senha;
String user_OTA;
String pass_OTA;
bool autoRebootOTA;
};
Figura 3 – Seleção da Placa para Compilação

Figura 4 – Circuito em Bancada

Figura 5 – Diagrama do Circuito

Figura 6 – Tela de Medições

Figura 7 – Tela de Parâmetros

Figura 8 – Tela Controle Relés

Figura 9 – Tela Autenticação Atualização

Figura 10 – Tela Atualização
#include <WiFi.h> // Biblioteca para a rede wifi #include <AsyncTCP.h> // Biblioteca usada pelo Servidor Assíncrono #include <ESP32Ping.h> // Biblioteca Ping #include <ESPAsyncWebServer.h> // Biblioteca para Servidor Web Assíncrono #include <ESPmDNS.h> // Biblioteca para adiocionar aliases no DNS da Rede Local #include <ArduinoJson.h> // Biblioteca para manipulação de estrutiras JSON #include <SPIFFS.h> // Biblioteca que implementa o filesystem #include <ArduinoJson.h> // Biblioteca para manipulação de estrutiras JSON #include <SPI.h> // Biblioteca para interface com periféricos #include <Wire.h> // Biblioteca para comunicação I2C #include <Adafruit_GFX.h> // Biblioteca para Display OLED #include <Adafruit_SSD1306.h> // Biblioteca para o Display OLED #include <Fonts/FreeSerif12pt7b.h> // Biblioteca de fontes para dipslay OLED #include <OneWire.h> // Bublioteca para comunicação ONE-WIRE #include <DallasTemperature.h> // Biblioteca para tratar os sensores DS18D20 #include <ElegantOTA.h> // Biblioteca para atualização via Web #include "esp_system.h" // Bibliotecas para consumo de recursos
//-----------------------------------------------------------------------------------------
// Função : Este programa tem como objetivo implementar uma estação de monitoramento
// de temperatura utilizando múltiplos sensores DS18B20 e, adicionalmente,
// permitir o acionamento de relés de acordo com a faixa de MÍNIMO e MÁXIMO
// de temperatura por sensor. A aplicação implementa uma interface HTML
// numa aplicação AsyncWebServer para ESP32 com interação com o Navegador
// via WebSocket.
//
// Funções adicionais
//
// 1) Inclusão de Alias no DNS da Rede Local (mDNS)
// 2) Pode operar no Modo AP (desconectado da Internet) ou no Modo WiFi (conectado)
// 3) Sincronização do Relógio interno com o Serviço NTP quando no Modo WiFi
// ou sincronismo com o relógio do Navegador quando no Modo AP
// 4) Atualização do Código via Interface Web (ElegantOTA)
// 5) Modo de Configuração dos Parãmetros
// 6) Suporta N x Relés e N x Sensores, cada sensor associado a um relé
// 7) Compilação condicional para ESP32, ESP32C3 e ESP32S3
//
// Referências
//
// 1) https://randomnerdtutorials.com/esp32-built-in-oled-ssd1306/
// 2) https://blog.eletrogate.com/guia-completo-sobre-sensor-de-temperatura-ds18b20-a-prova-dagua/
// 3) https://blog.eletrogate.com/automatizacao-do-processo-de-brassagem/
// 4) https://blog.eletrogate.com/guia-completo-do-display-oled-parte-2-como-programar-3/
//
// Autor : Dailton Menezes
// Versão : 1.0 Abr/2025
//-----------------------------------------------------------------------------------------
#include <WiFi.h> // Biblioteca para a rede wifi
#include <AsyncTCP.h> // Biblioteca usada pelo Servidor Assíncrono
#include <ESP32Ping.h> // Biblioteca Ping
#include <ESPAsyncWebServer.h> // Biblioteca para Servidor Web Assíncrono
#include <ESPmDNS.h> // Biblioteca para adiocionar aliases no DNS da Rede Local
#include <ArduinoJson.h> // Biblioteca para manipulação de estrutiras JSON
#include <SPIFFS.h> // Biblioteca que implementa o filesystem
#include <ArduinoJson.h> // Biblioteca para manipulação de estrutiras JSON
#include <SPI.h> // Biblioteca para interface com periféricos
#include <Wire.h> // Biblioteca para comunicação I2C
#include <Adafruit_GFX.h> // Biblioteca para Display OLED
#include <Adafruit_SSD1306.h> // Biblioteca para o Display OLED
#include <Fonts/FreeSerif12pt7b.h> // Biblioteca de fontes para dipslay OLED
#include <OneWire.h> // Bublioteca para comunicação ONE-WIRE
#include <DallasTemperature.h> // Biblioteca para tratar os sensores DS18D20
#include <ElegantOTA.h> // Biblioteca para atualização via Web
#include "esp_system.h" // Bibliotecas para consumo de recursos
//-------------------------------------
// Define os Pinos usados pelo programa
//-------------------------------------
#if defined(CONFIG_IDF_TARGET_ESP32C3) // ESP32C3 Mini
#define pinBUS 4 // Pino do barramento de sensores
#define pinRELE1 2 // Pino do Relé 1
#define pinRELE2 3 // Pino do Relé 2
#elif defined(CONFIG_IDF_TARGET_ESP32S3) // XIAO ESP32S3
#define pinBUS 4 // Pino do barramento de sensores
#define pinRELE1 2 // Pino do Relé 1
#define pinRELE2 3 // Pino do Relé 2
#elif defined(ARDUINO_LOLIN32) // ESP32 WEMOS LOLIN32 com OLED
#define pinBUS 16 // Pino do barramento de sensores
#define pinRELE1 25 // Pino do Relé 1
#define pinRELE2 26 // Pino do Relé 2
#elif defined(CONFIG_IDF_TARGET_ESP32) // ESP32 genérico
#define pinBUS 4 // Pino do barramento de sensores
#define pinRELE1 2 // Pino do Relé 1
#define pinRELE2 3 // Pino do Relé 2
#endif
//--------------------------------
// Definições Gerais
//--------------------------------
#define MAX_RELES 2 // Números de Relés
#define RELE_ON LOW // Estado para ligar o Relé
#define RELE_OFF HIGH // Estado para desligar o Relé
#define MAX_SENSORES 5 // N. máximo de DS18B20 no barramento
#define INTERVALO_CLEANUP 60000 // Intervalo entre de CleanUP
#define SCREEN_WIDTH 128 // Resolução horizontal do Display
#define SCREEN_HEIGHT 64 // Resolução vertical do display
#define INTERVALO_SCAN 15000 // Máximo intervalo de SCAN em ms
//-------------------------------
// Definições para WebServer
//-------------------------------
AsyncWebServer server(80);
AsyncWebSocket ws("/ws");
//------------------------------------
// Configuração para os Sensores
//------------------------------------
OneWire oneWire(pinBUS); // Prepara uma instância oneWire para comunicar com qualquer outro dispositivo oneWire
DallasTemperature sensors(&oneWire); // Passa uma referência oneWire para a biblioteca DallasTemperature
int nDevices = 0; // N. de dispositivos
DeviceAddress deviceAddr; // Endereço do dispositivo
//--------------------------------
// Configuração do Display OLED
//--------------------------------
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);
//-------------------------
// Configuração dos Relés
//-------------------------
int pinReles[MAX_RELES] = {pinRELE1, pinRELE2};
bool statusReles[MAX_RELES] = {false, false};
//-------------------------------
// Controles de Tempo
//-------------------------------
unsigned long lastScan = 0;
unsigned long intervaloVarredura = 5000;
unsigned long tempoExibicao = 1000;
unsigned long tempoUltimaExibicao = 0;
unsigned long lastCleanup = 0;
//--------------------------
// Controle da Temperatura
//--------------------------
float temperaturas[MAX_SENSORES] = {};
bool leituraPronta=false;
int sensorIndex=0;
//---------------------------------
// Controle do ScanWiFi assíncrono
//---------------------------------
bool iniciarScanSSID = false;
bool scanEmAndamento = false;
unsigned long tempoScanInicio = 0;
int resultadoScan = -1;
AsyncWebSocketClient *clienteScanSSID = nullptr;
//--------------------------------
// Controle Sincronismo NTP Server
//---------------------------------
IPAddress ip (1, 1, 1, 1); // The remote ip to ping, DNS do Google
//-------------------------------
// Nome do arquivo de parâmetros
//-------------------------------
#define CONFIG_PATH "/config.json"
//-------------------------------------------------------
// Estrutura para representar os parâmetros dos sensores
//-------------------------------------------------------
struct SensorConfig
{
String idSensor;
String descricao;
float minTemp;
String acaoMin; // "Ligar" ou "Desligar"
float maxTemp;
String acaoMax;
int releID;
bool habilitado;
};
//-------------------------------------------------------
// Estrutura para representar os parâmetros no SPIFFS
//-------------------------------------------------------
struct AppConfig
{
std::vector<SensorConfig> sensores;
unsigned long varredura;
String modo;
String alias;
int resolucao;
String ntpServer;
String timezone;
String ssid;
String senha;
String user_OTA;
String pass_OTA;
bool autoRebootOTA;
};
AppConfig appConfig;
//-------------------------
// Prototipação de Rotinas
//-------------------------
bool carregarConfig();
void setupWiFi();
void onWsEvent(AsyncWebSocket *server, AsyncWebSocketClient *client,
AwsEventType type, void *arg, uint8_t *data, size_t len);
void setupServer();
bool setDNSNAME(String nome);
void setupSensores();
void processaSensores();
void printAddress(const DeviceAddress deviceAddress);
void newAlarmHandler(const uint8_t* deviceAddress);
void printCurrentTemp(DeviceAddress deviceAddress);
void printTemp(const DeviceAddress deviceAddress);
void printAlarmInfo(const DeviceAddress deviceAddress);
int getDeviceIndex(const DeviceAddress deviceAddress);
void centerText(String text, int textSize);
void showError();
void salvarConfig();
void deleteFile(const char* path);
void displayTemperatura(int index);
void aplicarParametros();
String getTimeStamp();
bool getNTPtime(int sec);
void enviarStatusParaTodosClientes();
void alterarEstadoRele(int releIndex, bool estado);
void printFreeRAM(String context);
//--------------------
// HTML da Aplicação
//--------------------
const char index_html[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html lang="pt-br">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>Termostato V1.0</title>
<style>
* {
box-sizing: border-box;
}
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
background-color: #f0f0f0;
}
header {
background-color: #000;
color: #fff;
padding: 10px 20px;
text-align: center;
}
nav {
display: flex;
background-color: #333;
overflow: auto;
}
nav button {
flex: 1;
padding: 14px;
background-color: #333;
color: white;
border: none;
cursor: pointer;
font-size: 16px;
}
nav button.active {
background-color: #666;
}
.tabcontent {
display: none;
padding: 20px;
}
.tabcontent.active {
display: block;
}
@media (max-width: 600px) {
nav button {
font-size: 14px;
padding: 12px;
}
}
/* === SENSOR CARD === */
.sensor-card {
background-color: #fff;
border: 1px solid #ccc;
border-radius: 10px;
box-shadow: 2px 2px 6px rgba(0,0,0,0.1);
max-width: 400px;
margin: 15px auto;
padding: 10px 15px;
font-family: Arial, sans-serif;
}
.sensor-card.alarme-baixo {
border: 2px solid #007BFF; /* Azul */
}
.sensor-card.alarme-alto {
border: 2px solid #dc3545; /* Vermelho */
}
.sensor-header {
font-size: 13px;
display: flex;
justify-content: space-between;
margin-bottom: 4px;
color: #555;
}
.sensor-id {
font-size: 24px;
font-weight: bold;
text-align: center;
margin: 4px 0;
}
.sensor-temp {
font-size: 28px;
font-weight: bold;
text-align: center;
margin: 6px 0;
}
.sensor-temp.azul {
color: blue;
}
.sensor-temp.verde {
color: green;
}
.sensor-temp.vermelho {
color: red;
}
.sensor-footer {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 13px;
margin-top: 10px;
color: #444;
white-space: nowrap;
}
.sensor-footer span {
flex: 1;
text-align: center;
}
.sensor-footer .min-temp {
text-align: left;
}
.sensor-footer .max-temp {
text-align: right;
}
.sensor-footer .timestamp {
white-space: nowrap;
}
.sensor-alerta {
background-color: #ffc107;
color: #000;
text-align: center;
font-weight: bold;
padding: 4px 6px;
margin-bottom: 6px;
border-radius: 6px;
}
h4 {
margin-top: 8px;
margin-bottom: 6px;
font-size: 18px;
font-weight: bold;
}
/* === FORMS / SELECT === */
select {
width: 95%;
}
</style>
</head>
<body>
<header>
<h2>Termostato V1.0 Abr/2025</h2>
</header>
<nav>
<button onclick="openTab('medicoes')" class="active">Medições</button>
<button onclick="openTab('parametros')">Parâmetros</button>
<button onclick="openTab('reles')">Relés</button>
</nav>
<div id="medicoes" class="tabcontent active">
<div id="painelMedicoes">
<!-- Painéis dos sensores serão inseridos via JavaScript -->
</div>
</div>
<div id="parametros" class="tabcontent">
<!-- 🔷 Parâmetros por Sensor -->
<div style="border: 1px solid #ccc; border-radius: 10px; padding: 15px; background-color: #fff; margin-bottom: 20px;">
<h4>Parâmetros por Sensor</h4>
<form id="formSensor">
<label for="sensorID">Sensor ID:</label><br>
<select id="sensorID" onchange="carregarSensorConfig()" style="width: 95%;"></select>
<br><br>
<table style="width: 100%; border-collapse: collapse;">
<tr>
<td>
<label for="descricao">Descrição:</label><br>
<input type="text" id="descricao" value="" style="width: 95%;">
</td>
<td>
<label for="minTemp">Min Temp (°C):</label><br>
<input type="number" step="0.1" id="minTemp" value="" style="width: 95%;">
</td>
</tr>
<tr>
<td>
<label for="acaoMin">Ação Min:</label><br>
<select id="acaoMin" style="width: 95%;">
<option value="Ligar">Ligar</option>
<option value="Desligar">Desligar</option>
</select>
</td>
<td>
<label for="maxTemp">Max Temp (°C):</label><br>
<input type="number" step="0.1" id="maxTemp" value="" style="width: 95%;">
</td>
</tr>
<tr>
<td>
<label for="acaoMax">Ação Max:</label><br>
<select id="acaoMax" style="width: 95%;">
<option value="Ligar">Ligar</option>
<option value="Desligar">Desligar</option>
</select>
</td>
<td>
<label for="releID">Relé ID:</label><br>
<select id="releID" style="width: 95%;"></select>
</td>
</tr>
<tr>
<td colspan="2">
<label>
<input type="checkbox" id="habilitado"> Habilitado
</label>
</td>
</tr>
</table>
<div style="margin-top: 10px; text-align: right;">
<button type="button" onclick="salvarSensorConfig()">Salvar Sensor</button>
</div>
</form>
</div>
<!-- 🟨 Parâmetros Globais -->
<div style="border: 1px solid #ccc; border-radius: 10px; padding: 15px; background-color: #fff;">
<h4>Parâmetros Globais da Aplicação</h4>
<form id="formApp">
<table style="width: 100%; border-collapse: collapse;">
<tr>
<td>
<label for="varredura">Varredura (ms):</label><br>
<input type="number" id="varredura" value="" style="width: 95%;">
</td>
<td>
<label for="modo">Modo:</label><br>
<select id="modo" style="width: 95%;">
<option value="WiFi">WiFi</option>
<option value="AP">AP</option>
</select>
</td>
</tr>
<tr>
<td>
<label for="alias">Alias mDNS:</label><br>
<input type="text" id="alias" value="" style="width: 95%;">
</td>
<td>
<label for="resolucao">Resolução (9–12):</label><br>
<input type="number" id="resolucao" min="9" max="12" value="" style="width: 95%;">
</td>
</tr>
<tr>
<td>
<label for="ntpServer">Servidor NTP:</label><br>
<input type="text" id="ntpServer" value="" style="width: 95%;">
</td>
<td>
<label for="timezone">Fuso horário:</label><br>
<input type="text" id="timezone" value="" style="width: 95%;">
</td>
</tr>
<tr>
<td>
<label for="ssid">SSID WiFi:</label><br>
<select id="ssid" style="width: 95%;"></select>
</td>
<td>
<label for="senha">Senha:</label><br>
<input type="password" id="senha" value="" style="width: 95%;">
</td>
</tr>
<tr>
<td>
<label for="user_OTA">Usuário OTA:</label><br>
<input type="text" id="user_OTA" value="" style="width: 95%;">
</td>
<td>
<label for="pass_OTA">Senha OTA:</label><br>
<input type="password" id="pass_OTA" value="" style="width: 95%;">
</td>
</tr>
<tr>
<td colspan="2">
<label><input type="checkbox" id="autoRebootOTA"> Reboot após OTA</label>
</td>
</tr>
</table>
<div style="margin-top: 10px; text-align: center;">
<button type="button" onclick="salvarAppConfig()">Salvar Configuração</button>
<button type="button" onclick="cancelarParametros()">Cancelar</button>
<button type="button" onclick="sincronizarRelogio()">Sincronizar</button>
</div>
</form>
</div>
</div>
<div id="reles" class="tabcontent">
<div id="painelControles">
<!-- Painel de controle manual -->
<div style="margin-bottom: 20px; border: 1px solid #ccc; border-radius: 10px; padding: 15px; background-color: #fff;">
<h4>Controle Manual</h4>
<label for="releManual">Relé:</label>
<select id="releManual"></select>
<div style="margin-top: 10px;">
<button onclick="comandoReleManual('ligar')">Ligar</button>
<button onclick="comandoReleManual('desligar')">Desligar</button>
</div>
</div>
</div>
<div id="painelStatus">
<!-- Painel de status dos relés -->
<div style="border: 1px solid #ccc; border-radius: 10px; padding: 15px; background-color: #fff;">
<h4>Status dos Relés</h4>
<div id="statusReles">
<!-- Preenchido via WebSocket -->
</div>
</div>
</div>
</div>
<script>
let socket;
let ssidPersistido = "";
function conectarSocket() {
// Antes de abrir um novo socket, sempre verifica se já existe
if (!socket || socket.readyState === WebSocket.CLOSED) {
socket = new WebSocket(`ws://${location.hostname}/ws`);
}
socket.onopen = () => {
console.log("WebSocket conectado");
enviarComando({ cmd: "getReles" });
enviarComando({ cmd: "getSensorList" });
enviarComando({ cmd: "getReleList" });
enviarComando({ cmd: "getSSIDList" });
setTimeout(() => enviarComando({ cmd: "getStatusApp" }), 800);
};
socket.onmessage = (event) => {
const msg = JSON.parse(event.data);
if (msg.cmd === "listaReles") {
preencherSelectRele(msg.reles);
atualizarStatusReles(msg.status);
}
else if (msg.cmd === "statusReles") {
atualizarStatusReles(msg.status);
}
else if (msg.cmd === "sensorList") {
console.log("Recebido sensorList", msg.lista); // Adicione esta linha 👈
const sel = document.getElementById("sensorID");
sel.innerHTML = '<option value="" disabled selected>Selecione...</option>';
msg.lista.forEach(id => {
const opt = document.createElement("option");
opt.value = id;
opt.text = id;
sel.appendChild(opt);
});
}
else if (msg.cmd === "releList") {
const sel = document.getElementById("releID");
sel.innerHTML = "";
msg.lista.forEach(rid => {
const opt = document.createElement("option");
opt.value = rid;
opt.text = rid;
sel.appendChild(opt);
});
}
else if (msg.cmd === "sensorConfig") {
document.getElementById("descricao").value = msg.descricao || "";
document.getElementById("minTemp").value = msg.minTemp || 0;
document.getElementById("acaoMin").value = msg.acaoMin || "Ligar";
document.getElementById("maxTemp").value = msg.maxTemp || 0;
document.getElementById("acaoMax").value = msg.acaoMax || "Ligar";
document.getElementById("releID").value = msg.releID || "";
document.getElementById("habilitado").checked = msg.habilitado || false;
}
else if (msg.cmd === "ssidList") {
const sel = document.getElementById("ssid");
sel.innerHTML = "";
msg.lista.forEach(ssid => {
const opt = document.createElement("option");
opt.value = ssid;
opt.text = ssid;
sel.appendChild(opt);
});
// 🔥 Depois de popular o combobox, tenta selecionar o persistido
if (ssidPersistido) {
sel.value = ssidPersistido;
}
}
else if (msg.cmd === "statusApp") {
ssidPersistido = msg.ssid || "";
document.getElementById("varredura").value = msg.varredura || "";
document.getElementById("modo").value = msg.modo || "AP";
document.getElementById("alias").value = msg.alias || "";
document.getElementById("resolucao").value = msg.resolucao || 12;
document.getElementById("ntpServer").value = msg.ntpServer || "";
document.getElementById("timezone").value = msg.timezone || "";
document.getElementById("ssid").value = msg.ssid || "";
document.getElementById("senha").value = msg.senha || "";
document.getElementById("user_OTA").value = msg.user_OTA || "";
document.getElementById("pass_OTA").value = msg.pass_OTA || "";
document.getElementById("autoRebootOTA").checked = msg.autoRebootOTA || false;
// 🔵 Adiciona aqui para atualizar o SELECT do SSID:
const selSSID = document.getElementById("ssid");
if (selSSID) {
selSSID.value = ssidPersistido;
}
}
else if (msg.cmd === "tempUpdate") {
const painelId = `sensor-${msg.id}`;
let painel = document.getElementById(painelId);
if (!painel) {
painel = document.createElement("div");
painel.className = "sensor-card";
painel.id = painelId;
painel.innerHTML = `
<div class="sensor-header">
<span class="descricao">${msg.descricao}</span>
<span class="serial">${msg.serial}</span>
</div>
<div class="sensor-id">${msg.id}</div>
<div class="sensor-temp">--</div>
<div class="sensor-footer">
<span class="min">Min: --</span>
<span class="timestamp">--</span>
<span class="max">Max: --</span>
</div>
`;
document.getElementById("painelMedicoes").appendChild(painel);
}
// Atualiza temperatura e cor
const tempEl = painel.querySelector(".sensor-temp");
tempEl.textContent = `${msg.temp.toFixed(1)}°C`;
if (msg.temp < msg.minTemp) {
tempEl.className = "sensor-temp azul";
} else if (msg.temp > msg.maxTemp) {
tempEl.className = "sensor-temp vermelho";
} else {
tempEl.className = "sensor-temp verde";
}
// Atualiza timestamp
const min = (typeof msg.minTemp === "number") ? msg.minTemp : 0;
const max = (typeof msg.maxTemp === "number") ? msg.maxTemp : 0;
painel.querySelector(".min").textContent = `Min: ${min.toFixed(1)}°C`;
painel.querySelector(".max").textContent = `Max: ${max.toFixed(1)}°C`;
const data = new Date();
const stamp = `${data.toLocaleDateString("pt-BR")} ${data.toLocaleTimeString("pt-BR")}`;
painel.querySelector(".timestamp").textContent = stamp;
}
else if (msg.cmd === "saveSensorAck") {
console.log(`Sensor ${msg.id} salvo com sucesso!`);
// ✅ Mensagem visual simples
alert(`Sensor ${msg.id} salvo com sucesso!`);
}
else if (msg.cmd === "saveAppAck") {
console.log("Configuração da Aplicação salva com sucesso!");
// ✅ Mensagem visual simples
alert("Configuração da Aplicação salva com sucesso!");
}
};
socket.onclose = () => {
console.warn(`WebSocket fechado. Código: ${event.code}, Motivo: ${event.reason}`);
setTimeout(conectarSocket, 2000); // reconecta com delay
};
socket.onerror = (err) => {
console.error("Erro no WebSocket:", err);
socket.close();
};
};
window.onload = () => {
conectarSocket();
};
function preencherSelectRele(lista) {
const sel = document.getElementById("releManual");
sel.innerHTML = "";
lista.forEach((item, i) => {
const opt = document.createElement("option");
opt.value = item.id;
opt.text = `Relé #${item.id}`;
sel.appendChild(opt);
});
}
function atualizarStatusReles(statusArray) {
const painel = document.getElementById("statusReles");
painel.innerHTML = "";
statusArray.forEach(st => {
const p = document.createElement("p");
p.textContent = `Relé #${st.id}: ${st.estado ? "🟢 Ligado" : "🔴 Desligado"}`;
painel.appendChild(p);
});
}
function comandoReleManual(acao) {
const id = document.getElementById("releManual").value;
enviarComando({
cmd: "releManual",
id: parseInt(id),
acao: acao
});
}
function openTab(tabName) {
const contents = document.querySelectorAll(".tabcontent");
const buttons = document.querySelectorAll("nav button");
contents.forEach(div => div.classList.remove("active"));
buttons.forEach(btn => btn.classList.remove("active"));
document.getElementById(tabName).classList.add("active");
event.target.classList.add("active");
if (tabName === "parametros") {
enviarComando({ cmd: "getSensorList" });
setTimeout(() => {
enviarComando({ cmd: "getReleList" });
}, 100); // 100ms depois
setTimeout(() => {
enviarComando({ cmd: "getSSIDList" });
}, 200); // 200ms depois
}
}
function salvarSensorConfig() {
const msg = {
cmd: "saveSensor",
id: document.getElementById("sensorID").value,
descricao: document.getElementById("descricao").value,
minTemp: parseFloat(document.getElementById("minTemp").value),
acaoMin: document.getElementById("acaoMin").value,
maxTemp: parseFloat(document.getElementById("maxTemp").value),
acaoMax: document.getElementById("acaoMax").value,
releID: parseInt(document.getElementById("releID").value.replace("#", "")),
habilitado: document.getElementById("habilitado").checked
};
enviarComando(msg);
}
function salvarAppConfig() {
const msg = {
cmd: "saveApp",
varredura: parseInt(document.getElementById("varredura").value),
modo: document.getElementById("modo").value,
alias: document.getElementById("alias").value,
resolucao: parseInt(document.getElementById("resolucao").value),
ntpServer: document.getElementById("ntpServer").value,
timezone: document.getElementById("timezone").value,
ssid: document.getElementById("ssid").value,
senha: document.getElementById("senha").value,
user_OTA: document.getElementById("user_OTA").value,
pass_OTA: document.getElementById("pass_OTA").value,
autoRebootOTA: document.getElementById("autoRebootOTA").checked
};
enviarComando(msg);
}
function carregarSensorConfig() {
const id = document.getElementById("sensorID").value;
if (id === "") return; // não faz nada se ainda não selecionou
enviarComando({ cmd: "getSensor", id: id });
}
function cancelarParametros() {
enviarComando({ cmd: "getStatusApp" });
}
function sincronizarRelogio() {
const agora = new Date();
const timestamp = Math.floor(agora.getTime() / 1000); // segundos desde 1970
enviarComando({
cmd: "sincronizar",
timestamp: timestamp
});
}
function criarPainelSensor(dado) {
const card = document.createElement("div");
const timestamp = new Date().toLocaleString("pt-BR", {
day: "2-digit", month: "2-digit", year: "numeric",
hour: "2-digit", minute: "2-digit", second: "2-digit"
});
// Determina a classe de temperatura e se está em alarme
let tempClass = "verde"; // padrão
let cardClass = "";
let alerta = "";
if (dado.temperatura < dado.minTemp) {
tempClass = "azul";
cardClass = "alarme-baixo";
alerta = `<div class="sensor-alerta">⚠️ Temperatura abaixo do mínimo!</div>`;
} else if (dado.temperatura > dado.maxTemp) {
tempClass = "vermelho";
cardClass = "alarme-alto";
alerta = `<div class="sensor-alerta">⚠️ Temperatura acima do máximo!</div>`;
}
// Agora sim, aplica as classes ao card
card.classList.add("sensor-card");
if (cardClass) {
card.classList.add(cardClass);
}
// Conteúdo do painel do sensor
card.innerHTML = `
${alerta}
<div class="sensor-header">
<span class="sensor-descricao">${dado.descricao}</span>
<span class="sensor-serial">${dado.serial}</span>
</div>
<div class="sensor-id">${dado.id}</div>
<div class="sensor-temp ${tempClass}">${dado.temperatura.toFixed(1)}°C</div>
<div class="sensor-footer">
<span class="min">Min: ${dado.minTemp.toFixed(1)}°C</span>
<span class="timestamp">${timestamp}</span>
<span class="max">Max: ${dado.maxTemp.toFixed(1)}°C</span>
</div>
`;
return card;
}
function enviarComando(obj) {
if (socket && socket.readyState === WebSocket.OPEN) {
socket.send(JSON.stringify(obj));
} else {
console.warn("WebSocket não está pronto. Ignorando comando:", obj);
}
}
function mostrarSpinner() {
const painelMedicoes = document.getElementById("painelMedicoes");
painelMedicoes.innerHTML = '<p>Carregando...</p>';
}
function ocultarSpinner() {
const painelMedicoes = document.getElementById("painelMedicoes");
painelMedicoes.innerHTML = '';
}
// Evento para fechamento da janela do Navegador
window.addEventListener("unload", function() {
if (socket) {
socket.close();
}
});
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') {
if (!socket || socket.readyState !== WebSocket.OPEN) {
conectarSocket();
}
}
});
</script>
</body>
</html>
)rawliteral";
//--------------------------------------------
// Rotina de Inicialização Geral da Aplicação
//--------------------------------------------
void setup()
{
// Inicializa a Serial
Serial.begin(115200);
while (!Serial);
// Hello na Serial
Serial.println("Termostato V1.0 Abr/2025");
Serial.flush();
#if defined(ARDUINO_LOLIN32)
// I2C no ESP32 Wemos Lolin32 usa SDA = 5 e SCL = 4 para o display OLED
Wire.begin(5, 4);
#endif
// Inicializa com o I2C addr 0x3C
display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
display.clearDisplay();
// Inicializa os sensores
setupSensores();
// Carrega as Configurações se existerem
carregarConfig();
// Inicializa a Rede Wifi ou AP Mode
setupWiFi();
// Aplicar os parâmetros persistidos
aplicarParametros();
// Inicializa o Modo Web Server
setupServer();
// Define os pinos dos Relés
pinMode(pinRELE1, OUTPUT);
pinMode(pinRELE2, OUTPUT);
// Estado inicial dos Relés: desligados
digitalWrite(pinRELE1,RELE_OFF);
digitalWrite(pinRELE2,RELE_OFF);
// Mostra Consumo de Memória
printFreeRAM("Setup...");
}
//--------------------------------------------
// Rotina de Loop Principal da Aplicação
//--------------------------------------------
void loop()
{
// Verifica se deve fazer uma varredura dos sensores
if (nDevices > 0 && (millis() - lastScan) > appConfig.varredura)
{
lastScan = millis();
processaSensores();
leituraPronta = true;
}
// Verifica se deve mostrar a temperatura no Display
if (nDevices > 0 && leituraPronta && (millis() - tempoUltimaExibicao) >= tempoExibicao)
{
tempoUltimaExibicao = millis();
displayTemperatura(sensorIndex);
sensorIndex = (sensorIndex+1) % nDevices;
if (sensorIndex==0) leituraPronta = false;
}
// Preparação para o SCAN da rede WiFi assíncrono para não comprometer o AsyncWebServer
if (iniciarScanSSID && !scanEmAndamento)
{
resultadoScan = WiFi.scanNetworks(true); // ⚠️ Modo assíncrono!
scanEmAndamento = true;
iniciarScanSSID = false;
tempoScanInicio = millis();
}
// Verifica se terminou o SCAN assíncrono
if (scanEmAndamento && WiFi.scanComplete() >= 0)
{
DynamicJsonDocument doc(1024);
doc["cmd"] = "ssidList";
JsonArray lista = doc.createNestedArray("lista");
for (int i = 0; i < WiFi.scanComplete(); i++)
{
lista.add(WiFi.SSID(i));
}
String out;
serializeJson(doc, out);
if (clienteScanSSID) clienteScanSSID->text(out);
WiFi.scanDelete();
scanEmAndamento = false;
clienteScanSSID = nullptr;
}
// 🔴 Se passar muito tempo esperando e nada acontecer, cancela
if (scanEmAndamento && millis() - tempoScanInicio > INTERVALO_SCAN)
{
Serial.println("[ERRO] Timeout no scan de SSID");
WiFi.scanDelete();
scanEmAndamento = false;
clienteScanSSID = nullptr;
}
// Verifica se deve fazer o Cleanup
if (millis() - lastCleanup > INTERVALO_CLEANUP)
{
ws.cleanupClients();
lastCleanup = millis();
printFreeRAM("Pós Cleanup...");
}
// Verifica o OTA para saber se há atualização
ElegantOTA.loop();
}
//-------------------------------------------------
// Rotina para carregar as configurações do SPIFFS
//-------------------------------------------------
bool carregarConfig()
{
Serial.println("Tentando carregar o Config do SPIFFS...");
if (!SPIFFS.begin(true))
{
Serial.println("Não foi possível incializar o SPIFFS...");
return false;
}
//deleteFile(CONFIG_PATH); // Deleta o arquivo de configuração se necessário
if (!SPIFFS.exists(CONFIG_PATH))
{
Serial.println("Config não existe. Adotando default...");
// Valores padrão se não existir o arquivo
appConfig.modo = "AP";
appConfig.alias = "termostato";
appConfig.ssid = "";
appConfig.senha = "";
appConfig.varredura = 5000;
appConfig.ntpServer = "pool.ntp.org";
appConfig.timezone = "<-03>3";
appConfig.resolucao = 12;
appConfig.user_OTA = "admin";
appConfig.pass_OTA = "admin";
appConfig.autoRebootOTA = true;
for (int i=1;i<=nDevices;i++)
{
SensorConfig s;
String aux = String(i);
s.idSensor = "#" + aux;
s.descricao = "Sensor " + aux;
s.minTemp = 20.0;
s.acaoMin = "Ligar";
s.maxTemp = 30.0;
s.acaoMax = "Desligar";
s.releID = 1;
s.habilitado = true;
appConfig.sensores.push_back(s);
}
salvarConfig();
return false;
}
Serial.println("Lendo o Config existente...");
File file = SPIFFS.open(CONFIG_PATH);
if (!file)
{
Serial.println("Erro a abrir o Config...");
return false;
}
JsonDocument doc;
DeserializationError error = deserializeJson(doc, file);
serializeJsonPretty(doc, Serial);
Serial.println();
Serial.flush();
file.close();
if (error) return false;
// Corrigido: usa | diretamente no JsonVariant, sem cast
appConfig.modo = doc["modo"] | "AP";
appConfig.alias = doc["alias"] | "termostato";
appConfig.ssid = doc["ssid"] | "";
appConfig.senha = doc["senha"] | "";
appConfig.varredura = doc["varredura"] | 5000;
appConfig.ntpServer = doc["ntpServer"] | "pool.ntp.org";
appConfig.timezone = doc["timezone"] | "<-03>3";
appConfig.resolucao = doc["resolucao"] | 12;
appConfig.user_OTA = doc["user_OTA"] | "admin";
appConfig.pass_OTA = doc["pass_OTA"] | "admin";
appConfig.autoRebootOTA = doc["autoRebootOTA"] | false;
// Sensores
appConfig.sensores.clear();
JsonArray sensores = doc["sensores"].as<JsonArray>();
for (JsonObject s : sensores) {
SensorConfig sc;
sc.idSensor = s["id"] | "";
sc.descricao = s["desc"] | "";
sc.minTemp = s["min"] | 0.0;
sc.acaoMin = s["acaoMin"] | "";
sc.maxTemp = s["max"] | 0.0;
sc.acaoMax = s["acaoMax"] | "";
sc.releID = s["rele"] | 0;
sc.habilitado= s["hab"] | false;
appConfig.sensores.push_back(sc);
}
return true;
}
//------------------------------------------------
// Rotina para configurar o Modo AP ou o Modo WiFi
//------------------------------------------------
void setupWiFi()
{
if (appConfig.modo == "WiFi" && appConfig.ssid.length()) {
WiFi.begin(appConfig.ssid.c_str(), appConfig.senha.c_str());
Serial.print("Conectando ao WiFi");
unsigned long start = millis();
while (WiFi.status() != WL_CONNECTED && millis() - start < 10000) {
delay(500); Serial.print(".");
}
if (WiFi.status() == WL_CONNECTED) {
Serial.println("\nWiFi conectado: " + WiFi.localIP().toString());
// Imprime o MAC
Serial.print("MAC: ");
Serial.println(WiFi.macAddress());
// Imprime o Sinal Wifi
Serial.print("Sinal: ");
Serial.print(WiFi.RSSI());
Serial.println(" db");
// Verifica se está navegando pela internet pois às vezes fica conectado no AP porém sem internet
if (!Ping.ping(ip,4))
{
Serial.println("Sem internet no momento...");
}
else
{
Serial.print("Internet ativa com média de ");
Serial.print(Ping.averageTime());
Serial.println(" ms");
}
// Sincroniza o horário interno com o Servidor NTP nacional
Serial.print("Tentando sincronismo com o servidor NTP ");
Serial.print(appConfig.ntpServer.c_str());
Serial.print(" com TimeZone ");
Serial.println(appConfig.timezone.c_str());
configTime(0, 0, appConfig.ntpServer.c_str());
setenv("TZ", appConfig.timezone.c_str(), 1);
tzset();
if (getNTPtime(10))
{ // wait up to 10sec to sync
Serial.println("NTP Server sincronizado");
}
else
{
Serial.println("\nTimer interno não foi sincronizado");
//ESP.restart();
}
} else {
Serial.println("\nErro no WiFi, entrando em modo AP...");
appConfig.modo = "AP";
}
}
if (appConfig.modo == "AP") {
WiFi.softAP(appConfig.alias.c_str());
Serial.println("Modo AP iniciado: " + WiFi.softAPIP().toString());
}
}
//--------------------------------------------
// Rotina para tratar os eventos de WebSocket
//--------------------------------------------
void onWsEvent(AsyncWebSocket *server, AsyncWebSocketClient *client,
AwsEventType type, void *arg, uint8_t *data, size_t len)
{
uint32_t clientId = client->id();
if (type == WS_EVT_CONNECT)
{
Serial.printf("Cliente conectado: %u\n", clientId);
}
else if (type == WS_EVT_DISCONNECT) {
Serial.printf("Cliente desconectado: %u\n", clientId);
lastCleanup = 0;
}
else if (type == WS_EVT_DATA)
{
// A processar comandos...
AwsFrameInfo *info = (AwsFrameInfo*)arg;
if (info->opcode == WS_TEXT) {
data[len] = 0;
DynamicJsonDocument doc(512);
deserializeJson(doc, data);
String cmd = doc["cmd"];
if (cmd == "getReles") {
Serial.printf("Recebido: %s\n",cmd);
DynamicJsonDocument resp(512);
resp["cmd"] = "listaReles";
JsonArray rel = resp.createNestedArray("reles");
JsonArray sts = resp.createNestedArray("status");
for (int i = 0; i < 2; i++) {
JsonObject r = rel.createNestedObject();
r["id"] = i+1;
JsonObject s = sts.createNestedObject();
s["id"] = i+1;
s["estado"] = !digitalRead(pinReles[i]);
}
String out;
serializeJson(resp, out);
client->text(out);
}
else if (cmd == "releManual")
{
Serial.printf("Recebido: %s\n",cmd);
int id = doc["id"];
String acao = doc["acao"];
alterarEstadoRele(id-1, (acao == "ligar") ? true : false);
}
else if (cmd == "getSensor") {
Serial.printf("Recebido: %s\n",cmd);
if (!doc.containsKey("id")) {
Serial.println("[ERRO] getSensor sem campo 'id'");
return;
}
String id = doc["id"];
for (auto &sc : appConfig.sensores) {
if (sc.idSensor == id) {
DynamicJsonDocument resp(512);
resp["cmd"] = "sensorConfig";
resp["descricao"] = sc.descricao;
resp["minTemp"] = sc.minTemp;
resp["acaoMin"] = sc.acaoMin;
resp["maxTemp"] = sc.maxTemp;
resp["acaoMax"] = sc.acaoMax;
resp["releID"] = "#" + String(sc.releID);
resp["habilitado"] = sc.habilitado;
String out;
serializeJson(resp, out);
client->text(out);
break;
}
}
}
else if (cmd == "saveSensor") {
Serial.printf("Recebido: %s\n", cmd);
bool encontrado = false;
String id = doc["id"];
for (auto &sc : appConfig.sensores) {
if (sc.idSensor == id) {
sc.descricao = doc["descricao"] | "";
sc.minTemp = doc["minTemp"] | 0;
sc.acaoMin = doc["acaoMin"] | "Ligar";
sc.maxTemp = doc["maxTemp"] | 0;
sc.acaoMax = doc["acaoMax"] | "Ligar";
sc.releID = doc["releID"] | 1;
sc.habilitado = doc["habilitado"] | false;
encontrado = true;
break;
}
}
// Se não encontrado, adiciona como novo
if (!encontrado) {
SensorConfig novo;
novo.idSensor = id;
novo.descricao = doc["descricao"] | "";
novo.minTemp = doc["minTemp"] | 0;
novo.acaoMin = doc["acaoMin"] | "Ligar";
novo.maxTemp = doc["maxTemp"] | 0;
novo.acaoMax = doc["acaoMax"] | "Ligar";
novo.releID = doc["releID"] | 1;
novo.habilitado = doc["habilitado"] | false;
appConfig.sensores.push_back(novo);
}
salvarConfig(); // Salva no SPIFFS
aplicarParametros(); // Atualiza alarmes/resoluções se necessário
// 🔥 Responde com ACK para o navegador
DynamicJsonDocument ack(128);
ack["cmd"] = "saveSensorAck";
ack["id"] = id;
String out;
serializeJson(ack, out);
client->text(out);
Serial.println("[INFO] Sensor salvo e ACK enviado");
}
else if (cmd == "saveApp") {
Serial.printf("Recebido: %s\n",cmd);
appConfig.varredura = doc["varredura"] | 5000;
appConfig.modo = doc["modo"] | "AP";
appConfig.alias = doc["alias"] | "termostato";
appConfig.resolucao = doc["resolucao"] | 12;
appConfig.ntpServer = doc["ntpServer"] | "pool.ntp.org";
appConfig.timezone = doc["timezone"] | "<-03>3";
appConfig.ssid = doc["ssid"] | "";
appConfig.senha = doc["senha"] | "";
appConfig.user_OTA = doc["user_OTA"] | "admin";
appConfig.pass_OTA = doc["pass_OTA"] | "admin";
appConfig.autoRebootOTA = doc["autoRebootOTA"] | true;
salvarConfig(); // salva no SPIFFS
// ✅ Atualiza configuração de horário
configTzTime(appConfig.timezone.c_str(), appConfig.ntpServer.c_str());
// 🔥 Envia ACK
DynamicJsonDocument ack(128);
ack["cmd"] = "saveAppAck";
String out;
serializeJson(ack, out);
client->text(out);
Serial.println("[INFO] Configuração da Aplicação salva e ACK enviado");
}
else if (cmd == "getSSIDList") {
Serial.println("Recebido: getSSIDList");
if (!scanEmAndamento) {
iniciarScanSSID = true;
clienteScanSSID = client; // salva o ponteiro
}
}
else if (cmd == "getSensorList") {
Serial.printf("Recebido: %s\n", cmd);
DynamicJsonDocument doc(512);
doc["cmd"] = "sensorList";
JsonArray lista = doc.createNestedArray("lista");
// Adiciona todos os sensores detectados no barramento
for (int i = 0; i < nDevices; i++) {
String id = "#" + String(i + 1);
lista.add(id);
}
String out;
serializeJson(doc, out);
client->text(out);
}
else if (cmd == "getReleList") {
Serial.printf("Recebido: %s\n",cmd);
DynamicJsonDocument doc(512);
doc["cmd"] = "releList";
JsonArray lista = doc.createNestedArray("lista");
for (int i = 1; i <= MAX_RELES; i++) {
lista.add("#" + String(i));
}
String out;
serializeJson(doc, out);
client->text(out);
}
else if (cmd == "getStatusApp") {
Serial.printf("Recebido: %s\n", cmd);
DynamicJsonDocument resp(512);
resp["cmd"] = "statusApp";
resp["varredura"] = appConfig.varredura;
resp["modo"] = appConfig.modo;
resp["alias"] = appConfig.alias;
resp["resolucao"] = appConfig.resolucao;
resp["ntpServer"] = appConfig.ntpServer;
resp["timezone"] = appConfig.timezone;
resp["ssid"] = appConfig.ssid;
resp["senha"] = appConfig.senha;
// ⚠️ Campos adiconados posteriormente
resp["user_OTA"] = appConfig.user_OTA;
resp["pass_OTA"] = appConfig.pass_OTA;
resp["autoRebootOTA"] = appConfig.autoRebootOTA;
String out;
serializeJson(resp, out);
client->text(out);
}
else if (cmd == "sincronizar") {
time_t ts = doc["timestamp"] | 0;
struct timeval now = { ts, 0 };
settimeofday(&now, nullptr);
Serial.print("Horário atualizado: ");
Serial.println(ctime(&ts));
}
}
}
}
//------------------------------------------
// Rotina para inicializar o AsyncWebServer
//------------------------------------------
void setupServer()
{
ws.onEvent(onWsEvent);
server.addHandler(&ws);
server.on("/", HTTP_GET, [](AsyncWebServerRequest *req) {
req->send(200, "text/html", index_html);
});
server.begin();
ElegantOTA.begin(&server, appConfig.user_OTA.c_str(), appConfig.pass_OTA.c_str());
ElegantOTA.setAutoReboot(appConfig.autoRebootOTA);
}
//-------------------------------------------------------
// Define o HostName como DNS NAME
//-------------------------------------------------------
bool setDNSNAME(String nome)
{
WiFi.setHostname(nome.c_str());
bool ok = MDNS.begin(nome.c_str());
if (ok)
{
MDNS.addService("http", "tcp", 80);
MDNS.setInstanceName(nome.c_str()); // Adicionar o nome da instância
}
return ok;
}
//-----------------------------------------------------------------------------
// Rotina para inicializar os sensores e determinar quantos estão no barramento
//-----------------------------------------------------------------------------
void setupSensores()
{
// Inicia o sensor de temperatura
sensors.begin();
// Mostra o GPIO do Sensor
Serial.print("\nGPIO usado: ");
Serial.println(pinBUS);
// Localizando os sensores
nDevices = sensors.getDeviceCount();
Serial.print("N. de Dispositivos encontrados: ");
Serial.println(nDevices);
if (nDevices > 0)
{
Serial.println("Dispositivo\tEndereço");
Serial.println("-----------------------------------");
// Loop through each device, print out address
for (int i = 0; i < nDevices; i++)
{
// Obtém o endereço do dispositivo
if (sensors.getAddress(deviceAddr, i))
{
// Mostra na Serial
Serial.print(" ");
Serial.print(i+1, DEC);
Serial.print("\t ");
printAddress(deviceAddr);
Serial.println();
// Programa o alarme de temperatura mínima e máxima
//sensors.setLowAlarmTemp(deviceAddr, MIN_TEMP);
//sensors.setHighAlarmTemp(deviceAddr, MAX_TEMP);
}
else
{
Serial.print(" ");
Serial.print(i, DEC);
Serial.println("\t Endereço não reconhecido");
}
}
}
else
{
Serial.println("Verifique o hardware e reinicie...");
showError();
}
}
//----------------------------------------------
// Rotina para fazer as leituras de temperaturas
// e notificar a interface html
//----------------------------------------------
void processaSensores()
{
// Manda comando para ler temperaturas
sensors.requestTemperatures();
// Verifica ocorrência de alarme
sensors.processAlarms();
// Faz o loop nos dispositivos
for (int i=0; i<nDevices; i++)
{
// Obtém a temperatura em Celsius
temperaturas[i] = sensors.getTempCByIndex(i);
Serial.print("Temperatura[#");
Serial.print(i+1);
Serial.print("]: ");
Serial.print(temperaturas[i]);
Serial.println(" graus");
if (ws.count() > 0) {
DynamicJsonDocument doc(256);
doc["cmd"] = "tempUpdate";
doc["id"] = "#" + String(i + 1); // Ex: #1, #2
// Recupera o endereço do sensor e converte para string
if (sensors.getAddress(deviceAddr, i))
{
char serial[17] = {0};
for (uint8_t j = 0; j < 8; j++) {
sprintf(&serial[j * 2], "%02X", deviceAddr[j]);
}
doc["serial"] = serial;
}
else
{
doc["serial"] = "Desconhecido";
}
doc["temp"] = temperaturas[i];
// Procura limites definidos para esse sensor
String id = "#" + String(i + 1);
for (auto &sc : appConfig.sensores)
{
if (sc.idSensor == id)
{
doc["minTemp"] = sc.minTemp;
doc["maxTemp"] = sc.maxTemp;
doc["descricao"] = sc.descricao;
break;
}
}
String out;
serializeJson(doc, out);
ws.textAll(out); // Envia para todos os clientes
}
}
}
//---------------------------------------------
// Imprime o Endereço do Dispositico na Serial
//---------------------------------------------
void printAddress(const DeviceAddress deviceAddress)
{
for (uint8_t i = 0; i < 8; i++)
{
if (deviceAddress[i] < 16) Serial.print("0");
Serial.print(deviceAddress[i], HEX);
}
}
//-------------------------------------------------
// Tratamento de Alarme de temperatura dos sensores
//-------------------------------------------------
void newAlarmHandler(const uint8_t* deviceAddress)
{
int index = getDeviceIndex(deviceAddress);
if (index < 0 || index >= appConfig.sensores.size()) return;
SensorConfig sc = appConfig.sensores[index];
if (!sc.habilitado) return;
float temp = sensors.getTempC(deviceAddress);
float low = (float)sensors.getLowAlarmTemp(deviceAddress);
float high = (float)sensors.getHighAlarmTemp(deviceAddress);
int releIdx = sc.releID - 1;
if (releIdx < 0 || releIdx >= MAX_RELES) return;
Serial.printf("[ALARM] %s: %.1f°C (limites: %.1f–%.1f)\n",
sc.idSensor.c_str(), temp, low, high);
if (temp < low)
{
Serial.printf("→ Abaixo do limite. Ação MIN: %s\n", sc.acaoMin.c_str());
alterarEstadoRele(releIdx, (sc.acaoMin == "Ligar") ? true : false);
}
else if (temp > high)
{
Serial.printf("→ Acima do limite. Ação MAX: %s\n", sc.acaoMax.c_str());
alterarEstadoRele(releIdx, (sc.acaoMax == "Ligar") ? true : false);
}
}
//-----------------------------------------
// Mostra a corrente temperatura na Console
//-----------------------------------------
void printCurrentTemp(DeviceAddress deviceAddress)
{
printAddress(deviceAddress);
printTemp(deviceAddress);
Serial.println();
}
//-----------------------------------------
// Mostra a corrente temperatura na Console
//-----------------------------------------
void printTemp(const DeviceAddress deviceAddress)
{
float tempC = sensors.getTempC(deviceAddress);
if (tempC != DEVICE_DISCONNECTED_C)
{
Serial.print("Temp Atual: ");
Serial.print(tempC);
Serial.print("C");
}
else Serial.print("DEVICE DISCONNECTED");
Serial.print(" ");
}
//-----------------------------------------
// Mostra as informações do Sensor
//-----------------------------------------
void printAlarmInfo(const DeviceAddress deviceAddress)
{
char temp;
printAddress(deviceAddress);
Serial.print(" ");
Serial.print("Limite Inferior: ");
temp = sensors.getLowAlarmTemp(deviceAddress);
Serial.print(temp, DEC);
Serial.print("C");
temp = sensors.getHighAlarmTemp(deviceAddress);
Serial.print(" Limite Superior: ");
Serial.print(temp, DEC);
Serial.print("C");
Serial.print(" ");
}
//--------------------------------------------
// Obtém o índice do sensor a partir do Serial
//--------------------------------------------
int getDeviceIndex(const DeviceAddress deviceAddress)
{
DeviceAddress device;
for (int i=0; i < nDevices; i++)
{
if (sensors.getAddress(device, i) && memcmp(device,deviceAddress,sizeof(DeviceAddress))==0) return i;
}
return -1;
}
//-------------------------------------
// Centraliza um texto no Dispplay OLED
//-------------------------------------
void centerText(String text, int textSize)
{
// 6 pixel x tamanho da fonte = ocupação em pixel de um char
int charWidth = 6 * textSize;
// Limpa o display
display.clearDisplay();
// Define as características do texto
display.setTextColor(WHITE);
display.setTextSize(textSize); // Tamanho da fonte para o texto principal
int txtWidth = charWidth * text.length();
// Posiciona o cursor
display.setCursor((128-txtWidth)/2, (64-charWidth)/2); // Para Display de 128x64
// Mostra o Texto
display.print(text);
display.display();
}
//---------------------------------------------------------
// Mostra Erro quando não encontrou sensores no barramento
//---------------------------------------------------------
void showError()
{
// Limpa o display
display.clearDisplay();
// Define as características do texto
display.setTextColor(WHITE);
display.setTextSize(1);
// Posiciona o cursor
display.setCursor(0,20);
// Mostra o Texto
display.println("Nenhum Dispositivo");
display.println("encontrado...");
display.println("Check o Hardware");
display.println("e Reinicie...");
display.display();
while (true)
{
display.startscrollright(0x00, 0x0f);
delay(7000);
display.stopscroll();
delay(1000);
display.startscrollleft(0x00, 0x0f); // Movimenta texto para a esquerda
delay(7000);
display.stopscroll();
delay(1000);
}
}
//----------------------------------------------
// Rotina para persistir os parâmetros no SPIFFS
//-----------------------------------------------
void salvarConfig()
{
Serial.println("Salvando a configuração...");
JsonDocument doc;
doc["modo"] = appConfig.modo;
doc["alias"] = appConfig.alias;
doc["ssid"] = appConfig.ssid;
doc["senha"] = appConfig.senha;
doc["varredura"] = appConfig.varredura;
doc["ntpServer"] = appConfig.ntpServer;
doc["timezone"] = appConfig.timezone;
doc["resolucao"] = appConfig.resolucao;
doc["user_OTA"] = appConfig.user_OTA;
doc["pass_OTA"] = appConfig.pass_OTA;
doc["autoRebootOTA"] = appConfig.autoRebootOTA;
JsonArray sensores = doc.createNestedArray("sensores");
for (auto &sc : appConfig.sensores) {
JsonObject s = sensores.createNestedObject();
s["id"] = sc.idSensor;
s["desc"] = sc.descricao;
s["min"] = sc.minTemp;
s["acaoMin"] = sc.acaoMin;
s["max"] = sc.maxTemp;
s["acaoMax"] = sc.acaoMax;
s["rele"] = sc.releID;
s["hab"] = sc.habilitado;
}
File file = SPIFFS.open(CONFIG_PATH, FILE_WRITE);
if (file) {
serializeJsonPretty(doc, file);
serializeJsonPretty(doc, Serial);
Serial.println();
Serial.flush();
file.close();
Serial.println("Configuração salva no SPIFFS");
aplicarParametros();
}
}
//----------------------------------------------
// Função para delatar arquivos do SPIFFS
// Útil para resetar estados iniciais
//----------------------------------------------
void deleteFile(const char* path)
{
// Verifica se o arquivo existe
if (SPIFFS.exists(path))
{
if (SPIFFS.remove(path))
{
Serial.printf("Arquivo %s deletado com sucesso\n", path);
}
else
{
Serial.printf("Falha ao deletar o arquivo %s\n", path);
}
}
else
{
Serial.printf("Arquivo %s não encontrado\n", path);
}
}
//--------------------------------------------------
// Rotina para mostrar a temperatura no Display OLED
//--------------------------------------------------
void displayTemperatura(int index)
{
int auxTemp = round(temperaturas[index]);
String txtTemp = String(auxTemp);
// Limpa o display
display.clearDisplay();
// Configure os tamanhos e posições iniciais
int txtWidth = (128 / 7) * (txtTemp.length() + 1.5); // Em 128 bits cabem cerca de 7 chars
// 1 para C e 0.5 para símbolo Centígrados
display.setTextColor(WHITE);
display.setTextSize(3); // Tamanho da fonte para o texto principal
String txtID = "#" + String(index+1);
int idWidth = (128 / 7) * txtID.length();
display.setCursor((128-idWidth)/2, 0);
display.print(txtID);
display.setCursor((128-txtWidth)/2, 30); // Posição inicial da Temperatura
display.print(txtTemp);
// Calcula a posição para o caractere "°"
display.setCursor((128/7)*(txtTemp.length()+1.5)+4, 26); // Posiciona o caractere "°"
display.setTextSize(2); // Tamanho menor para o símbolo "°"
display.write(9); // Código ASCII para o símbolo "°"
// Exibe o "C" após o símbolo "°"
display.setTextSize(3); // Retorna ao tamanho original para "C"
display.setCursor((128/7)*(txtTemp.length()+2)+2, 30);
display.print("C");
display.display();
}
//-----------------------------------------------------------
// Rotina para aplicar os parãmetros redefinidos na interface
//-----------------------------------------------------------
void aplicarParametros()
{
Serial.println("[INFO] Aplicando parâmetros...");
// Reseta os alarmes antes de reprogramar
//sensors.resetAlarmSearch();
sensors.setLowAlarmTemp(deviceAddr, 0);
sensors.setHighAlarmTemp(deviceAddr, 0);
// ✅ Resolução global
sensors.setResolution(appConfig.resolucao);
// Mostra a Resolução adotada
Serial.print("Resolução: ");
Serial.println(sensors.getResolution());
// ✅ Configuração individual de sensores
for (auto &sc : appConfig.sensores) {
int index = sc.idSensor.substring(1).toInt() - 1;
if (index >= 0 && index < nDevices && sensors.getAddress(deviceAddr, index)) {
//sensors.setHighAlarmTemp(deviceAddr, (char)round(sc.maxTemp));
//sensors.setLowAlarmTemp(deviceAddr, (char)round(sc.minTemp));
sensors.setLowAlarmTemp(deviceAddr, (char)floor(sc.minTemp));
sensors.setHighAlarmTemp(deviceAddr, (char)ceil(sc.maxTemp));
Serial.printf("[DEBUG] Sensor %s - setLow = %d, setHigh = %d\n",
sc.idSensor.c_str(),
sensors.getLowAlarmTemp(deviceAddr),
sensors.getHighAlarmTemp(deviceAddr));
if (sc.habilitado) {
sensors.setAlarmHandler(newAlarmHandler); // handler global
} else {
sensors.resetAlarmSearch(); // desativa alarmes para todos
}
}
}
// ✅ DNS Name (mDNS)
if (setDNSNAME(appConfig.alias.c_str())) {
Serial.println("mDNS iniciado: http://" + appConfig.alias + ".local");
}
else Serial.println("Erro ao adicionar no MDNS!");
// Configura o TimeZone
configTzTime(appConfig.timezone.c_str(), appConfig.ntpServer.c_str());
// Monta o tempo de exibição no display
if (nDevices > 0)
{
tempoExibicao = max(200UL, (unsigned long)((appConfig.varredura * 0.9) / nDevices));
Serial.printf("tempoExibicao ajustado para %lu ms\n", tempoExibicao);
}
// ✅ Detecta mudança de modo de rede e reinicia se necessário
static bool primeiraAplicacao = true;
static String modoAnterior = "";
if (!primeiraAplicacao && appConfig.modo != modoAnterior) {
Serial.printf("[INFO] Modo de rede alterado de %s para %s. Reiniciando...\n", modoAnterior.c_str(), appConfig.modo.c_str());
delay(1000);
ESP.restart();
}
modoAnterior = appConfig.modo;
primeiraAplicacao = false;
Serial.println("[INFO] Parâmetros aplicados com sucesso.");
}
//------------------------------------------------
// Devolve o localtime dd/mm/aaaa hh:mm:ss
//------------------------------------------------
String getTimeStamp()
{
time_t now;
time(&now);
char timestamp[30];
strftime(timestamp, 30, "%d/%m/%Y %T", localtime(&now));
return String(timestamp);
}
//---------------------------------------------------------
// Sincroniza o horário do ESP32 com NTP server brasileiro
//---------------------------------------------------------
bool getNTPtime(int sec)
{
{
uint32_t start = millis();
tm timeinfo;
time_t now;
int cont=0;
do
{
time(&now);
localtime_r(&now, &timeinfo);
if (++cont % 80 == 0) Serial.println();
else Serial.print(".");
delay(10);
} while (((millis() - start) <= (1000 * sec)) && (timeinfo.tm_year < (2016 - 1900)));
if (timeinfo.tm_year <= (2016 - 1900)) return false; // the NTP call was not successful
Serial.print("\nnow ");
Serial.println(now);
Serial.print("Time ");
Serial.println(getTimeStamp());
}
return true;
}
//----------------------------------------------------------
// Função para avisar aos clientes que Status de Relé mudou
//----------------------------------------------------------
void enviarStatusParaTodosClientes()
{
if (ws.count()==0) return;
DynamicJsonDocument resp(256);
resp["cmd"] = "statusReles";
JsonArray sts = resp.createNestedArray("status");
for (int i = 0; i < 2; i++) {
JsonObject s = sts.createNestedObject();
s["id"] = i+1;
s["estado"] = !digitalRead(pinReles[i]);
}
String out;
serializeJson(resp, out);
ws.textAll(out); // Envia para todos os clientes conectados
}
//------------------------------------------------------------
// Função para alterar o estado do Rele e avisar na interface
//------------------------------------------------------------
void alterarEstadoRele(int releIndex, bool estado)
{
digitalWrite(pinReles[releIndex], estado ? RELE_ON : RELE_OFF);
statusReles[releIndex] = estado;
enviarStatusParaTodosClientes();
}
//--------------------------------------------------
// Função para monitorar o uso de memória disponível
//--------------------------------------------------
void printFreeRAM(String context)
{
size_t freeHeap = esp_get_free_heap_size();
//Serial.printf("RAM disponível (%s): %u bytes\n", context.c_str(), freeHeap);
Serial.printf("Consumo de RAM (%s) Total heap: %u, Free heap: %u, Min ever free: %u\n",
context.c_str(),
heap_caps_get_total_size(MALLOC_CAP_DEFAULT),
heap_caps_get_free_size(MALLOC_CAP_DEFAULT),
heap_caps_get_minimum_free_size(MALLOC_CAP_DEFAULT));
}
O projeto buscou mostrar uma solução flexível para monitoramento e controle térmico utilizando ESP32. A combinação de sensores DS18B20 com relés configuráveis, suporte a múltiplos modos de operação (AP/WiFi), atualização OTA e sincronização de horário resulta numa estação compacta e versátil. O projeto pode ser escalável e pode ser adaptado facilmente a diferentes contextos — desde aplicações domésticas até controle de processos industriais simples.
Como o projeto se encaixaria?
|
|
A Eletrogate é uma loja virtual de componentes eletrônicos do Brasil e possui diversos produtos relacionados à Arduino, Automação, Robótica e Eletrônica em geral.
Tenha a Metodologia Eletrogate dentro da sua Escola! Conheça nosso Programa de Robótica nas Escolas!