No post anterior ESP8266 Relay com Programação de Eventos Temporais apresentamos uma aplicação voltada ao controle autônomo de relés, programando horários de acionamento diretamente no módulo ESP-01S acoplado ao adaptador ESP8266 Relay.
Neste novo projeto, avançamos um passo além: a criação de uma rede de módulos ESP-01 equipados com Relé ou Sensor DHT11, todos se comunicando entre si e com uma Estação Central baseada em ESP32 por meio do protocolo ESP-NOW.
A Estação Central atua como um hub de gerenciamento, recebendo periodicamente as informações publicadas por cada nó e disponibilizando, via interface Web responsiva, uma visão consolidada de toda a rede.
Por essa interface é possível:
Tudo isso sem depender de roteadores externos ou infraestrutura de rede Wi-Fi complexa, pois a comunicação principal entre os módulos ocorre via ESP-NOW, um protocolo leve, rápido e de baixo consumo de energia.
Projetos anteriores mostraram o potencial do ESP8266 em aplicações simples de automação e monitoramento. Contudo, cada módulo operava de forma independente, exigindo acesso direto ao seu IP ou página de configuração para qualquer ajuste — um processo viável em pequenas instalações, mas pouco prático quando se deseja escalar o sistema para vários pontos de controle.
A motivação central deste projeto foi, portanto, evoluir de módulos autônomos para uma arquitetura coordenada, onde uma única Estação Central pudesse:
Além de otimizar o controle em aplicações domésticas e industriais, esse modelo cria uma base sólida para futuras expansões, como:
Em resumo, este projeto demonstra como o pequeno ESP-01S pode participar de um ecossistema distribuído de automação, robusto, configurável e gerenciável de forma centralizada, mantendo a simplicidade e o baixo custo que tornaram o ESP8266 tão popular.

Figura 1 – Adaptador ESP01 com Relé

Figura 2 – Adaptador ESP01 com DHT11

Figura 3 – Módulo ESP01

Figura 4 – Módulo ESP32 30 Pinos (Opção 1)

Figura 5 – Módulo ESP32C3 Mini (Opção 2)
O projeto foi estruturado para suportar diversas funções complementares que aumentam sua abrangência e demonstram boas práticas em IoT com recursos limitados:
Estação Central (ESP32)
Estações ESP-01 (Relé e DHT11)
Essas funções tornam o projeto uma plataforma modular e evolutiva, equilibrando simplicidade de uso com recursos típicos de sistemas IoT mais avançados.
Uso do WifiManager (Estação Central)
A biblioteca WifiManager foi utilizada pela permitir a definição da rede Wifi (na Estação Central) a ser utilizada pelo próprio usuário evitando assim que o SSID/Senha fiquem internos no código implicando em recompilação do aplicativo toda vez que a rede fosse mudada ou a senha alterada. Além disso, o WifiManager permite que o usuário defina certos parâmetros da aplicação dando maior flexibilidade e liberdade.
Passo a passo de como utilizar:

Figura 6 – Tela Inicial do WifiManager

Figura 7 – Tela de Conexão no Modo AP do WifiManager

Figura 8 – Tela de Funções do WifiManager

Figura 9 – Tela de Definição da Rede Wifi e Parâmetros
Utilização da ElegantOTA (Estação Central e Estações ESP01 Relé/DHT11)
A biblioteca ElegantOTA foi utilizada para permitir a atualização do aplicativo pela interface Web sem a necessidade de levar o circuito do ESP32 até a estação de compilação. Isso permite, por exemplo, que o desenvolvedor libere uma nova versão em qualquer lugar do mundo e o próprio usuário instale a nova versão dando maior independência.
Importante: na Referência 3 existe um link para instruções de como configurar a biblioteca ElegantOTA, de forma manual, em Assíncrono Mode. Isso é importante para a compilação dos fontes das três estações. Basta seguir os passos no tutorial.
Na Estação Central, a atualização é acionada através da URL http://central.local/update e, nas estações com relé e dht11, pressionando o botão “Atualizar Firmware”. Em seguida, basta autenticar com as credenciais definidas no WifiManager e selecionar o arquivo contendo a imagem a ser carregada.
A imagem pode ser gerada no Arduino IDE através do Menu conforme a figura a seguir. O arquivo .bin será gerado numa subpasta dentro da pasta do projeto.

Figura 10 – Geração da Imagem para Atualização via ElegantOTA

Figura 11 – Autenticação para Atualização de Versão

Figura 12 – Tela de Seleção da Versão a ser Atualizada
A arquitetura da solução baseia-se em uma rede ESP-NOW composta por uma Estação Central (ESP32) e vários nós periféricos ESP01S, configurados em duas modalidades:
O que é uma Rede ESP-NOW
O ESP-NOW é um protocolo de comunicação sem fio desenvolvido pela Espressif que permite a troca direta de mensagens entre módulos ESP8266 e ESP32, sem a necessidade de um roteador Wi-Fi. Ele utiliza o mesmo hardware de rádio do Wi-Fi tradicional, porém em um modo de operação “peer-to-peer”, com baixa latência e consumo reduzido de energia. Os dispositivos comunicam-se diretamente entre si (nós e estação central) por meio de endereços MAC, sendo essencial que todos estejam operando no mesmo canal de rádio. Embora uma rede ESP-NOW possa coexistir com o Wi-Fi convencional, ela não depende dele — o roteador é opcional e serve apenas para oferecer conectividade adicional, como dashboards web ou integração com nuvem.

Figura 13 – Exemplo de Rede ESP-NOW
Fluxo Geral de Comunicação
O fluxo de operação é ilustrado na figura a seguir.

Figura 14 – Fluxo de Operação das Estações
A Estação Central (no centro do diagrama) atua como controlador e servidor Web, recebendo periodicamente informações dos nós e permitindo o envio de comandos de controle.

Figura 15 – Tela Principal da Estação Central no Desktop

Figura 16 – Tela Principal da Estação Central no Celular
Gerenciamento e Estabilidade
Para garantir estabilidade:
Modo de Configuração e Atualização
Cada nó ESP01S pode entrar em modo de configuração de duas formas:
No modo de configuração, o módulo opera como Access Point (AP), exibindo uma página HTML simples para parametrização do SSID e alias.
A tabela a seguir mostra como os SSID’s aparecerão na Rede Wifi para a conexão e configuração:
Estação | SSID na Rede Wifi |
ESP01-Relé | ESP_Relay_nnnnnn |
ESP01-DHT11 | ESP_DHT11_nnnnnn |
Nesta mesma interface, é possível acessar o recurso ElegantOTA, permitindo atualização de firmware remota diretamente pelo navegador pressionando o botão Atualizar Firmware.

Figura 17 – Tela de Configuração nas Estações com Relé e DHT11
Determinação do RSSI em Rede ESP‑NOW
Nesta implementação, o monitoramento do nível de sinal (“RSSI” — Received Signal Strength Indicator) entre os nós (ESP01S) e a estação central (ESP32) foi tratado, porém com uma particularidade técnica importante, a saber: a versão do core usada no ESP32. A seguir, descrevemos o método adotado e como esse panorama evolui em versões posteriores do framework.
Contexto técnico
Para a versão **Arduino-ESP32 Core 2.0.17, que utilizamos neste projeto, o callback padrão da função esp_now_register_recv_cb(…) utiliza a assinatura:
void onRecv(const uint8_t *mac_addr, const uint8_t *data, int len);
Essa versão não disponibiliza direto no callback o RSSI ou outros metadados do pacote recebido. Documentação e fóruns apontam que a assinatura com estrutura esp_now_recv_info_t (que inclui rx_ctrl.rssi) só está disponível em ambientes baseados no ESP‑NOW Library mais recentes ou nas versões do Arduino Core baseadas em IDF 5.x. Arduino Forum+2Developer Portal+2 Portanto, na versão 2.0.17, para obter o RSSI, utilizou-se uma técnica “sniffer” em modo promíscuo da interface WiFi.
Método implementado
Esse método, apesar de “adicional”, mostrou-se confiável e com impacto negligenciável no desempenho da rede.
Futuro (versões Core 3.x ou baseadas em IDF 5.x)
Com a introdução da Arduino-ESP32 Core 3.x (ou versões baseadas em ESP-IDF 5.x), a API de ESP-NOW permite registrar o callback na nova assinatura:
void onNowRecv(const esp_now_recv_info_t *info, const uint8_t *data, int len);
Nesse contexto, o parâmetro info contém diretamente campos como info->src_addr, info->rx_ctrl.rssi, info->rx_ctrl.channel, etc. Espressif Docs+1 Dessa forma, não é mais necessário usar o sniffer ou cache externo: basta ler info->rx_ctrl.rssi dentro do callback e gravar no NodeInfo de imediato.
Comparativo rápido
Ambiente | Callback suportado | Método para RSSI |
Arduino-ESP32 Core 2.0.17 | void func(const uint8_t*, const uint8_t*, int) | Sniffer + cache + busca MAC |
Arduino-ESP32 Core 3.x / IDF 5 | void func(const esp_now_recv_info_t*, const uint8_t*, int) | Leitura direta de info->rx_ctrl.rssi |
Considerações finais
A escolha do método reflete uma decisão de compatibilidade: manter estabilidade e compatibilidade com a versão 2.0.17, ao mesmo tempo garantindo funcionalidade completa. Para futuros upgrades e adoção de versões mais novas, recomenda-se migrar para a assinatura moderna, simplificando o código e eliminando a sobrecarga de sniffer/cache. Assim, a documentação técnica do projeto está alinhada tanto com o cenário atual quanto com a evolução esperada da plataforma.
Preparação para carga do programa no ESP01S
Para carregamos o código no ESP01S precisaremos utilizar o adaptador USB com uma pequena modificação soldando dois fios nos pinos definidos na figura a seguir:

Figura 18 – Pinos para Soldar os fios para carga de programa
Utilizaremos a garra jacaré para unir os dois fios (curto circuito) para colocar o ESP01S no modo de carga de programa quando for rebootado. A figura a seguir mostra o adaptador já preparado:

Figura 19 – Adaptador USB preparado para usar na carga de programas no ESP01S
A tabela a seguir mostra qual Placa deve ser selecionada no ambiente Arduino IDE para a compilação dos fontes:
Microcontrolador | Placa a selecionar no IDE |
ESP32 | ESP32 Dev Module |
ESP01S | Generic ESP8266 Module |
//-----------------------------------------------------------------------------------------
// Função : Este programa tem como objetivo escutar broadcast de estações ESP8266 Relay
// e manter uma lista de nós para permitir o acesso Web ESP32 AsyncWebServer.
// As estações ESP8266 Relay poderão ser gerenciadas a partir do html
// e fazer o gerenciamento de todos os módulos numa interface html central.
//
// Resumo: 1) Recebe a auto divulgação via broadcast na Rede ESP-NOW
// 2) Envia comandos de LIGAR/DESLIGAR/GETSTATUS
// 3) Recebe o Status
//
// Funções adicionais
//
// 1) Inclusão de Alias no DNS da Rede Local (mDNS) para permitir o acesso através da
// URL http://central.local
// 2) Sincronização do Relógio interno com o Serviço NTP quando no Modo WiFi
// ou sincronismo com o relógio do Navegador junto com os Parâmetros
// 3) Utilização do WifiManager para configurações de Rede e Parâmetros. Durante a execução,
// pressionando-se o botão BOOT o WifiManager é ativado para reconfiguração da rede wifi
// e parâmetros. Quando o WifiManager é ativado no modo AP, o SSID=ESP_nnnn será
// visto nas redes disponíveis onde nnnn é o número de série do ESP32 em uso. Uma vez
// conectado neste SSID, o WifiManager pode ser acionado pela URL http://192.168.4.1:8080
// 4) Utilização da biblioteca ElegantOTA para atualização do código pela interface Web através
// da URL http://central.local/updade ou através do botão na interface.
//
//-----------------------------------------------------------------------------------------
// Autor : Dailton Menezes
// Versão : 1.0 Out/2025
//-----------------------------------------------------------------------------------------
#include <WiFi.h> // Biblioteca para a rede wifi genérica
#include <esp_wifi.h> // Biblioteca para a rede wifi do ESP32
#include <esp_wifi_types.h> // Bivlioteca para os types da rede Wifi
#include <esp_now.h> // Biblioteca do Protocolo ESP-NOW
#include <ArduinoJson.h> // Biblioteca para manipulação de estrutiras JSON
#include <SPIFFS.h> // Biblioteca que implementa o filesystem
#include <FS.h> // Biblioteca para tratar arquivos
#include <unordered_map> // Biblioteca para manuseio de lista não ordenadas
#include <vector> // Biblioteca para manuseio de vetores
#include <AsyncTCP.h> // Biblioteca usada pelo Servidor Assíncrono
#include <ESP32Ping.h> // Biblioteca para fazer Ping
#include <WiFiManager.h> // Biblioteca WiFi Manager
#include <ESPAsyncWebServer.h> // Biblioteca para Servidor Web Assíncrono
#include <ESPmDNS.h> // Biblioteca para adiocionar aliases no DNS da Rede Local
#include <freertos/FreeRTOS.h> // Biblioteca do Sistema Operacional
#include <freertos/queue.h> // Biblioteca para filas
#include <ElegantOTA.h> // Biblioteca para atualização via Web
//------------------------
// Definições do Programa
//------------------------
#define BAUDRATE 115200 // Baudrate para a Console
#define QUEUE_SIZE 32 // Tamanho da fila de mensagens
#define SNIFFER_SIZE 16 // Tamanho do buffer para SNIFFER
#define INTERVALO_VIDA 3000 // Intervalo de verificação do tempo de vida das estações
#define TEMPO_VIDA 30000 // 20s de limite sem receber mensagem da estação
#define NTP_SERVER "pool.ntp.org" // NTP Server para sincronismo do clock
#define TIMEZONE_LOCAL "<-03>3" // Timezone do Brasil
#define DNS_NAME "central" // Nome DNS
#define CONFIG_FILE "/appconfig.json" // Nome do Arquivo de COnfiguração no SPIFFS
#define USER_OTA "admin" // Default Usuário de atualização OTA
#define PASS_OTA "admin" // Default Senha de atualização OTA
#define AUTOREBOOT_OTA true // Default AutoReboot OTA
#define LED_BUILTIN GPIO_NUM_2 // Usado para ligar o LED se conectado no Wifi
#define LED_ON HIGH // Estado para ligar o Led BuiltIn
#define LED_OFF LOW // Estado para desligar o Led BuiltIn
#define PIN_BOOT GPIO_NUM_0 // Pino para forçar entrar no WifiManager
#define WIFIMANAGER_PORT 8080 // Porta para o WifiManager
#define INTERVALO_RECONECT 30000 // Intervalo para checar conexaõ com roteador
#define INTERVALO_CLEANUP 60000 // Intervalo de 10s para fazer o cleanup websocket
#define INTERVALO_NTP 30000 // Intervalo para retentar o sincronismo NTP
//--------------------------------------
// Definições de Constantes Importantes
//--------------------------------------
constexpr float RSSI_ALPHA = 0.25f; // suavização do RSSI móvel
using MacKey = uint64_t;
//--------------------
// Tipos de Mensagens
//--------------------
enum MsgType : uint8_t
{
MSG_DISC=1, // Brodacast de divulgação das estações para Central
MSG_CMD=2, // Comando ON/OFF do Relé
MSG_STATUS=3, // Status do Relé
MSG_GETALL=4, // Broadcast quem está aí a pedido da Central
MSG_SENSOR=5, // Status de estações cm DHT11
MSG_CFG=6 // Pedido da Central para a estação entrar em configuração
};
//-------------------------------
// Estrutura da Mensagem ESP-NOW
//-------------------------------
struct __attribute__((packed)) NowMsg
{
uint8_t ver, // Parar diferenciar registros entre outras aplicação
type, // Tipo de Mensagem
relay, // Estado do Relé
mode, // Modo Wifi da Estação
channel, // Canal da Rede ESP-NOW
aliasLen, // Tamanho do campo alias
rsv[2], // Para alinhamento
mac[6]; // MAC da estação
char alias[32]; // Alias da Estação (identificação)
int16_t tempC10; // temperatura em décimos de grau (ex.: 253 = 25.3°C)
uint8_t hum; // Umidade em %
};
//------------------------------------------------------------
// Estrutura da Fila de Mensagens recebidas das estações 8266
//------------------------------------------------------------
struct RxEvt
{
uint8_t mac[6]; // MAC recebido
uint8_t len; // tamanho
NowMsg msg; // regsitro Recebido
uint32_t ts; // timestamp do enfileiramento
int8_t rssi; // RSSI em dBm
};
//---------------------------------
// Estrutura da Mensagem WebSocket
//---------------------------------
struct NodeInfo
{
uint8_t mac[6]; // MAC da estação em bytes
String macStr; // MAC da estação em string
String alias; // Nome da Estação
uint8_t relay; // estado do relé
uint8_t mode; // Modo da Rede Wifi
uint8_t channel; // Canal da Rede ESP-NOW
uint32_t lastSeen; // millis() da última atualização
int16_t tempC10; // temperatura em décimos de °C (253 => 25.3°C)
uint8_t hum; // umidade relativa em %
bool isSensor; // true = card de sensor; false = card de relé
int8_t rssi; // último RSSI instantâneo
float rssiAvg; // média móvel
};
//-------------------------------------------------------
// Estrutura para representar os parâmetros no SPIFFS
//-------------------------------------------------------
struct AppConfig
{
int queue_size; // Intervalo de envio das medições em min
String alias; // Nome para adiocnar ao mDNS da Rede
String ntpServer; // URL do Servidor NTP para sinconizar relógio
String timezone; // String do Timezone
String user_OTA; // Usuário para atualização OTA
String pass_OTA; // Senha para atualização OTA
bool autoRebootOTA; // Se deve rebootar após a atualização OTA
};
AppConfig appConfig; // Estrutura para conter os parâmetros
//---------------------------------
// Cache de RSSI visto pelo sniffer
//---------------------------------
struct RssiEntry
{
uint8_t mac[6];
int8_t rssi;
uint32_t ts;
};
RssiEntry gRssiCache[SNIFFER_SIZE];
//-----------------------------------------------------------
// Cabeçalho 802.11 (suficiente para pegar addr2 = remetente)
//------------------------------------------------------------
typedef struct __attribute__((packed))
{
uint8_t frame_ctrl[2];
uint8_t duration[2];
uint8_t addr1[6];
uint8_t addr2[6]; // TRANSMITTER (fonte)
uint8_t addr3[6];
uint8_t seq_ctrl[2];
} wifi_ieee80211_mac_hdr_t;
//-------------------
// Variáveis Globais
//-------------------
AsyncWebServer server(80); // Servidor Web para receber acessos http
AsyncWebSocket ws("/ws"); // WebSocket para comunicação com o Navegador
IPAddress ip (1, 1, 1, 1); // The remote ip to ping, DNS do Google
std::unordered_map<MacKey, NodeInfo> nodes; // hash nativo p/ uint64_t existe
uint8_t gMyMac[6]; // MAC da estação
uint32_t tVarredura = 0; // ùltima varredura de tempo de vida das estações
QueueHandle_t qNow; // Fila de Mensagens recebidas
volatile bool buttonState = false; // Estado do botão Boot para Reconfiguração do WiFi
unsigned long lastCleanup = 0; // última limpeza de conexões perdidas WebSocket
unsigned long lastNTP = 0; // última tentatita de sincronismo com NTP Server
unsigned long lastReconect = 0; // ùltima tentativa de reconexão com o rpteador
bool sincronizado = false; // Se sincronizado ao NTP Server
//------------------------------
// Variáveis para o WifiManager
//------------------------------
WiFiManager wm; // Instância do Serviço WifiManager
WiFiManagerParameter *p_queue_size; // Parâmetro InetrvaloEnvio
WiFiManagerParameter *p_alias; // Parãmetro Alias
WiFiManagerParameter *p_ntpServer; // Parãmetro NTP Server
WiFiManagerParameter *p_timezone; // Parãmetro Timezone
WiFiManagerParameter *p_user_OTA; // Parãmetro User OTA
WiFiManagerParameter *p_pass_OTA; // Parãmetro Senha OTA
WiFiManagerParameter *p_autoRebootOTA; // Parãmetro autoRebbotOTA
//-------------------
// HTML Principal
//-------------------
const char* HTML = R"HTML(
<!doctype html><html><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Central ESPNOW</title>
<style>
.badge.rssi-excelente { background:#d4edda; color:#155724; border-color:#b6e2b8; }
.badge.rssi-bom { background:#fff3cd; color:#856404; border-color:#ffe08a; }
.badge.rssi-fraco { background:#f8d7da; color:#721c24; border-color:#f1aeb5; }
.badge.rssi-ruim { background:#f5c6cb; color:#721c24; border-color:#eb9fa4; }
/* grupo dos botões */
.controls { display:flex; gap:8px; }
/* botões com largura fixa, sem crescer no flex */
.controls .btn {
flex: 0 0 90px !important; /* largura fixa */
width: 90px !important;
white-space: nowrap;
}
.meta {
display:flex;
gap:8px;
align-items:center;
margin-top:10px;
}
.meta .btn-cfg {
margin-left:auto; /* encosta à direita */
flex: 0 0 auto;
padding: 6px 12px;
white-space: nowrap;
}
/* Base */
body{
font-family:system-ui,Segoe UI,Arial;
margin:20px;
}
/* Grade: no desktop empilha horizontal e quebra quando precisa */
.grid{
display:grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap:12px;
align-items:stretch;
}
/* Card */
.card{
border:1px solid #ddd;
border-radius:12px;
padding:12px;
width:auto;
max-width:100%;
box-sizing:border-box;
}
.card.sensor .top .state{ opacity: .9 }
.card .bottom-line {
display:flex;
justify-content:space-between;
align-items:center;
margin-top:8px;
}
/* Cabeçalho do card */
.top{
display:flex;
justify-content:space-between;
align-items:center;
}
/* Badge e MAC: evitar quebra feia */
.badge, .mac{
font-size:12px;
border-radius:999px;
word-break:break-all;
}
.badge{ padding:2px 8px; border:1px dashed #999; }
.mac{ font-family:monospace; color:#555; border:none; }
/* Botões amigáveis ao toque */
.btn {
padding: 6px 10px;
border-radius: 8px;
border: 1px solid #aaa;
background: #f7f7f7;
cursor: pointer;
flex: 1; /* <<< cada botão ocupa metade da linha */
text-align: center;
white-space: nowrap; /* evita quebra de texto */
}
.btn:hover {
filter: brightness(0.97);
}
.state{ font-weight:700; }
/* ===== Responsivo ===== */
/* Celular: força 1 coluna (vertical) e aumenta áreas de toque */
@media (max-width: 480px){
body{ margin:10px; font-size:15px; }
.grid{ grid-template-columns: 1fr; } /* só vertical */
.btn{ padding:12px 14px; font-size:15px; }
}
/* (Opcional) Telas muito largas: cards um pouco maiores antes de quebrar */
@media (min-width: 1280px){
.grid{ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); }
}
</style>
</head><body>
<h2>Central de Estações ESPNOW</h2>
<div id="grid" class="grid"></div>
<script>
let ws;
// mapa para controlar "idade" por MAC
const nodeAges = {}; // { "E8:DB:84:...": { localTs: Date.now(), lastSeenMs: n.lastSeen } }
const grid = document.getElementById('grid');
const cards = new Map();
// formata texto "atualizado há ..."
function formatElapsed(s){
if (s < 5) return "atualizado agora";
if (s < 60) return `atualizado há ${s}s`;
const m = Math.floor(s/60);
if (m < 60) return `atualizado há ${m}min`;
const h = Math.floor(m/60);
return `atualizado há ${h}h`;
}
// atualiza a badge de um nó específico
function updateAges(){
const now = Date.now();
document.querySelectorAll('.age').forEach(span=>{
const ts = parseInt(span.dataset.ts || '0', 10);
if (!ts) return;
const sec = Math.floor((now - ts)/1000);
span.textContent = (sec < 1) ? 'atualizado agora' : `atualizado há ${sec} s`;
});
}
// roda a cada 1s e atualiza todas as badges
setInterval(updateAges, 1000);
// Renderiza os Cards de Relé e DHT11
function renderCard(n){
let el = cards.get(n.mac);
const isSensor = !!n.isSensor; // ou derive por ('tempC' in n)
if(!el){
el = document.createElement('div');
el.className='card';
// bloco comum do topo
el.innerHTML = `
<div class="top">
<div><div class="state" id="st"></div><div class="mac" id="mac"></div></div>
<div class="badge" id="alias"></div>
</div>
<div style="margin-top:8px">Modo: <span id="mode"></span> · Canal: <span id="ch"></span></div>
<div id="body" style="margin-top:10px"></div>
<!-- linha inferior com idade, rssi e botão Configurar -->
<div class="meta">
<span class="badge age" data-ts="${Date.now()}">atualizado há 0 s</span>
<span class="badge" id="rssi">–</span>
<button class="btn btn-cfg" id="cfg">Configurar</button>
</div>
`;
// corpo específico por tipo
const body = el.querySelector('#body');
if (isSensor) {
body.innerHTML = `
<div style="display:flex; justify-content:space-between; gap:12px;">
<div>Temperatura: <b id="t"></b></div>
<div>Umidade: <b id="h"></b></div>
</div>
`;
} else {
body.innerHTML = `
<div class="controls">
<button class="btn" id="on">Ligar</button>
<button class="btn" id="off">Desligar</button>
</div>
`;
el.querySelector('#on').onclick = ()=> send({cmd:'set', mac:n.mac, relay:1});
el.querySelector('#off').onclick = ()=> send({cmd:'set', mac:n.mac, relay:0});
}
// ação do botão Configurar (envia comando para o nó entrar em Modo AP)
el.querySelector('#cfg').onclick = () => {
if (confirm(`Colocar ${n.alias || n.mac} em Modo AP de Configuração?\nIsso irá reiniciar o módulo.`)) {
send({ cmd: 'config', mac: n.mac }); // <<< use "config"
// feedback visual opcional por 3s
const b = el.querySelector('#cfg');
const old = b.textContent;
b.disabled = true; b.textContent = 'Enviando...';
setTimeout(()=>{ b.disabled=false; b.textContent=old; }, 3000);
}
};
cards.set(n.mac, el);
grid.appendChild(el);
}
// atualizações comuns
el.querySelector('#mac').textContent = n.mac;
el.querySelector('#alias').textContent = n.alias || '(sem alias)';
el.querySelector('#mode').textContent = ({0:'STA',1:'AP',2:'AP+STA'})[n.mode]||n.mode;
el.querySelector('#ch').textContent = n.channel;
// estado / leituras
if (isSensor) {
el.querySelector('#st').textContent = 'Sensor DHT11';
if (el.querySelector('#t')) el.querySelector('#t').textContent =
(n.tempC !== undefined) ? `${n.tempC.toFixed(1)} °C` : '–';
if (el.querySelector('#h')) el.querySelector('#h').textContent =
(n.hum !== undefined) ? `${n.hum} %` : '–';
} else {
el.querySelector('#st').textContent = n.relay ? 'Ligado' : 'Desligado';
}
// RSSI (comum)
if (el.querySelector('#rssi')) {
const rssi = n.rssi;
let text = '–';
let cls = 'rssi-ruim'; // default
if (rssi !== undefined) {
const cat = rssiClass(rssi);
text = `${rssi} dBm (${cat})`;
cls = 'rssi-' + cat;
}
const span = el.querySelector('#rssi');
span.textContent = text;
span.className = 'badge ' + cls;
}
const ageSpan = el.querySelector('.age');
if (ageSpan) ageSpan.dataset.ts = String(Date.now());
// “idade” (comum) — você já tem seu ticker de age; aqui só atualiza o carimbo
if (!nodeAges[n.mac]) nodeAges[n.mac] = {};
nodeAges[n.mac].localTs = Date.now();
nodeAges[n.mac].lastSeen = n.lastSeen; // vindo do backend
}
function rssiClass(dbm){
if (dbm >= -60) return 'excelente';
if (dbm >= -70) return 'bom';
if (dbm >= -80) return 'fraco';
return 'ruim';
}
function isValidMacStr(s){
return typeof s === 'string' && /^([0-9A-F]{2}:){5}[0-9A-F]{2}$/i.test(s);
}
function applySnapshot(nodesArray){
if (!Array.isArray(nodesArray)) {
// caso venha como objeto {mac:node,...}
nodesArray = Object.values(nodesArray || {});
}
for (const n of nodesArray) {
if (isValidMacStr(n?.mac)) renderCard(n);
else console.warn('[UI] Snapshot item sem MAC válido:', n);
}
}
function send(o) {
ws.send(JSON.stringify(o));
}
function removeCard(mac){
if (!isValidMacStr(mac)) return;
const el = cards.get(mac);
if (el && el.parentNode) el.parentNode.removeChild(el);
cards.delete(mac);
delete nodeAges[mac]; // para o setInterval não tentar atualizar a badge
}
function inicializaWebSocket() {
const proto = location.protocol === 'https:' ? 'wss' : 'ws';
ws = new WebSocket(`${proto}://${location.host}/ws`);
ws.onmessage = (ev)=>{
let msg;
try { msg = JSON.parse(ev.data); } catch(e){ return; }
if (msg.type === 'snapshot') {
applySnapshot(msg.nodes);
} else if (msg.type === 'update') {
renderCard(msg);
} else if (msg.type === 'remove') {
removeCard(msg.mac);
}
};
}
window.onload = inicializaWebSocket;
window.addEventListener("unload", () => ws?.close());
document.addEventListener("visibilitychange", () => {
if (document.visibilityState === 'visible' && (!ws || ws.readyState !== WebSocket.OPEN)) {
inicializaWebSocket();
}
});
</script>
</body></html>
)HTML";
//-------------------------
// Prototipação de Rotinas
//-------------------------
String macToStr(const uint8_t m[6]);
//void ensurePeer(const uint8_t mac[6], uint8_t channel);
void broadcastNodesListToWS();
void onNowRecv(const uint8_t *mac, const uint8_t *data, int len);
void onWsMessage(void *arg, uint8_t *data, size_t len);
void setupEspNowCentral();
void setupWifi();
void setupServer();
static inline MacKey macToKey(const uint8_t m[6]);
static inline bool strToMac(const String& s, uint8_t out[6]);
static inline bool macIsZero(const uint8_t m[6]);
void expireStaleNodesAndNotify(uint32_t ttlMs = 20000);
bool setDNSNAME(String nome);
void ensurePeerOnHomeChannel(const uint8_t mac[6]);
esp_err_t sendNowWithPeerFix(const uint8_t mac[6], const uint8_t* buf, size_t len);
void debugPeer(const uint8_t mac[6]);
void drainNowQueue();
String makeUpdateJson(const NodeInfo& n); // para nós de RELÉ
String makeSensorUpdateJson(const NodeInfo& n); // para nós DHT
String macToStr(const uint8_t m[6]);
void printFreeRAM(String context) ;
String formatTimeStamp(time_t now);
String getTimeStamp();
bool getNTPtime(int sec);
bool refreshNTPServer();
void WiFiEvent(WiFiEvent_t event);
void configModeCallback(WiFiManager *myWiFiManager);
bool loadConfig();
bool saveConfig();
void buttonISR();
static inline bool macEq(const uint8_t a[6], const uint8_t b[6]);
static void rssiCachePut(const uint8_t mac[6], int8_t rssi);
static bool rssiCacheGet(const uint8_t mac[6], int8_t &out);
static void IRAM_ATTR snifferCb(void* buf, wifi_promiscuous_pkt_type_t type);
static void enableSniffer();
//---------------------------
// Inicialização do Programa
//---------------------------
void setup()
{
// Inicializa a Serial
Serial.begin(BAUDRATE);
delay(1000);
Serial.println(F("Central de Relay V1.0 Out/2025"));
Serial.flush();
// Inicializa o Botão interno do ESP para Acionar o WifiManager
pinMode(PIN_BOOT, INPUT_PULLUP);
// Configura a interrupção para detectar a borda de descida do botão Boot
attachInterrupt(digitalPinToInterrupt(PIN_BOOT), buttonISR, FALLING);
// Define o LED_BUILTIN como indicador WiFi. Será ligado pela rotina de
// eventos quando conectado no WiFi ou desligado quando fora
pinMode(LED_BUILTIN,OUTPUT);
// Carrega o Config
loadConfig();
// Cria a fila para enfileiramento de mensagens
qNow = xQueueCreate(appConfig.queue_size, sizeof(RxEvt));
// Inicializa o Wifi
setupWifi();
// Inicializa a Rede ESP-NOW
setupEspNowCentral();
// Inicializa o Servidor Web
setupServer();
// Mostra Consumo de Memória
printFreeRAM("Setup...");
}
//----------------------------
// Loop Prinicpal do Programa
//----------------------------
void loop()
{
// Esgota a fila de Mensagens
drainNowQueue();
// Verifica se tem que fazer a varredura de tempo de vida das estações
uint32_t now = millis();
if (now - tVarredura >= INTERVALO_VIDA)
{
tVarredura = now;
expireStaleNodesAndNotify(TEMPO_VIDA); // 20 s de TTL
}
//--------------------------------------------------------------------------------------------------
// Verifica se o botão de BOOT foi apertado para forçar a entrada no modo de configuração.
// É útil quando a senha do wifi mudou ou está se conectando em outra rede wifi. Isso
// evita ter o SSID/senha no código, a recompilação e upload do código no ESP32.
//--------------------------------------------------------------------------------------------------
if (buttonState)
{
// Reseta o estado do botão
buttonState = false;
// Força a entrada em modo de configuração
wm.resetSettings();
ESP.restart();
}
// Verifica se precisa reconectar a WiFi caso o roteador tenha sido rebootado
if ((WiFi.status() != WL_CONNECTED) && (now - lastReconect >=INTERVALO_RECONECT))
{
Serial.println("Reconnecting to WiFi...");
WiFi.disconnect();
WiFi.reconnect();
lastReconect = now;
}
// Verifica se não está sincronizado e no intervalo para tentar sincronizar novamente
if (!sincronizado && millis()-lastNTP > INTERVALO_NTP)
{
sincronizado = refreshNTPServer();
lastNTP = millis();
}
// Verifica se deve fazer o Cleanup de conexões perdidas
if (now - lastCleanup > INTERVALO_CLEANUP) {
lastCleanup = now;
ws.cleanupClients();
}
// Verifica o OTA para saber se há atualização
ElegantOTA.loop();
}
//--------------------------------------
// Convertet o MAC de bytes para String
//--------------------------------------
String macToStr(const uint8_t m[6])
{
char b[18];
sprintf(b, "%02X:%02X:%02X:%02X:%02X:%02X", m[0],m[1],m[2],m[3],m[4],m[5]);
return String(b);
}
//----------------------------------------------------------------
// Envia a lista de Estações na Rede ESP-NOW para a interface Web
//----------------------------------------------------------------
void broadcastNodesListToWS()
{
// Monta JSON manualmente para evitar alocação excessiva
String json;
json.reserve(256 + nodes.size() * 96);
json = F("{\"type\":\"snapshot\",\"nodes\":[");
bool first = true;
for (auto &kv : nodes) {
const NodeInfo &n = kv.second;
if (!first) json += ',';
first = false;
json += F("{\"mac\":\""); json += n.macStr; json += F("\",");
json += F("\"alias\":\""); json += n.alias; json += F("\",");
json += F("\"relay\":"); json += String(n.relay); json += F(",");
json += F("\"mode\":"); json += String(n.mode); json += F(",");
json += F("\"channel\":"); json += String(n.channel); json += F(",");
json += F("\"last\":"); json += String(n.lastSeen);
json += F("}");
}
json += F("]}");
ws.textAll(json);
}
//---------------------------------------------------
// Trata a recepção de mensagens da rede ESP-NOW.
// enfileirando para tratar depois e não comprometer
// a recepção sem colisão (perda de mensagem)
//---------------------------------------------------
void onNowRecv(const uint8_t *mac, const uint8_t *data, int len)
{
RxEvt evt{};
memcpy(evt.mac, mac, 6);
evt.len = (uint8_t)min(len, (int)sizeof(NowMsg));
memcpy(&evt.msg, data, evt.len);
evt.ts = millis();
int8_t r;
evt.rssi = rssiCacheGet(mac, r) ? r : -127; // fallback se não achar
xQueueSendFromISR(qNow, &evt, nullptr); // não bloqueia
}
//-------------------------------------------
// Trata as mensagens recebidas do WebSocket
//-------------------------------------------
void onWsMessage(void *arg, uint8_t *data, size_t len)
{
AwsFrameInfo *info = (AwsFrameInfo*)arg;
if (!(info->final && info->index == 0 && info->len == len && info->opcode == WS_TEXT)) return;
StaticJsonDocument<200> doc;
if (deserializeJson(doc, data, len)) return;
String cmd = doc["cmd"] | ""; // "set", "get", "config"
String macS = doc["mac"] | ""; // "AA:BB:CC:DD:EE:FF"
// MAC obrigatório para comandos por nó
uint8_t macBytes[6];
if (!strToMac(macS, macBytes)) {
Serial.println(F("[WS] MAC inválido no comando"));
return;
}
MacKey key = macToKey(macBytes);
auto it = nodes.find(key);
if (it == nodes.end()) {
Serial.printf("[WS] Nó %s não encontrado na tabela\n", macS.c_str());
return;
}
auto &n = it->second;
// Monta a mensagem ESP-NOW
NowMsg out{};
out.ver = 1;
if (cmd == "set") {
out.type = MSG_CMD;
out.relay = (uint8_t)(doc["relay"] | 0); // 0 = OFF, 1 = ON
}
else if (cmd == "get") {
out.type = MSG_CMD;
out.relay = 2; // 2 = GET (status)
}
else if (cmd == "config") {
// Pedido para o nó entrar em modo de configuração:
// apagar arquivo de config e reiniciar (o nó implementa isso no RX)
out.type = MSG_CFG;
out.relay = 1; // subcódigo simples
}
else {
Serial.printf("[WS] Comando desconhecido: %s\n", cmd.c_str());
return;
}
// Log e envio
debugPeer(n.mac); // seu helper de depuração de peer
// IMPORTANTE: não use n.channel diretamente; deixe o sendNowWithPeerFix
// garantir que o peer está no canal correto do STA.
esp_err_t rc = sendNowWithPeerFix(n.mac, (uint8_t*)&out, sizeof(NowMsg));
Serial.printf("[WS] cmd=%s -> %s rc=%d (homeCh=%u)\n",
cmd.c_str(), macS.c_str(), rc, WiFi.channel());
}
//----------------------------------
// Trata os eventos do Servidor Web
//----------------------------------
void wsEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type,
void *arg, uint8_t *data, size_t len)
{
if (type == WS_EVT_CONNECT)
{
// manda snapshot inicial
broadcastNodesListToWS();
}
else if (type == WS_EVT_DATA)
{
onWsMessage(arg, data, len);
}
}
//----------------------------------------
// Rotina para Inicializar a Rede ESP-NOW
//----------------------------------------
void setupEspNowCentral()
{
//WiFi.mode(WIFI_STA);
//WiFi.disconnect(); // mantém livre para trocar canal por peer
Serial.print("Iniciando a Rede ESP-NOW ... ");
if (esp_now_init()!=ESP_OK)
{
Serial.println("Erro ao inicializar");
ESP.restart();
}
int rc = esp_now_register_recv_cb(onNowRecv);
Serial.printf("iniciada com sucesso e callback registrado Rc=%d\n",rc);
// Habilita a escuta Sniffer
enableSniffer();
}
//----------------------------------------
// Rotina para Inicializar a Rede Wifi
//----------------------------------------
void setupWifi()
{
Serial.print(F("Iniciando a Conexão Wifi "));
WiFi.onEvent(WiFiEvent);
WiFi.mode(WIFI_STA);
WiFi.setAutoReconnect(true);
// Defina a porta do WiFiManager para 8080 no modo AP para não conflitar com a
// porta 80 que vamos utilizar para responder as requisições
wm.setHttpPort(WIFIMANAGER_PORT); // Porta
wm.setConfigPortalTimeout(180); // Timeout de 3 minutos
wm.setTimeout(180); // o Portal fica aberto por 180s
wm.setConnectTimeout(30); // tenta conectar por 30 segundos antes de entrar no Portal
wm.setAPCallback(configModeCallback); // Callback para o Modo AP de Configuração
wm.setWiFiAPChannel(6); // Força o canal ser 6 para ser mais fácil identificado
// Criação dos parâmetros
p_queue_size = new WiFiManagerParameter("queueSize", "Tamanho da Fila", String(appConfig.queue_size).c_str(), 3);
p_alias = new WiFiManagerParameter("alias", "Alias mDNS", appConfig.alias.c_str(), 30);
p_ntpServer = new WiFiManagerParameter("ntpServer", "Servidor NTP", appConfig.ntpServer.c_str(), 64);
p_timezone = new WiFiManagerParameter("timezone", "Fuso horário", appConfig.timezone.c_str(), 64);
p_user_OTA = new WiFiManagerParameter("user_OTA", "Usuário OTA", appConfig.user_OTA.c_str(), 32);
p_pass_OTA = new WiFiManagerParameter("pass_OTA", "Senha OTA", appConfig.pass_OTA.c_str(), 32);
p_autoRebootOTA = new WiFiManagerParameter("autoRebootOTA", "Reiniciar após OTA", "true", 6);
// Adiciona os parâmetros ao WiFiManager
wm.addParameter(p_queue_size);
wm.addParameter(p_alias);
wm.addParameter(p_ntpServer);
wm.addParameter(p_timezone);
wm.addParameter(p_user_OTA);
wm.addParameter(p_pass_OTA);
wm.addParameter(p_autoRebootOTA);
// Nome da rede AP temporária
char ssid[32];
sprintf(ssid, "ESP_%6X",(uint32_t)ESP.getEfuseMac());
//wm.resetSettings(); // vou forçar como teste
if (!wm.autoConnect(ssid))
{
Serial.println("Falha na conexão ou timeout. Reiniciando...");
delay(3000);
ESP.restart();
return;
}
Serial.println("WiFi conectado com sucesso!");
Serial.print("IP: ");
Serial.println(WiFi.localIP());
// Imprime o MAC
Serial.print("MAC: ");
Serial.println(WiFi.macAddress());
// Imprime o Sinal Wifi
Serial.print("Sinal: ");
Serial.print(WiFi.RSSI());
Serial.println(" db");
// Guarda o MAC
esp_wifi_get_mac(WIFI_IF_STA, gMyMac); // ou WIFI_IF_AP para modo AP
Serial.printf("Mac=%s Canal=%d\n",macToStr(gMyMac).c_str(),WiFi.channel());
// Desabilita Sleep na interface Wifi para não perder broadcast de estações
WiFi.setSleep(false);
esp_wifi_set_ps(WIFI_PS_NONE);
// Atualiza appConfig com os valores fornecidos
appConfig.queue_size = atoi(p_queue_size->getValue());
appConfig.alias = String(p_alias->getValue());
appConfig.ntpServer = String(p_ntpServer->getValue());
appConfig.timezone = String(p_timezone->getValue());
appConfig.user_OTA = String(p_user_OTA->getValue());
appConfig.pass_OTA = String(p_pass_OTA->getValue());
String r = String(p_autoRebootOTA->getValue());
r.toLowerCase();
r.trim();
appConfig.autoRebootOTA = (r == "true" || r == "1");
// Sincroniza o horário interno com o Servidor NTP nacional
sincronizado = refreshNTPServer();
// Definição do DNSName e Hostname
Serial.println("Adicionando " + appConfig.alias + " no mDNS... ");
if (setDNSNAME(appConfig.alias))
{
Serial.println("adicionado corretamente no mDNS!");
}
else
{
Serial.println("Erro ao adicionar no mDNS!");
}
// Salva o Config pois pode ter passado pelo WifiManager
saveConfig();
}
//----------------------------------------
// Rotina para Inicializar o Servidor Web
//----------------------------------------
void setupServer()
{
// Define a rotima de eventos (callback)
ws.onEvent(wsEvent);
// Adiciona hadle do WebSocket
server.addHandler(&ws);
// Define a rota principal
server.on("/", HTTP_GET, [](AsyncWebServerRequest* req){ req->send(200, "text/html", HTML); });
// Inicializa o serviço web
server.begin();
// Inicializa o serviço OTA
ElegantOTA.begin(&server, appConfig.user_OTA.c_str(), appConfig.pass_OTA.c_str());
ElegantOTA.setAutoReboot(appConfig.autoRebootOTA);
}
//-----------------------------------------
// Macro para gerar o hash a partir do MAC
//-----------------------------------------
static inline MacKey macToKey(const uint8_t m[6]) {
return ((uint64_t)m[0] << 40) |
((uint64_t)m[1] << 32) |
((uint64_t)m[2] << 24) |
((uint64_t)m[3] << 16) |
((uint64_t)m[4] << 8) |
((uint64_t)m[5] << 0);
}
//------------------------------------------------
// Macro para converter o MAC de string para bytes
//------------------------------------------------
static inline bool strToMac(const String& s, uint8_t out[6]) {
unsigned int v[6];
if (sscanf(s.c_str(), "%02x:%02x:%02x:%02x:%02x:%02x",
&v[0],&v[1],&v[2],&v[3],&v[4],&v[5]) != 6) return false;
for (int i=0;i<6;i++) out[i] = (uint8_t)v[i];
return true;
}
//---------------------------------------------------------------------
// ===== Expiração de nós inativos =====
// Remove quem ficou sem atualizar por 'ttlMs' e notifica via snapshot
//----------------------------------------------------------------------
void expireStaleNodesAndNotify(uint32_t ttlMs)
{
const uint32_t now = millis();
std::vector<MacKey> toErase;
toErase.reserve(nodes.size());
for (auto &kv : nodes) {
const NodeInfo &n = kv.second;
// comparação wrap-safe
if ((int32_t)(now - n.lastSeen) > (int32_t)ttlMs) {
toErase.push_back(kv.first);
}
}
for (auto &key : toErase) {
const NodeInfo &n = nodes[key];
if (!macIsZero(n.mac)) {
StaticJsonDocument<96> d;
d["type"] = "remove";
d["mac"] = macToStr(n.mac);
String s; serializeJson(d, s);
ws.textAll(s); // avisa o front-end PARA REMOVER
}
nodes.erase(key); // remove do mapa
}
}
//-------------------------------------------------------
// 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;
}
//--------------------------------------
// Garante o peer no mesmo canal da Rede
//--------------------------------------
void ensurePeerOnHomeChannel(const uint8_t mac[6])
{
const uint8_t home = WiFi.channel(); // canal atual do STA (Central)
if (esp_now_is_peer_exist(mac)) {
esp_now_peer_info_t p{};
if (esp_now_get_peer(mac, &p) == ESP_OK) {
if (p.channel == home && p.ifidx == WIFI_IF_STA) {
return; // já está correto
}
}
esp_now_del_peer(mac); // canal/IF divergentes → remove e recria
}
esp_now_peer_info_t info{};
memcpy(info.peer_addr, mac, 6);
info.ifidx = WIFI_IF_STA; // Central opera em STA
info.channel = home; // SEMPRE o canal da Central
info.encrypt = false;
esp_now_add_peer(&info);
}
//-----------------------------------------------------------------------
// Envia para o peer garantido que o mesmo está registardo no canal certo
//-----------------------------------------------------------------------
esp_err_t sendNowWithPeerFix(const uint8_t mac[6], const uint8_t* buf, size_t len)
{
ensurePeerOnHomeChannel(mac);
esp_err_t rc = esp_now_send(mac, buf, len);
if (rc == 12390 /* peer/home mismatch */ || rc == 12293 /* not found, variações */) {
esp_now_del_peer(mac);
ensurePeerOnHomeChannel(mac);
rc = esp_now_send(mac, buf, len);
}
return rc;
}
//----------------------------------
// Mostra o MAC em situação de debug
//----------------------------------
void debugPeer(const uint8_t mac[6])
{
esp_now_peer_info_t p{};
if (esp_now_get_peer(mac, &p) == ESP_OK) {
Serial.printf("[32] peer %02X:%02X:%02X:%02X:%02X:%02X ch=%u if=%u\n",
mac[0],mac[1],mac[2],mac[3],mac[4],mac[5], p.channel, p.ifidx);
} else {
Serial.println("[32] peer não encontrado");
}
}
//-------------------------------------------------
// Rotina para esvaziar/tratar a fila de mensagens
//-------------------------------------------------
void drainNowQueue()
{
RxEvt evt;
// drena alguns por loop (evita monopolizar)
for (int i=0; i<8; ++i) {
if (xQueueReceive(qNow, &evt, 0) != pdTRUE) break;
// valida tamanho mínimo
if (evt.len < 1) continue;
// atualiza/insere nó
MacKey key = macToKey(evt.mac);
NodeInfo &n = nodes[key];
memcpy(n.mac, evt.mac, 6);
n.lastSeen = evt.ts;
n.rssi = -127;
n.rssiAvg = NAN;
// RSSI
n.rssi = evt.rssi;
if (!isfinite(n.rssiAvg)) n.rssiAvg = evt.rssi;
else n.rssiAvg = RSSI_ALPHA * evt.rssi + (1.0f - RSSI_ALPHA) * n.rssiAvg;
// DISC / STATUS (nó de RELÉ)
if (evt.msg.type == MSG_STATUS || evt.msg.type == MSG_DISC)
{
n.isSensor = false;
n.relay = evt.msg.relay;
n.mode = evt.msg.mode;
n.channel = evt.msg.channel;
if (evt.msg.aliasLen) n.alias = String(evt.msg.alias);
ws.textAll(makeUpdateJson(n)); // <<< RELÉ
}
// SENSOR (DHT11)
else if (evt.msg.type == MSG_SENSOR)
{
n.isSensor = true;
n.mode = evt.msg.mode; // opcional
n.channel = evt.msg.channel; // opcional
if (evt.msg.aliasLen) n.alias = String(evt.msg.alias);
n.tempC10 = evt.msg.tempC10; // <<< use tempC10 (não tempC)
n.hum = evt.msg.hum;
// RSSI (seu código atual)
n.rssi = evt.rssi;
if (!isfinite(n.rssiAvg)) n.rssiAvg = evt.rssi;
else n.rssiAvg = 0.25f*evt.rssi + 0.75f*n.rssiAvg;
ws.textAll(makeSensorUpdateJson(n)); // <<< SENSOR
}
}
}
//----------------------------------------
// Monta o JSON com dados da Estação Relé
//----------------------------------------
String makeUpdateJson(const NodeInfo& n) {
StaticJsonDocument<256> d;
d["type"] = "update";
d["mac"] = macToStr(n.mac);
d["alias"] = n.alias;
d["mode"] = n.mode;
d["channel"] = n.channel;
d["relay"] = n.relay;
d["lastSeen"]= n.lastSeen;
d["rssi"] = n.rssi; // instantâneo
d["rssiAvg"] = lroundf(n.rssiAvg); // média arredondada
String s; serializeJson(d, s); return s;
}
//----------------------------------------
// Monta o JSON com dados da Estação DTH11
//----------------------------------------
String makeSensorUpdateJson(const NodeInfo& n)
{
StaticJsonDocument<256> d;
d["type"] = "update";
d["isSensor"] = true;
d["mac"] = macToStr(n.mac);
d["alias"] = n.alias;
d["mode"] = n.mode;
d["channel"] = n.channel;
// envia já em °C com 1 casa
d["tempC"] = (float)n.tempC10 / 10.0f;
d["hum"] = n.hum;
d["lastSeen"] = n.lastSeen;
d["rssi"] = n.rssi;
d["rssiAvg"] = lroundf(n.rssiAvg);
String s; serializeJson(d, s); return s;
}
//-----------------------------------
// Verifica se o MAC não está zerado
//-----------------------------------
static inline bool macIsZero(const uint8_t m[6])
{
for (int i=0;i<6;i++) if (m[i]!=0) return false;
return true;
}
//--------------------------------------------------
// 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));
}
//------------------------------------------------
// Formata Time para String
//------------------------------------------------
String formatTimeStamp(time_t now)
{
char timestamp[30];
strftime(timestamp, 30, "%d/%m/%Y %T", localtime(&now));
return String(timestamp);
}
//------------------------------------------------
// Devolve o localtime dd/mm/aaaa hh:mm:ss
//------------------------------------------------
String getTimeStamp()
{
time_t now;
time(&now);
return formatTimeStamp(now);
}
//---------------------------------------------------------
// 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 de fazer o sincronismo do relógio interno com o
// servidor NTP definido nos parâmetros do WifiManager
//----------------------------------------------------------
bool refreshNTPServer()
{
// 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());
// 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");
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");
return true;
}
else
{
Serial.println("Timer interno não foi sincronizado");
//ESP.restart();
}
}
return false;
}
//------------------------------------------------
// Evento chamado no processo de conexão do Wifi
//------------------------------------------------
void WiFiEvent(WiFiEvent_t event)
{
Serial.printf("[Evento Wi-Fi] evento: %d\n", event);
switch (event)
{
case SYSTEM_EVENT_WIFI_READY:
Serial.println("interface WiFi pronta");
break;
case SYSTEM_EVENT_SCAN_DONE:
Serial.println("Pesquisa por AP completada");
break;
case SYSTEM_EVENT_STA_START:
Serial.println("Cliente WiFi iniciado");
break;
case SYSTEM_EVENT_STA_STOP:
Serial.println("Clientes WiFi cancelados");
break;
case SYSTEM_EVENT_STA_CONNECTED:
Serial.println("Conectado ao AP");
digitalWrite(LED_BUILTIN,LED_ON); // Liga o LED Vermelho para mostrar a conexão com WiFi
break; // Observe que é invertido
case SYSTEM_EVENT_STA_DISCONNECTED:
Serial.println("Desconectado do AP WiFi");
digitalWrite(LED_BUILTIN,LED_OFF); // Desliga o LED Vermelho para mostrar a desconexão com WiFi
break; // Observe que é invertido
case SYSTEM_EVENT_STA_AUTHMODE_CHANGE:
Serial.println("Modo de Autenticação do AP mudou");
break;
case SYSTEM_EVENT_STA_GOT_IP:
Serial.print("Endereço IP obtido: ");
Serial.println(WiFi.localIP());
break;
case SYSTEM_EVENT_STA_LOST_IP:
Serial.println("Endereço IP perdido e foi resetado para 0");
break;
case SYSTEM_EVENT_STA_WPS_ER_SUCCESS:
Serial.println("WPS: modo enrollee bem sucedido");
break;
case SYSTEM_EVENT_STA_WPS_ER_FAILED:
Serial.println("WPS: modo enrollee falhou");
break;
case SYSTEM_EVENT_STA_WPS_ER_TIMEOUT:
Serial.println("WPS: timeout no modo enrollee");
break;
case SYSTEM_EVENT_STA_WPS_ER_PIN:
Serial.println("WPS: pin code no modo enrollee");
break;
case SYSTEM_EVENT_AP_START:
Serial.println("AP Wifi Iniciado");
break;
case SYSTEM_EVENT_AP_STOP:
Serial.println("AP Wifi parado");
break;
case SYSTEM_EVENT_AP_STACONNECTED:
Serial.println("Cliente conectado");
break;
case SYSTEM_EVENT_AP_STADISCONNECTED:
Serial.println("Cliente desconectado");
break;
case SYSTEM_EVENT_AP_STAIPASSIGNED:
Serial.println("IP associado ao Cliente");
break;
case SYSTEM_EVENT_AP_PROBEREQRECVED:
Serial.println("Requisição de probe recebida");
break;
case SYSTEM_EVENT_GOT_IP6:
Serial.println("IPv6 é preferencial");
break;
case SYSTEM_EVENT_ETH_START:
Serial.println("Interface Ethernet iniciada");
break;
case SYSTEM_EVENT_ETH_STOP:
Serial.println("Interface Ethernet parada");
break;
case SYSTEM_EVENT_ETH_CONNECTED:
Serial.println("Interface Ethernet conectada");
break;
case SYSTEM_EVENT_ETH_DISCONNECTED:
Serial.println("Interface Ethernet desconectada");
break;
case SYSTEM_EVENT_ETH_GOT_IP:
Serial.println("Endereço IP obtido");
break;
default: break;
}
}
//----------------------------------------------------------
// Callback para WifiManager
//----------------------------------------------------------
void configModeCallback(WiFiManager *myWiFiManager)
// É chamado no modo de configuração
{
Serial.println("Entrando no modo de configuração...");
Serial.print("Config SSID: ");
Serial.println(myWiFiManager->getConfigPortalSSID());
Serial.print("Config IP Address: ");
Serial.print(WiFi.softAPIP());
Serial.print("Config Port: ");
Serial.println(WIFIMANAGER_PORT);
}
//----------------------------------------
// Carrega os parâmetros do SPIFFS
//----------------------------------------
bool loadConfig()
{
Serial.println("Tentando carregar o Config do SPIFFS...");
if (!SPIFFS.begin(true))
{
Serial.println("Não foi possível incializar o SPIFFS...");
SPIFFS.format();
Serial.println("Sistema de Arquivo no SPIFFS foi formatado");
delay(100);
//return false;
}
//deleteFile(CONFIG_FILE); // para forçar os defaults durante o desenvolvimento
if (!SPIFFS.exists(CONFIG_FILE))
{
Serial.println("Config não existe. Adotando default...");
// Valores padrão se não existir o arquivo
appConfig.alias = DNS_NAME;
appConfig.queue_size = QUEUE_SIZE;
appConfig.ntpServer = NTP_SERVER;
appConfig.timezone = TIMEZONE_LOCAL;
appConfig.user_OTA = USER_OTA;
appConfig.pass_OTA = PASS_OTA;
appConfig.autoRebootOTA = AUTOREBOOT_OTA;
saveConfig();
return false;
}
File file = SPIFFS.open(CONFIG_FILE, FILE_READ);
if (!file) return false;
DynamicJsonDocument doc(1024);
DeserializationError err = deserializeJson(doc, file);
serializeJsonPretty(doc, Serial);
Serial.flush();
file.close();
if (err) return false;
appConfig.queue_size = doc["queueSize"] | QUEUE_SIZE;
appConfig.alias = doc["alias"] | DNS_NAME;
appConfig.ntpServer = doc["ntpServer"] | NTP_SERVER;
appConfig.timezone = doc["timezone"] | TIMEZONE_LOCAL;
appConfig.user_OTA = doc["user_OTA"] | USER_OTA;
appConfig.pass_OTA = doc["pass_OTA"] | PASS_OTA;
appConfig.autoRebootOTA = doc["autoRebootOTA"] | AUTOREBOOT_OTA;
return true;
}
//--------------------------------------
// Salva os parâmetros no SPIFFS
//--------------------------------------
bool saveConfig()
{
Serial.println("Salvando a configuração...");
DynamicJsonDocument doc(1024);
doc["queueSize"] = appConfig.queue_size;
doc["alias"] = appConfig.alias;
doc["ntpServer"] = appConfig.ntpServer;
doc["timezone"] = appConfig.timezone;
doc["user_OTA"] = appConfig.user_OTA;
doc["pass_OTA"] = appConfig.pass_OTA;
doc["autoRebootOTA"] = appConfig.autoRebootOTA;
File file = SPIFFS.open(CONFIG_FILE, FILE_WRITE);
if (!file) return false;
serializeJson(doc, file);
serializeJsonPretty(doc, Serial);
Serial.println();
Serial.flush();
file.close();
return true;
}
//--------------------------------------------------
// Rotina de Tratamento da Interrupção do Botão Boot
//--------------------------------------------------
void buttonISR()
{
buttonState = true;
}
//--------------------------------------------------
// Rotina de Cpmparação de MAC's
//--------------------------------------------------
static inline bool macEq(const uint8_t a[6], const uint8_t b[6])
{
for (int i=0;i<6;i++)
{
if (a[i]!=b[i]) return false;
}
return true;
}
//--------------------------------------------------
// Rotina para colocar o MAC no Cache
//--------------------------------------------------
static void rssiCachePut(const uint8_t mac[6], int8_t rssi)
{
// procura slot existente
int best = -1; uint32_t oldest = 0xFFFFFFFF;
for (int i=0;i<SNIFFER_SIZE;i++)
{
if (macEq(gRssiCache[i].mac, mac)) { best = i; break; }
if (gRssiCache[i].ts < oldest) { oldest = gRssiCache[i].ts; best = i; }
}
memcpy(gRssiCache[best].mac, mac, 6);
gRssiCache[best].rssi = rssi;
gRssiCache[best].ts = millis();
}
//--------------------------------------------------
// Rotina para pegar um MAC do Cache
//--------------------------------------------------
static bool rssiCacheGet(const uint8_t mac[6], int8_t &out)
{
for (int i=0;i<SNIFFER_SIZE;i++)
{
if (macEq(gRssiCache[i].mac, mac)) { out = gRssiCache[i].rssi; return true; }
}
return false;
}
//--------------------------------------------------
// Rotina para escutar os pacotes SNIFFER
//--------------------------------------------------
static void IRAM_ATTR snifferCb(void* buf, wifi_promiscuous_pkt_type_t type)
{
if (type != WIFI_PKT_MGMT && type != WIFI_PKT_DATA) return;
const wifi_promiscuous_pkt_t* ppkt = (const wifi_promiscuous_pkt_t*)buf;
if (!ppkt || !ppkt->payload) return;
// payload começa no cabeçalho MAC
const wifi_ieee80211_mac_hdr_t* hdr = (const wifi_ieee80211_mac_hdr_t*)ppkt->payload;
// RSSI em dBm
int8_t rssi = ppkt->rx_ctrl.rssi;
// addr2 é o MAC de quem TRANSMITIU (nó que nos interessa)
rssiCachePut(hdr->addr2, rssi);
}
//--------------------------------------------------
// Rotina para habilitar a escuta SNIFFER
//--------------------------------------------------
static void enableSniffer()
{
wifi_promiscuous_filter_t f{};
// ESPNOW usa quadros de gerenciamento (vendor action); mantenha DATA também
f.filter_mask = WIFI_PROMIS_FILTER_MASK_MGMT | WIFI_PROMIS_FILTER_MASK_DATA;
esp_wifi_set_promiscuous_filter(&f);
esp_wifi_set_promiscuous_rx_cb(&snifferCb);
esp_wifi_set_promiscuous(true);
Serial.println(F("[32] Sniffer promíscuo habilitado (MGMT+DATA)"));
}
//-----------------------------------------------------------------------------------------
// Função : Este programa tem como objetivo receber comandos para LIGAR/DESLIGAR o relé
// do módulo vindos pela Rede ESP-NOW. Adicionalmente, o programa se divulga
// para que a estação Central mantenha uma lista de todos os ESP8266 Relays
// e faça o gerenciamento de todos os módulos numa interface html central.
// Resumo: 1) Faz a auto divulgação vai broadcast na Rede ESP-NOW
// 2) Responde aos comandos de LIGAR/DESLIGAR/GETSTATUS
//-----------------------------------------------------------------------------------------
// Autor : Dailton Menezes
// Versão : 1.0 Out/2025
//-----------------------------------------------------------------------------------------
#include <ESP8266WiFi.h> // Biblioteca para a rede wifi genérica
#include <ESPAsyncTCP.h> // Biblioteca usada pelo Servidor Assíncrono
#include <ESPAsyncWebServer.h> // Biblioteca para Servidor Web Assíncrono
#include <espnow.h> // Biblioteca do Protocolo ESP-NOW
#include <FS.h> // Biblioteca para tratar arquivos
#include <ElegantOTA.h> // Biblioteca para atualização via Web
#include <Ticker.h> // Biblioteca para gerar timer
extern "C"
{
#include "user_interface.h"
}
//------------------------
// Definições do Programa
//------------------------
#define RELAY_PIN 0 // D1 (ajuste ao seu hardware)
#define DISC_MS 5000 // intervalo de divulgação
#define PMK_KEY "SuaPMKde16b" // opcional: 16 bytes (ex.: "1234567890ABCDEF")
#define RELE_ON LOW // Estado para o Relé Ativo
#define RELE_OFF HIGH // Estado para Relé desativado
#define BAUDRATE 115200 // Baudrate para a Console
#define CFG_PATH "/cfg.bin" // arquivo no SPIFFS
#define SSID_AP "NODE-RELAY-SETUP" // SSID para Modo AP/Config
#define PASS_AP "12345678" // Senha do Modo AP/Config
#define USER_OTA "admin" // Usuário para atualização OTA
#define PASS_OTA "admin" // Senha para atualização OTA
//-------------------
// Variáveis Globais
//-------------------
AsyncWebServer server(80); // Servidor Web para receber acessos http
volatile uint8_t gRelay = 0; // Estado do Relay
uint8_t gMyMac[6]; // MAC da Estação em bytes
uint8_t gChannel = 11; // Canal da Rede ESP-NOW
bool modo_config = false; // Se estar no modo de configuração ou não
Ticker rebootTimer; // Timer para gerar um atraso no reboot
//-----------------------------
// Config persistida
//-----------------------------
struct CfgNode
{
char ssid[33]; // SSID até 32 chars
char alias[24]; // alias curto
uint8_t magic; // 0xA5 = válido
};
CfgNode gCfg;
//--------------------
// Tipos de Mensagens
//--------------------
enum MsgType : uint8_t
{
MSG_DISC=1, // Brodacast de divulgação das estações para Central
MSG_CMD=2, // Comando ON/OFF do Relé
MSG_STATUS=3, // Status do Relé
MSG_GETALL=4, // Broadcast quem está aí a pedido da Central
MSG_SENSOR=5, // Status de estações cm DHT11
MSG_CFG=6 // Pedido da Central para a estação entrar em configuração
};
//-------------------------------
// Estrutura da Mensagem ESP-NOW
//-------------------------------
struct __attribute__((packed)) NowMsg
{
uint8_t ver, // Parar diferenciar registros entre outras aplicação
type, // Tipo de Mensagem
relay, // Estado do Relé
mode, // Modo Wifi da Estação
channel, // Canal da Rede ESP-NOW
aliasLen, // Tamanho do campo alias
rsv[2], // Para alinhamento
mac[6]; // MAC da estação
char alias[32]; // Alias da Estação (identificação)
int16_t tempC10; // temperatura em décimos de grau (ex.: 253 = 25.3°C)
uint8_t hum; // Umidade em %
};
//----------------------
// HTML de Configuração
//----------------------
const char* HTML_CFG = R"HTML(
<!doctype html><meta charset="utf-8">
<meta name=viewport content="width=device-width,initial-scale=1">
<title>Config Nó</title>
<style>
body{font-family:system-ui,Segoe UI,Arial;margin:16px;max-width:560px}
label{display:block;margin-top:10px;font-weight:600}
input,button{padding:10px;margin-top:6px;border:1px solid #ccc;border-radius:10px;font-size:16px}
input{width:100%}
.row{display:flex;gap:10px;flex-wrap:wrap;margin-top:14px}
.row button{flex:1 1 220px;cursor:pointer}
.hint{font-size:12px;color:#666;margin-top:4px}
</style>
<h3>Configuração do Nó</h3>
<label>SSID
<input id="ssid" maxlength="32"
placeholder="Ex.: MinhaRedeWiFi"
title="Informe o SSID (nome) da rede Wi-Fi da Estação Central">
</label>
<div class="hint">Nome exato da rede Wi-Fi onde está a Estação Central.</div>
<label>Alias
<input id="alias" maxlength="23"
placeholder="Ex.: Sala, Quarto, DHT-Cozinha"
title="Rótulo amigável para identificar este nó">
</label>
<div class="hint">Use um nome curto para aparecer nos cards (ex.: "Sala").</div>
<div class="row">
<button onclick="salvar()">Salvar & Reiniciar</button>
<button onclick="atualizar()">Atualizar Firmware</button>
</div>
<script>
async function salvar(){
const ssid = document.getElementById('ssid').value.trim();
const alias = document.getElementById('alias').value.trim();
if(!ssid){ alert('Informe o SSID.'); return; }
const body = JSON.stringify({ssid, alias});
try{
const r = await fetch('/save', {
method: 'POST',
headers: {'Content-Type':'application/json'},
body
});
// chegou resposta OK antes do reboot
if(!r.ok) throw new Error('Falha ao salvar');
document.body.innerHTML = '<p>Configurações salvas. Reiniciando o módulo…</p>';
// não precisa reload aqui; o AP sumirá e está tudo certo
}catch(e){
// Em caso raro de erro, apenas informa sem travar a UX
console.warn('Erro no fetch /save:', e);
document.body.innerHTML = '<p>Configurações foram enviadas. O módulo pode já estar reiniciando…</p>';
}
}
function atualizar(){
// abre a rota do ElegantOTA (você cuidará do handler /update)
location.href = '/update';
}
// pré-carrega valores atuais
fetch('/cfg')
.then(r=>r.json())
.then(j=>{
const ssid = document.getElementById('ssid');
const alias = document.getElementById('alias');
ssid.value = (j && j.ssid) ? j.ssid : '';
alias.value = (j && j.alias) ? j.alias : '';
ssid.focus();
})
.catch(()=>{});
</script>
)HTML";
//-------------------------
// Prototipação de Rotinas
//-------------------------
void onEspNowRecv(uint8_t *mac, uint8_t *data, uint8_t len);
void sendStatus(const uint8_t* toMac);
void sendDiscovery();
String macToStr(const uint8_t m[6]);
bool loadCfg();
bool saveCfg(const char* ssid, const char* alias);
uint8_t getWiFiChannelRetry(const char* ssid, uint8_t tentativas=3, uint16_t esperaMs=400);
void startConfigAP();
void startEspNowOnChannel(uint8_t ch);
void enterConfigByEraseAndReset();
//---------------------------
// Inicialização do Programa
//---------------------------
void setup()
{
// Inicializa a Serial
Serial.begin(BAUDRATE);
delay(1000);
Serial.println(F("ESP01S Relay V1.0 Out/2025"));
Serial.flush();
// Inicializa o Relé
pinMode(RELAY_PIN, OUTPUT);
digitalWrite(RELAY_PIN, RELE_OFF);
bool temCfg = loadCfg();
if (!temCfg)
{
// 1ª vez: ir direto ao modo de configuração
startConfigAP();
return;
}
// Temos SSID salvo → descobrir canal
if (strlen(gCfg.ssid) > 0)
{
uint8_t ch = getWiFiChannelRetry(gCfg.ssid, 4, 500);
if (ch != 0)
{
startEspNowOnChannel(ch); // modo normal
return;
}
}
Serial.printf("Falhou na obtenção do canal para SSID: %s\n",gCfg.ssid);
// SSID vazio ou não encontrado → entra em config
startConfigAP();
}
//----------------------------
// Loop Prinicpal do Programa
//----------------------------
void loop()
{
// Verifica se está no modo config para permitir a atualização OTA
if (modo_config)
{
ElegantOTA.loop();
}
else
{
// Modo ESP_NOW mantendo a comunicação
static uint32_t t0=0;
uint32_t now = millis();
if (now - t0 >= DISC_MS)
{
t0 = now;
sendDiscovery();
}
}
}
//------------------------------------------------
// Rotina para tratar a mensagem ESP-NOW recebida
//------------------------------------------------
void onEspNowRecv(uint8_t *mac, uint8_t *data, uint8_t len)
{
// Log de entrada com MAC formatado corretamente
Serial.printf("Mensagem ESP-NOW RX: mac=%s len=%u\n",macToStr(mac).c_str(),len);
if (len < (sizeof(NowMsg) - 32)) {
Serial.println("RX descartado: len insuficiente para NowMsg base");
return;
}
NowMsg msg;
memcpy(&msg, data, min((int)len, (int)sizeof(NowMsg)));
Serial.printf("Decodificado: Ver=%u Type=%u Relay=%u Ch=%u AliasLen=%u\n",
msg.ver, msg.type, msg.relay, msg.channel, msg.aliasLen);
if (msg.ver != 1) {
Serial.println(F("Versão inválida, desprezado"));
return;
}
if (msg.type == MSG_CFG && msg.relay == 1)
{
Serial.println(F("[NOW] Recebido CMD CONFIG -> apagar cfg e reiniciar"));
enterConfigByEraseAndReset();
return;
}
if (msg.type == MSG_CMD)
{
if (msg.relay == 0)
{
digitalWrite(RELAY_PIN, RELE_OFF); gRelay = 0;
}
else if (msg.relay == 1)
{
digitalWrite(RELAY_PIN, RELE_ON); gRelay = 1;
}
// msg.relay==2 => GET status (não muda)
sendStatus(mac); // responde STATUS ao remetente
}
}
//-----------------------------------------------------
// Rotina para enviar o status atual pela Rede ESP-NOW
//-----------------------------------------------------
void sendStatus(const uint8_t* toMac)
{
NowMsg s{};
s.ver=1;
s.type=MSG_STATUS;
s.relay=gRelay;
s.mode=(WiFi.getMode()==WIFI_STA)?0: (WiFi.getMode()==WIFI_AP)?1:2;
s.channel = wifi_get_channel();
s.aliasLen = strlcpy(s.alias, gCfg.alias, sizeof(s.alias));
memcpy(s.mac, gMyMac, 6);
// int rc = esp_now_send((uint8_t*)toMac, (uint8_t*)&s, sizeof(NowMsg));
uint8_t bcast[6];
memset(bcast, 0xFF, 6);
int rc = esp_now_send(bcast, (uint8_t*)&s, sizeof(NowMsg));
Serial.printf("SendStatus enviado. Rc=%d\n",rc);
}
//------------------------------------------------------
// Rotina para enviar self divulgação pela Rede ESP-NOW
//------------------------------------------------------
void sendDiscovery()
{
NowMsg d{};
d.ver=1;
d.type=MSG_DISC;
d.relay=gRelay;
d.mode=(WiFi.getMode()==WIFI_STA)?0: (WiFi.getMode()==WIFI_AP)?1:2;
d.channel = wifi_get_channel();
d.aliasLen = strlcpy(d.alias, gCfg.alias, sizeof(d.alias));
memcpy(d.mac, gMyMac, 6);
uint8_t bcast[6];
memset(bcast, 0xFF, 6);
int rc = esp_now_send(bcast, (uint8_t*)&d, sizeof(NowMsg));
Serial.printf("Discovery enviado. Rc=%d\n",rc);
}
//---------------------------------------------
// Rotina para detectar o canal utilizado pela
// Rede Wi-fi cujo o SSID foi definido com
// retry e n. de tentativas
//---------------------------------------------
uint8_t getWiFiChannelRetry(const char* ssid, uint8_t tentativas, uint16_t esperaMs)
{
for (uint8_t t=0; t<tentativas; ++t) {
int n = WiFi.scanNetworks(/*async=*/false, /*hidden=*/true);
for (int i=0; i<n; ++i)
{
String s = WiFi.SSID(i);
if (s.length() && strcmp(s.c_str(), ssid)==0) {
return (uint8_t)WiFi.channel(i);
}
}
delay(esperaMs);
}
return 0;
}
//--------------------------------------
// Convertet o MAC de bytes para String
//--------------------------------------
String macToStr(const uint8_t m[6])
{
char b[18];
sprintf(b, "%02X:%02X:%02X:%02X:%02X:%02X", m[0],m[1],m[2],m[3],m[4],m[5]);
return String(b);
}
//--------------------------------------
// Recupera os parâmetros do Config
//--------------------------------------
bool loadCfg()
{
if (!SPIFFS.begin()) return false;
if (!SPIFFS.exists(CFG_PATH)) return false;
File f = SPIFFS.open(CFG_PATH, "r");
if (!f) return false;
if (f.size() != sizeof(CfgNode)) { f.close(); return false; }
f.readBytes((char*)&gCfg, sizeof(CfgNode));
f.close();
return (gCfg.magic == 0xA5);
}
//--------------------------------------
// Persiste os parâmetros no Config
//--------------------------------------
bool saveCfg(const char* ssid, const char* alias)
{
strncpy(gCfg.ssid, ssid ? ssid : "", sizeof(gCfg.ssid));
strncpy(gCfg.alias, alias ? alias : "", sizeof(gCfg.alias));
gCfg.ssid[sizeof(gCfg.ssid)-1] = 0;
gCfg.alias[sizeof(gCfg.alias)-1] = 0;
gCfg.magic = 0xA5;
if (!SPIFFS.begin()) SPIFFS.begin();
File f = SPIFFS.open(CFG_PATH, "w");
if (!f) return false;
size_t wr = f.write((const uint8_t*)&gCfg, sizeof(CfgNode));
f.close();
return (wr == sizeof(CfgNode));
}
//-------------------------------
// Entra no Modo de Configuração
//-------------------------------
void startConfigAP()
{
modo_config = true;
WiFi.mode(WIFI_AP);
wifi_set_sleep_type(NONE_SLEEP_T);
// Nome da rede AP temporária
char ssid[32];
snprintf(ssid, sizeof(ssid), "ESP_Relay_%06X", ESP.getChipId());
WiFi.softAP(ssid, PASS_AP, 6);
server.on("/", HTTP_GET, [](AsyncWebServerRequest *r){
r->send(200,"text/html", HTML_CFG);
});
server.on("/cfg", HTTP_GET, [](AsyncWebServerRequest *r){
String s = "{\"ssid\":\""+String(gCfg.magic==0xA5? gCfg.ssid:"")+"\",\"alias\":\""+String(gCfg.magic==0xA5? gCfg.alias:"")+"\"}";
r->send(200,"application/json", s);
});
server.on("/save", HTTP_POST,
[](AsyncWebServerRequest *r){},
nullptr,
[](AsyncWebServerRequest *r, uint8_t *data, size_t len, size_t, size_t){
data[len]=0;
String body((char*)data);
// parse simples
auto extrai = [&](const char* key)->String{
String k = String("\"")+key+"\":\"";
int p = body.indexOf(k); if (p<0) return "";
p += k.length();
int q = body.indexOf('"', p); if (q<0) return "";
return body.substring(p, q);
};
String ssid = extrai("ssid");
String alias = extrai("alias");
bool ok = saveCfg(ssid.c_str(), alias.c_str());
// 1) responde já!
auto* resp = r->beginResponse(200, "application/json", "{\"ok\":true}");
resp->addHeader("Connection", "close");
r->send(resp);
// 2) agenda o reboot um pouco depois (sem usar delay)
rebootTimer.once_ms(1500, [](){
ESP.restart();
});
}
);
server.begin();
Serial.printf("[CFG] AP iniciado em %s/%s canal 6\n",SSID_AP,PASS_AP);
ElegantOTA.begin(&server, USER_OTA, PASS_OTA);
ElegantOTA.setAutoReboot(true);
}
//---------------------------------------
// Entra no Modo de Comnunicação ESP-NOW
//---------------------------------------
void startEspNowOnChannel(uint8_t ch)
{
modo_config = false;
WiFi.mode(WIFI_STA);
wifi_set_sleep_type(NONE_SLEEP_T);
// Fixar canal sem associar ao AP:
wifi_promiscuous_enable(1);
wifi_set_channel(ch);
wifi_promiscuous_enable(0);
if (esp_now_init()!=0) {
Serial.println(F("[NOW] falha init")); return;
}
esp_now_set_self_role(ESP_NOW_ROLE_COMBO);
esp_now_register_recv_cb(onEspNowRecv);
// peer broadcast
uint8_t b[6]; memset(b,0xFF,6);
esp_now_add_peer(b, ESP_NOW_ROLE_COMBO, 0 /* canal ignorado */, NULL, 0);
Serial.printf("[NOW] iniciado no canal %u, SSID=%s, alias=%s\n", ch, gCfg.ssid, gCfg.alias);
// TODO: registrar callbacks RX/TX e seguir com sua lógica atual…
}
//-----------------------------------------
// Força a entrada no modo de Configuração
//-----------------------------------------
void enterConfigByEraseAndReset()
{
// encerra ESPNOW rapidamente para evitar lixo no rádio durante reboot
esp_now_deinit();
if (SPIFFS.begin()) {
if (SPIFFS.exists(CFG_PATH)) {
SPIFFS.remove(CFG_PATH);
Serial.println(F("[CFG] Arquivo de configuração removido."));
} else {
Serial.println(F("[CFG] Arquivo de configuração não existe."));
}
} else {
Serial.println(F("[CFG] SPIFFS.begin() falhou (seguindo com reset)."));
}
delay(150);
ESP.reset(); // reinicia; no boot cairá no modo AP /config (pois não há arquivo)
}
//-----------------------------------------------------------------------------------------
// Função : Este programa tem como objetivo coletar a temperatura e a umidade do sensor
// dht11 integrado com o adaptador ESP01S e enviar via broadcast na rede espnow
// para estação central tratar e atualizar a interface html
//-----------------------------------------------------------------------------------------
// Autor : Dailton Menezes
// Versão : 1.0 Out/2025
//-----------------------------------------------------------------------------------------
#include <ESP8266WiFi.h> // Biblioteca para a rede wifi genérica
#include <ESPAsyncTCP.h> // Biblioteca usada pelo Servidor Assíncrono
#include <ESPAsyncWebServer.h> // Biblioteca para Servidor Web Assíncrono
#include <espnow.h> // Biblioteca do Protocolo ESP-NOW
#include <FS.h> // Biblioteca para tratar arquivos
#include <ElegantOTA.h> // Biblioteca para atualização via Web
#include <Ticker.h> // Biblioteca para gerar timer
#include "DHT.h"
extern "C"
{
#include "user_interface.h"
}
//------------------------
// Definições do Programa
//------------------------
#define ENVIO_MS 5000 // intervalo de divulgação
#define PMK_KEY "SuaPMKde16b" // opcional: 16 bytes (ex.: "1234567890ABCDEF")
#define BAUDRATE 115200 // Baudrate para a Console
#define CFG_PATH "/cfg.bin" // arquivo no SPIFFS
#define DHTPIN 2 // GPIO2 do ESP-01S
#define DHTTYPE DHT11 // Definição DHT11
#define SSID_AP "NODE-DHT11-SETUP" // SSID para Modo AP/Config
#define PASS_AP "12345678" // Senha do Modo AP/Config
#define USER_OTA "admin" // Usuário para atualização OTA
#define PASS_OTA "admin" // Senha para atualização OTA
//-------------------
// Variáveis Globais
//-------------------
AsyncWebServer server(80); // Servidor Web para receber acessos http
DHT dht(DHTPIN, DHTTYPE); // Instância do DHT11
uint8_t gMyMac[6]; // MAC da Estação em bytes
uint8_t gChannel = 11; // Canal da Rede ESP-NOW
uint32_t nextTx; // Momento do próximo Envio
bool modo_config = false; // Se estar no modo de configuração ou não
Ticker rebootTimer; // Timer para gerar um atraso no reboot
//-----------------------------
// Config persistida
//-----------------------------
struct CfgNode
{
char ssid[33]; // SSID até 32 chars
char alias[24]; // alias curto
uint8_t magic; // 0xA5 = válido
};
CfgNode gCfg;
//--------------------
// Tipos de Mensagens
//--------------------
enum MsgType : uint8_t
{
MSG_DISC=1, // Brodacast de divulgação das estações para Central
MSG_CMD=2, // Comando ON/OFF do Relé
MSG_STATUS=3, // Status do Relé
MSG_GETALL=4, // Broadcast quem está aí a pedido da Central
MSG_SENSOR=5, // Status de estações cm DHT11
MSG_CFG=6 // Pedido da Central para a estação entrar em configuração
};
//-------------------------------
// Estrutura da Mensagem ESP-NOW
//-------------------------------
struct __attribute__((packed)) NowMsg
{
uint8_t ver, // Parar diferenciar registros entre outras aplicação
type, // Tipo de Mensagem
relay, // Estado do Relé
mode, // Modo Wifi da Estação
channel, // Canal da Rede ESP-NOW
aliasLen, // Tamanho do campo alias
rsv[2], // Para alinhamento
mac[6]; // MAC da estação
char alias[32]; // Alias da Estação (identificação)
int16_t tempC10; // temperatura em décimos de grau (ex.: 253 = 25.3°C)
uint8_t hum; // Umidade em %
};
//----------------------
// HTML de Configuração
//----------------------
const char* HTML_CFG = R"HTML(
<!doctype html><meta charset="utf-8">
<meta name=viewport content="width=device-width,initial-scale=1">
<title>Config Nó</title>
<style>
body{font-family:system-ui,Segoe UI,Arial;margin:16px;max-width:560px}
label{display:block;margin-top:10px;font-weight:600}
input,button{padding:10px;margin-top:6px;border:1px solid #ccc;border-radius:10px;font-size:16px}
input{width:100%}
.row{display:flex;gap:10px;flex-wrap:wrap;margin-top:14px}
.row button{flex:1 1 220px;cursor:pointer}
.hint{font-size:12px;color:#666;margin-top:4px}
</style>
<h3>Configuração do Nó</h3>
<label>SSID
<input id="ssid" maxlength="32"
placeholder="Ex.: MinhaRedeWiFi"
title="Informe o SSID (nome) da rede Wi-Fi da Estação Central">
</label>
<div class="hint">Nome exato da rede Wi-Fi onde está a Estação Central.</div>
<label>Alias
<input id="alias" maxlength="23"
placeholder="Ex.: Sala, Quarto, DHT-Cozinha"
title="Rótulo amigável para identificar este nó">
</label>
<div class="hint">Use um nome curto para aparecer nos cards (ex.: "Sala").</div>
<div class="row">
<button onclick="salvar()">Salvar & Reiniciar</button>
<button onclick="atualizar()">Atualizar Firmware</button>
</div>
<script>
async function salvar(){
const ssid = document.getElementById('ssid').value.trim();
const alias = document.getElementById('alias').value.trim();
if(!ssid){ alert('Informe o SSID.'); return; }
const body = JSON.stringify({ssid, alias});
try{
const r = await fetch('/save', {
method: 'POST',
headers: {'Content-Type':'application/json'},
body
});
// chegou resposta OK antes do reboot
if(!r.ok) throw new Error('Falha ao salvar');
document.body.innerHTML = '<p>Configurações salvas. Reiniciando o módulo…</p>';
// não precisa reload aqui; o AP sumirá e está tudo certo
}catch(e){
// Em caso raro de erro, apenas informa sem travar a UX
console.warn('Erro no fetch /save:', e);
document.body.innerHTML = '<p>Configurações foram enviadas. O módulo pode já estar reiniciando…</p>';
}
}
function atualizar(){
// abre a rota do ElegantOTA (você cuidará do handler /update)
location.href = '/update';
}
// pré-carrega valores atuais
fetch('/cfg')
.then(r=>r.json())
.then(j=>{
const ssid = document.getElementById('ssid');
const alias = document.getElementById('alias');
ssid.value = (j && j.ssid) ? j.ssid : '';
alias.value = (j && j.alias) ? j.alias : '';
ssid.focus();
})
.catch(()=>{});
</script>
)HTML";
//-------------------------
// Prototipação de Rotinas
//-------------------------
String macToStr(const uint8_t m[6]);
float safeFloat(float valor);
bool loadCfg();
bool saveCfg(const char* ssid, const char* alias);
uint8_t getWiFiChannelRetry(const char* ssid, uint8_t tentativas=3, uint16_t esperaMs=400);
void startConfigAP();
void startEspNowOnChannel(uint8_t ch);
void onEspNowRecv(uint8_t *mac, uint8_t *data, uint8_t len);
void enterConfigByEraseAndReset();
//---------------------------
// Inicialização do Programa
//---------------------------
void setup()
{
// Inicializa a Serial
Serial.begin(115200);
delay(1000);
Serial.println(F("ESP01S DHT11 V1.0 Out/2025"));
Serial.flush();
// Inicializa o DHT11
dht.begin();
bool temCfg = loadCfg();
if (!temCfg)
{
// 1ª vez: ir direto ao modo de configuração
startConfigAP();
return;
}
// Temos SSID salvo → descobrir canal
if (strlen(gCfg.ssid) > 0)
{
uint8_t ch = getWiFiChannelRetry(gCfg.ssid, 4, 500);
if (ch != 0)
{
startEspNowOnChannel(ch); // modo normal
return;
}
}
// SSID vazio ou não encontrado → entra em config
startConfigAP();
// Inicializa o intervalo de envio
nextTx = millis() + random(0, 1500);
}
//----------------------------
// Loop Prinicpal do Programa
//----------------------------
void loop()
{
// Verifica se está no modo config para permitir a atualização OTA
if (modo_config)
{
ElegantOTA.loop();
}
else
{
// Modo ESP_NOW mantendo a comunicação
if ((int32_t)(millis() - nextTx) >= 0)
{
float t = safeFloat(dht.readTemperature()); // °C
float h = safeFloat(dht.readHumidity()); // %
NowMsg m{}; m.ver=1; m.type=MSG_SENSOR;
m.mode=0; m.channel=wifi_get_channel();
uint8_t mac[6]; wifi_get_macaddr(STATION_IF, mac); memcpy(m.mac, mac, 6);
strlcpy(m.alias, gCfg.alias, sizeof(m.alias)); m.aliasLen = strlen(m.alias);
m.tempC10 = (int16_t)lroundf(t * 10.0f);
m.hum = (uint8_t)lroundf(h);
uint8_t br[6]; memset(br, 0xFF, 6);
int rc= esp_now_send(br, (uint8_t*)&m, sizeof(NowMsg)); // broadcast
Serial.printf("Dados DHT11 enviados. Rc=%d\n",rc);
nextTx += ENVIO_MS + random(-300, 300); // 5 s ± jitter
}
}
}
//---------------------------------------------
// Rotina para detectar o canal utilizado pela
// Rede Wi-fi cujo o SSID foi definido com
// retry e n. de tentativas
//---------------------------------------------
uint8_t getWiFiChannelRetry(const char* ssid, uint8_t tentativas, uint16_t esperaMs)
{
for (uint8_t t=0; t<tentativas; ++t) {
int n = WiFi.scanNetworks(/*async=*/false, /*hidden=*/true);
for (int i=0; i<n; ++i)
{
String s = WiFi.SSID(i);
if (s.length() && strcmp(s.c_str(), ssid)==0) {
return (uint8_t)WiFi.channel(i);
}
}
delay(esperaMs);
}
return 0;
}
//--------------------------------------
// Convertet o MAC de bytes para String
//--------------------------------------
String macToStr(const uint8_t m[6])
{
char b[18];
sprintf(b, "%02X:%02X:%02X:%02X:%02X:%02X", m[0],m[1],m[2],m[3],m[4],m[5]);
return String(b);
}
//-------------------------------
// Retorna um valor Float seguro
//-------------------------------
float safeFloat(float valor)
{
return !isnan(valor) ? valor : 0.00;
}
//--------------------------------------
// Recupera os parâmetros do Config
//--------------------------------------
bool loadCfg()
{
if (!SPIFFS.begin()) return false;
if (!SPIFFS.exists(CFG_PATH)) return false;
File f = SPIFFS.open(CFG_PATH, "r");
if (!f) return false;
if (f.size() != sizeof(CfgNode)) { f.close(); return false; }
f.readBytes((char*)&gCfg, sizeof(CfgNode));
f.close();
return (gCfg.magic == 0xA5);
}
//--------------------------------------
// Persiste os parâmetros no Config
//--------------------------------------
bool saveCfg(const char* ssid, const char* alias)
{
strncpy(gCfg.ssid, ssid ? ssid : "", sizeof(gCfg.ssid));
strncpy(gCfg.alias, alias ? alias : "", sizeof(gCfg.alias));
gCfg.ssid[sizeof(gCfg.ssid)-1] = 0;
gCfg.alias[sizeof(gCfg.alias)-1] = 0;
gCfg.magic = 0xA5;
if (!SPIFFS.begin()) SPIFFS.begin();
File f = SPIFFS.open(CFG_PATH, "w");
if (!f) return false;
size_t wr = f.write((const uint8_t*)&gCfg, sizeof(CfgNode));
f.close();
return (wr == sizeof(CfgNode));
}
//-------------------------------
// Entra no Modo de Configuração
//-------------------------------
void startConfigAP()
{
modo_config = true;
WiFi.mode(WIFI_AP);
wifi_set_sleep_type(NONE_SLEEP_T);
// Nome da rede AP temporária
char ssid[32];
snprintf(ssid, sizeof(ssid), "ESP_DHT11_%06X", ESP.getChipId());
WiFi.softAP(ssid, PASS_AP, 6);
server.on("/", HTTP_GET, [](AsyncWebServerRequest *r){
r->send(200,"text/html", HTML_CFG);
});
server.on("/cfg", HTTP_GET, [](AsyncWebServerRequest *r){
String s = "{\"ssid\":\""+String(gCfg.magic==0xA5? gCfg.ssid:"")+"\",\"alias\":\""+String(gCfg.magic==0xA5? gCfg.alias:"")+"\"}";
r->send(200,"application/json", s);
});
server.on("/save", HTTP_POST,
[](AsyncWebServerRequest *r){},
nullptr,
[](AsyncWebServerRequest *r, uint8_t *data, size_t len, size_t, size_t){
data[len]=0;
String body((char*)data);
// parse simples
auto extrai = [&](const char* key)->String{
String k = String("\"")+key+"\":\"";
int p = body.indexOf(k); if (p<0) return "";
p += k.length();
int q = body.indexOf('"', p); if (q<0) return "";
return body.substring(p, q);
};
String ssid = extrai("ssid");
String alias = extrai("alias");
bool ok = saveCfg(ssid.c_str(), alias.c_str());
// 1) responde já!
auto* resp = r->beginResponse(200, "application/json", "{\"ok\":true}");
resp->addHeader("Connection", "close");
r->send(resp);
// 2) agenda o reboot um pouco depois (sem usar delay)
rebootTimer.once_ms(1500, [](){
ESP.restart();
});
}
);
server.begin();
Serial.printf("[CFG] AP iniciado em %s/%s canal 6\n",SSID_AP,PASS_AP);
ElegantOTA.begin(&server, USER_OTA, PASS_OTA);
ElegantOTA.setAutoReboot(true);
}
//---------------------------------------
// Entra no Modo de Comnunicação ESP-NOW
//---------------------------------------
void startEspNowOnChannel(uint8_t ch)
{
modo_config = false;
WiFi.mode(WIFI_STA);
wifi_set_sleep_type(NONE_SLEEP_T);
// Fixar canal sem associar ao AP:
wifi_promiscuous_enable(1);
wifi_set_channel(ch);
wifi_promiscuous_enable(0);
if (esp_now_init()!=0) {
Serial.println(F("[NOW] falha init")); return;
}
esp_now_set_self_role(ESP_NOW_ROLE_COMBO);
esp_now_register_recv_cb(onEspNowRecv);
// peer broadcast
uint8_t b[6]; memset(b,0xFF,6);
esp_now_add_peer(b, ESP_NOW_ROLE_COMBO, 0 /* canal ignorado */, NULL, 0);
Serial.printf("[NOW] iniciado no canal %u, SSID=%s, alias=%s\n", ch, gCfg.ssid, gCfg.alias);
// TODO: registrar callbacks RX/TX e seguir com sua lógica atual…
}
//------------------------------------------------
// Rotina para tratar a mensagem ESP-NOW recebida
//------------------------------------------------
void onEspNowRecv(uint8_t *mac, uint8_t *data, uint8_t len)
{
// Log de entrada com MAC formatado corretamente
Serial.printf("Mensagem ESP-NOW RX: mac=%s len=%u\n",macToStr(mac).c_str(),len);
if (len < (sizeof(NowMsg) - 32)) {
Serial.println("RX descartado: len insuficiente para NowMsg base");
return;
}
NowMsg msg;
memcpy(&msg, data, min((int)len, (int)sizeof(NowMsg)));
Serial.printf("Decodificado: Ver=%u Type=%u Relay=%u Ch=%u AliasLen=%u\n",
msg.ver, msg.type, msg.relay, msg.channel, msg.aliasLen);
if (msg.ver != 1) {
Serial.println(F("Versão inválida, desprezado"));
return;
}
if (msg.type == MSG_CFG && msg.relay == 1)
{
Serial.println(F("[NOW] Recebido CMD CONFIG -> apagar cfg e reiniciar"));
enterConfigByEraseAndReset();
return;
}
}
//-----------------------------------------
// Força a entrada no modo de Configuração
//-----------------------------------------
void enterConfigByEraseAndReset()
{
// encerra ESPNOW rapidamente para evitar lixo no rádio durante reboot
esp_now_deinit();
if (SPIFFS.begin()) {
if (SPIFFS.exists(CFG_PATH)) {
SPIFFS.remove(CFG_PATH);
Serial.println(F("[CFG] Arquivo de configuração removido."));
} else {
Serial.println(F("[CFG] Arquivo de configuração não existe."));
}
} else {
Serial.println(F("[CFG] SPIFFS.begin() falhou (seguindo com reset)."));
}
delay(150);
ESP.reset(); // reinicia; no boot cairá no modo AP /config (pois não há arquivo)
}
O projeto buscou demonstrar que, mesmo com os recursos limitados do ESP-01S, é possível construir uma rede inteligente de automação utilizando o protocolo ESP-NOW como base de comunicação.
Com a combinação de módulos equipados com Relé e Sensor DHT11, e uma Estação Central ESP32 atuando como ponto de controle e visualização, obtivemos uma solução estável, responsiva e de fácil expansão.
Além do caráter técnico, o trabalho reforça o potencial educacional e experimental do ecossistema ESP, permitindo que estudantes e profissionais explorem conceitos como comunicação distribuída, configuração dinâmica, atualização OTA e gerenciamento centralizado de dispositivos IoT.
A arquitetura resultante é versátil, podendo evoluir para aplicações reais de automação residencial, monitoramento ambiental ou controle industrial leve, mantendo o equilíbrio entre simplicidade, desempenho e custo reduzido — características que fazem do ESP8266 e do ESP32 uma dupla ainda imbatível no mundo maker.
|
|
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!