Automação Residencial

Sistema de Monitoramento de CPD

Eletrogate 28 de novembro de 2023

Introdução

CPD é um termo em inglês antigo e quase fora de uso, que significa Departamento de Processamento de Dados Central. Na prática, esse termo se refere a uma sala onde os servidores de uma organização ficam instalados e em operação. Geralmente possuem pouco acesso de pessoas e com esquema de segurança no acesso. Possuem um sistema de refrigeração ou condicionamento que às vezes pode falhar, elevando a temperatura e podendo gerar risco para os processadores dos servidores e/ou gerar interrupção nos sistemas críticos da organização. Daí surgiu a ideia para desenvolvimento deste sistema de monitoramento, que é teórico, mas que poderia ser ampliado para virar um produto e ser usado no mundo real. De qualquer forma, ele tem o objetivo de servir como base para projetos mais abrangentes.


Bibliotecas

ESPAsyncWebServer
AsyncTCP
ESP32Ping
WiFiManager
DHT, FS, SPIFFS, Time, ArduinoJson, LiquidCrystal_I2C (Instalados através do Gerenciador de Bibliotecas)


Descrição

Os objetivos específicos deste projeto são:

  1. Usar o SPIFFS (sistema de arquivo na FLASH do ESP32) para coletar o SSID/Senha/CPDID/Intervalo do Timer e persistir. Como HTTP Server, os seguintes modos foram implementados:
    •  modo AP usado para configurar o SSID e senha da rede a ser usada, CPDID e o Intervalo na porta 8080. Este modo é acionado pressionando-se o PUSH BUTTON interno do ESP32 (BOOT) durante a operação (não o botão RESET). Quando neste modo, o operador deve procurar uma rede com ssid CPD_AP_nnnnn através de um celular ou computador e deve se conectar usando a senha “password”. Depois de conectado, o operador deve acessar o endereço 192.168.4.1:8080 através do navegador, uma página será mostrada e clicar o botão Config Wifi, depois escolher o ssid, informar a senha e definir os parâmetros cpdID (até 50 caracteres) e intervaloTimer (deve ser maior que 60 segundos, até 4 dígitos). Por último pressionar o botão SAVE na interface. Os parâmetros serão salvos na FLASH do ESP32 para não perder quando desligado. A partir daí, o modo AP é encerrado e o modo normal é acionado. (Figura 8 a Figura 11)
    • modo Normal é o estado conectado e respondendo às requisições na porta 80 e o monitoramento a cada intervalo do timer definido e esperando 6 blocos de funções (Bloco 1 – Reset do WatchDog; Bloco 2 – Se expirou o timer para verificação dos sensores; Bloco 3 – Verificação se o PUSH BUTTON do ESP32 para configuração do WiFi; Bloco 4 – Verificação dos sensores de Fogo e Fumaça; Bloco 5 – Verificação do sensor de presença; Bloco 6 – Limpeza de conexões perdidas no servidor http para liberar memória). A partir deste ponto, o usuário pode acessar via navegador no IP, mostrado no monitor serial no startup, na porta 81, ou usar programas como FING para celular que permitem identificar o IP na rede. O ESP32 aparece com a identificação “Espressif” em tais programas. Uma alternativa, caso o usuário tenha um servidor DNS próprio em sua rede, seria criar uma entrada no DNS usando o CPDID e o IP, que pode ser uma tarefa mais complexa para a maioria dos usuários. O app FING é uma boa alternativa para descobrir o IP pela rede (Figura 3). A seguir, alguns exemplos já conhecendo o IP do ESP32:
  2. Programar o TIMER 0 de HW para cada 1 min (ou o valor definido no modo AP) para monitorar a Temperatura, Umidade, Luminosidade e checar o ping para saber se a internet está disponível e não apenas conectada ao AP:
    • O DHT22 informa a Temperatura e umidade.
    • O LDR informa a luminosidade. Como usa porta analógica, deve ser ligado numa porta ADC1 para uso concomitante com o Wifi no ESP32: 32, 33, 34, 35, 36, 37, 38 ou 39.
    • LED RGB para mostrar a cor correspondente da temperatura como alternativa à visualização do Display. Isso pode facilitar a visualização de longe, sem a necessidade de entrar na sala do CPD para ver o Display. Naturalmente, deve existir uma janela ou porta com a possibilidade de se ver de fora da sala.
    • Atualizar o LEDBUILTIN (LED AZUL da placa ligado ou apagado) caso não haja internet disponível.
  3. Esperar conexão http na porta 80 respondendo as URI’s a seguir:
    • / ➔ devolve no formato HTML os valores dos indicadores numa página AUTOREFRESH (a cada intervalo);
    • /sensores ➔ devolve os indicadores no formato JSON (Local, Temp, Umid, Lumin, Fogo, Gás, TimeStamp);
    • /info ➔ devolve as informações do server no formato JSON (Local, IP, MAC, StartupTime);
    • /acessos ➔ devolve os Top 10 acessos.
  4. O LED BUILTIN ficará acesso caso haja a conexão wifi e apagado quando a conexão acabar. Caso uma desconexão aconteça, uma reconexão será tentada posteriormente.
  5. Acender uma alerta no LED RGB com a cor relativa ao valor da temperatura:
    • Temp < 10 | azul-claro ➔ gelado;
    • 10 ≤ Temp < 20 | azul-escuro ➔ frio;
    • 20 ≤ Temp < 25 verde ➔ normal;
    • 25 ≤ Temp < 30 vermelho fraco ➔ quente;
    • Temp ≥ 30 vermelho forte ➔ muito quente.
  6. Mostrar a Temperatura em °C, (U)midade em %, (L)uminosidade em % e Data/Hora da última varredura num Display LCD 16×2 (Figura 2).
  7. Programar o TIMER 1 de HW para cada 1 min (ou o valor definido no modo AP) para implementação de WatchDog caso haja travamento no loop principal. Isso causa o reset do ESP32 para evitar paralisação do monitoramento até que alguma ação corretiva presencial seja feita. Normalmente, os equipamentos de monitoramentos ficam em lugares remotos e, muitas vezes, de difícil acesso. Qualquer travamento impede a função de monitoramento, e as emergências podem ficar em descoberto. O recurso de WatchDog permite gerar um reboot automático assim que um travamento for identificado. Isso permite a volta rápida do monitoramento, mas é uma solução paliativa até a solução definitiva da causa raiz do travamento.
  8. Monitorar os sensores de Fogo, Gás/Fumaça e de Presença. É considerado emergência quando FOGO ou GÁS é detectado e um sinal sonoro é emitido enquanto os sensores informam o sinal positivo para o evento. O sensor de presença é usado para registrar/persistir uma lista com as Top 10 ocorrências de detecção. Um intervalo de 5 min é considerado para aceitar uma nova detecção de presença. Isso para não registrar ocorrências muito próximas, já que apenas 10 detecções são armazenadas. Uma atenção especial deve ser feita para o sensor de gás/fumaça pois a tensão de saída é de 5V. Foi necessário fazer a redução de tensão para não comprometer a porta do ESP32 que opera em 3,3V. Dois resistores de 22k e 33k foram usados (veja os detalhes na Figura 2 e as opções para sensores de fumaça na Figura 12). Quanto ao sensor de presença, apesar de ser alimentado com 5V, a tensão de saída é menor do que o limite de 3,3V da porta do ESP32, sem preocupação.
Blog-Eletrogate-Diagrama-do-Circuito

Diagrama do Circuito

Blog-Eletrogate-Circuito-em-Bancada

Circuito em Bancada

Blog-Eletrogate-App-Fing

App Fing para descobrir o IP obtido na Rede

Blog-Eletrogate-Pagina-Principal

Página Principal – /

Blog-Eletrogate-Dados-dos-sensores

Dados dos Sensores – /sensores

Blog-Eletrogate-informacoes-do-servidor

Informações do Servidor – /info

Blog-Eletrogate-Top-10-acessos

Top 10 Acessos

Blog-Eletrogate-selecao-do-equipamento

Seleção do equipamento no Modo AP

Blog-Eletrogate-Conectado-ao-equipamento

Conectado ao equipamento no Modo AP

Blog-Eletrogate-Tela-Principal-da-configuracao

Tela Principal da Configuração

Blog-Eletrogate-tela-para-definicao

Tela para Definição das Configurações

Blog-Eletrogate-tipos-de-sensores-de-gas

Tipos de Sensores de Gás Existentes no Mercado


Código

//----------------------------------------------------------------------------------------
// Componentes : 1) Placa ESP32
//               2) DHT 22  + 1x Resistor de 4.7k
//               3) LED RGB + 3x Resistores 220 Ohms
//               4) LDR + 1x resistor de 10k
//               5) Jumpers diversos
//               6) 2x protoboards 400 adaptadas
//               7) 1x Display LCD 16x2 com I2C
//               8) 1x Buzzer Ativo 3V
//               9) 1x Sensor de Chama Digital
//              10) 1x Sensor de Gás MQ-2 + 1x Resistor 33K + 1x Resistor 22K
//              11) 1x Sensor de Presença PIR 
//
// Bibliotecas : ESPAsyncWebServer    => https://github.com/me-no-dev/ESPAsyncWebServer
//               AsyncTCP             => https://github.com/me-no-dev/AsyncTCP
//               ESP32Ping            => https://github.com/marian-craciunescu/ESP32Ping
//               WiFiManager          => https://github.com/tzapu/WiFiManager
//               DHT, FS, SPIFFS,
//               Time, ArduinoJson,
//               LiquidCrystal_I2C,   => Instalado através do Gerenciador de Bibliotecas
//
// Objetivos   : 1) Usar o SPIFFS (sistema de arquivo na FLASH do ESP32) para pegar o SSID/Senha/CPDID/Intervalo do Timer e 
//                  persistir. Como HTTP Server, os seguintes modos foram implementados:
//
//                  . modo AP usado para configurar o SSID e senha da rede a ser usada, CPDID e o Intervalo na porta 8080. Este modo é
//                    acionado pressionando-se o PUSH BUTTON interno do ESP32 (BOOT) durante a operação (não o botão RESET) 
//                    => quando neste modo, o operador deve procurar uma rede com ssid CPD_AP_nnnnn através de um celular ou 
//                    computador e deve se conectar usando a senha "password". Depois de conectado, o operador deve acessar 
//                    o endereço http://192.168.4.1:8080 através do navegador, uma página será mostrada e clicar o botão Config Wifi, 
//                    depois escolher o ssid, informar a senha e definir os parâmetros cpdID (até 50 caracteres) e 
//                    intervaloTimer (deve ser maior que 60 seg até 4 dígitos). Por último pressionar o botão SAVE na 
//                    interface. Os parâmetros serão salvos na FLASH do ESP32 para não perde quando desligado. A partir daí, 
//                    o modo AP é encerrado e o modo normal é acionado.
//
//                  . modo Normal é o estado conectado e respondendo aos requests na porta 80 e o monitoramento a cada intervalo
//                    do timer definido e esperando 6 blocos de funcões.  A partir deste ponto, o usuário pode acessar via 
//                    navegador no IP, mostrado no monitor serial no startup, na porta 81, ou usar programas como FING para 
//                    celular que permitem identificar o IP na rede. O ESP32 aparece com a identificação "Espressif" em tais
//                    programas. Outra alternativa, caso o usuário tenha um servidor DNS próprio em sua rede, bastaria criar uma 
//                    entrada no DNS usando o CPDID e o IP, que pode ser uma tarefa mais complexa para a maioria dos usuários. 
//                    O app FING é uma boa alternativa para descobrir o IP pela rede. A seguir, alguns exemplos já conhecendo 
//                    o IP do ESP32: 
//
//                    Ex1: http://10.0.0.100
//                    Ex2: http://10.0.0.100/sensores
//                    Ex3: http://10.0.0.100/info  
//                    Ex4: http://10.0.0.100/acessos
//
//               2) Programar o TIMER0 de HW para cada 1 min (ou o valor definido no modo AP) para monitorar a Temperatura, Umidade,
//                  Luminosidade e checar o ping para saber se a internet está disponível e não apenas conectada no AP.
//
//                  . o DHT22 informa a Temperatura e umidade
//                  . o LDR informa a luminosidade => como usa porta analógia, deve ser ligado
//                    numa porta ADC1 para uso concomitante com o Wifi no ESP32: 32, 33, 34, 35,
//                    36, 37, 38 ou 39.
//                  . LED RGB para mostrar a cor correspondente da temperatura como alternativa à visualização do Display.
//                    Isso pode facilitar a visualização de longe, sem a necessidade de entrar na sala do CPD
//                    para ver o Display. Naturalmente, deve existir uma janela ou porta com a possibilidade de se ver
//                    de fora da sala.
//                  . Atualizar o LEDBUILTIN (LED AZUL da placa ligado ou apagado) caso não haja internet disponível.
//
//               3) Esperar conexão http na porta 80 respondendo as URI's a seguir:
//
//                  /         => devolve no formato html os valores dos indicadores numa página AUTOREFRESH (a cada intervalo)
//                  /sensores => devolve os indicadores no formato JSON (Local, Temp, Umid, Lumin, TimeStamp)
//                  /info     => devolve as informações do server no formato JSON (Local, IP, MAC, StartupTime)
//
//               4) O LED BUILTIN ficará acesso caso haja a conexão wifi e apagado quando a conexão acabar. 
//                  Caso uma desconexão aconteça, uma reconexão será tentada posteriormente.
//
//               5) Acender uma alerta no LED RGB com a cor relativa ao valor da temperatura
//
//                         Temp < 10  => azul claro     => gelado
//                   10 <= Temp < 20  => azul escuro    => frio
//                   20 <= Temp < 25  => verde          => normal
//                   25 <= Temp < 30  => vermelho fraco => quente
//                         Temp >=30  => vermelho forte => muito quente
//
//              6) Mostrar a Temperatura em °C, (U)midade em % e (L)uminosidade em %, Data/Hora da última varredura num
//                 Display LCD 16x2. 
//
//              7) Programar o TIMER1 de HW para cada 1 min (ou o valor definido no modo AP) para implementação de WatchDog
//                 caso haja travamento no loop principal. Isso causa o reset do ESP32 para evitar paralisação do
//                 monitoramento até que alguma ação corretiva presencial seja feita. 
//
//              8) Monitorar os sensores de Fogo, Gás/Fumaça e de Presença. É considerado emergência quando FOGO ou GÁS é
//                 detectado e um sinal sonoro é emitido enquanto os sensores informam o sinal positvo para o evento.
//                 O sensor de presença é usado para registrar/persistir uma lista com as Top 10 ocorrências de detecção. Um
//                 intervalo de 5 min é considerado para aceitar uma nova detecção de presença. 
//    
// Sugestões   : A seguir, relacionamos algumas possibilidades de extensão de funcionalidades que os MAKERS podem
//               implementar a partir das ideias básicas deste projeto, lembrando que o céu é o limite:
//
//              1) Mandar e-mail para destinatários chave em caso de emergência (temperatura muito alta, fogo ou gás).
//              2) Acionar diferentes Relés para atuar em caso de emergência: acionar uma sirene ou um sistema de combate 
//                 a incêndio, ou um sistema de refrigeração/condicionamento de emergência, etc.
//              3) Como este projeto foi imaginado em essência para acesso numa rede local, sua funcionalidade pode ser estendida 
//                 para usar redes mundiais/globais da Arduino, RainMaker ou Blynk para permitir integração com celulares, etc. 
//              4) Da forma que este projeto foi implementado, como SERVIDOR HTTP, mais funcionalidades podem ser adicionadas 
//                 na página principal. Ex: adição de botões para acionar Relés e atuar na situação de emergência.
//              5) Outra ideia seria, em caso caso de múltiplos sistemas iguais a este em operação em vários CPD's, 
//                 uma aplicação centralizada poderia ser desenvolvida para fazer polling nos diversos sistemas via requisição REST/JSON
//                 e fazer persistência em banco de dados para permitir uma visão histórica dos parâmetros monitorados.
//                 Por isso implementamos as requisições REST/JSON "/sensores", "/info" e "/acessos".
//              6) Com uma integração com FIREWALL especialidado (Linux ou outro), este projeto poderia ser usado com NAT
//                 (NetWork Address Translation) para permitir acesso pela Internet em qualquer lugar do mundo sem mudar nada 
//                 no código. Neste caso, alguma autenticação deveria ser implementada para aumentar a segurança.
//              7) Registrar automaticamente o IP obtido no ESP32 num servidor DNS da rede para o usuário não ter a necessidade de descobrir
//                 o IP usando a biblioteca ESP-NETIF.
//              8) Monitorar os sensores de Fogo, Gás/Fumaça e de Presença. É considerado emergência quando FOGO ou GÁS é
//                 detectado e um sinal sonoro é emitido enquanto os sensores informam o sinal positivo para o evento. 
//                 O sensor de presença é usado para registrar/persistir uma lista com as Top 10 ocorrências de detecção. 
//                 Um intervalo de 5 min é considerado para aceitar uma nova detecção de presença. Isso para não registrar 
//                 ocorrências muito próximas, já que apenas 10 detecções são armazenadas. Uma atenção especial deve ser feita 
//                 para o sensor de gás/fumaça pois a tensão de saída é de 5V. Foi necessário fazer a redução de tensão para não 
//                 comprometer a porta do ESP32 que opera em 3,3V. Dois resistores de 22k e 33k foram usados. Quanto ao sensor de 
//                 presença, apesar de ser alimentado com 5V, a tensão de saída é menor do que o limite de 3,3V da porta do ESP32, 
//                 sem a necessidade de preocupação.           
//     
// Autor       : Alberto Menezes
//               Dailton Menezes
//
// Referências : 1) https://www.youtube.com/watch?v=VnfX9YJbaU8
//               2) https://www.youtube.com/watch?v=373k6-KwOEE
//               3) https://docs.espressif.com/projects/arduino-esp32/en/latest/api/timer.html
//               4) https://randomnerdtutorials.com/esp32-async-web-server-espasyncwebserver-library/
//
// Versão      : 1.0 Ago/2022
//----------------------------------------------------------------------------------------

#include <Arduino.h>                // Biblioteca Arduino
#include <WiFi.h>                   // Biblioteca WiFi
#include <AsyncTCP.h>               // Biblioteca AsyncTCP usado pelo Web
#include <ESP32Ping.h>              // Biblioteca Ping
#include <FS.h>                     // Biblioteca FileSystem
#include <SPIFFS.h>                 // Biblioteca SPIFFS
#include <WiFiManager.h>            // Biblioteca WiFi Manager
#include <ESPAsyncWebServer.h>      // Biblioteca Asynch Web Server

#include <DHT.h>                    // Biblioteca DHT 
#include <DHT_U.h>                  // Biblioteca DHT complemenbto
#include <time.h>                   // Biblioteca Time para manipulação de data/hora
#include <ArduinoJson.h>            // Biblioteca JSON para comunicação e parãmetros

#include <LiquidCrystal_I2C.h>      // Biblioteca do Display LCD

#define MAX_TRILHA   10                                // As 10 últimas presenças serão memorizadas 
#define PIR_MIN_TIME 5*60                              // Intervalo em seg. para aceitar nova detecção de presença 5 min = 300seg.
#define ESP_DRD_USE_SPIFFS true                        // Uso com SPIFFS
#define JSON_CONFIG_FILE "/cpd_config.json"            // Arquivo JSON de configuração
#define ESP_getChipId()   ((uint32_t)ESP.getEfuseMac() // Simular ID da placa ESP

#define alarmeBreve 20              // Define 20 mseg para alarme breve
#define alarmeLeve  200             // Define 200 mseg para alarme leve
#define alarmeFogo  500             // Define 500 mseg para alarme de fogo
#define alarmeGas   1000            // Define 1000 mseg para alarme de Gás 

#define cleanUpCycle 5000           // 5000 millisec para CleanUp ou 5 sec (sockect's)

#define pinLDR      33              // Pino do LDR 
#define pinBuzzer   27              // Pino do Buzzer
#define pinGas      25              // Pino do Sensor de Gás
#define pinDHT      23              // Pino do DHT22
#define RedPin      19              // Pino RED do LED RGB
#define GreenPin    18              // Pino GREEN do Led RGB
#define pinFogo     15              // Pino do Sensor de Fogo  
#define pinPIR      26              // Pino do Sensor de Presença
#define BluePin     5               // Pino BLUE do LED RGB
#define LED_BUILTIN 2               // Pino para o Led Interno do ESP32
#define TRIGGER_PIN 0               // Pino do botão para forçar a entrada no modo de configuração do WiFi

#define LCDSDAPin   21              // Pino SDA do Display LCD
#define LCDSCLPin   22              // Pino SCL do Dispaly LCD

#define DHTTYPE    DHT22            // Modelo do DHT a ser usado

#define PWM_R_ledChannel    0       // Canal do LED RGB Red
#define PWM_G_ledChannel    1       // Canal do LED RGB Green
#define PWM_B_ledChannel    2       // Canal do LED RGB Blue

// Setting LED PWM

#define PWM_freq        5000        // Frequência para a trativa do LED RGB na porta PVM
#define PWM_resolution  8           // Resolução da porta PVM

#define Temp_Gelado   0             // Indicador de Temperatura gelada
#define Temp_Frio     1             // Indicador de Temperatura fria
#define Temp_Normal   2             // Indicador de Temperatura normal
#define Temp_Quente   3             // Indicador de Temperatura quente
#define Temp_Fervendo 4             // Indicador de Temperatura elevada

#define Time_Slice    60000000      // Default intervalo do timer 1 min
#define timeoutWifi   15*1000       // Default tempo de tentativa de reconexão WiFi 15 sec

// Definições para sensores de gás e fogo

#define GAS           LOW           // Estado do Sensor quando há gás detectado
#define FOGO          LOW           // Estado do Sensor quando há fogo detectado
#define BUZZER_OFF    LOW           // Nível do Buzzer ativo
#define BUZZER_ON     HIGH          // Nível do Buzzer ativo

DHT dht(pinDHT, DHTTYPE);           // Tratativa da Temperatura e Umidade

// Alertas visuais de cores do LED para a temperatura

int Alerta[][3] = {
  {0, 191, 255},                    // Azul Claro            T < 10 graus
  {0, 0, 255},                      // Azul entre           10 <= T < 20
  {0, 128, 0},                      // Verde entre          20 <= T < 25
  {250, 128, 114},                  // Vermelho Claro entre 25 <= T < 30
  {255, 0, 0}                       // Vermelho              T >= 30
};

volatile int interruptCounter = 0;  // Variáveis para sincronizar a interrupção de HW para o Timer
hw_timer_t * timerSensores = NULL;  // Timer para a varredura dos sensores
hw_timer_t * timerWatchDog = NULL;  // Timer para a implementação de Watchdog para reset do ESP32 em caso de travamento

portMUX_TYPE timerMux = portMUX_INITIALIZER_UNLOCKED; // semáforo para sincronismo com o loop principal

AsyncWebServer sv(80);              // Servidor http na porta 80 (WifiManager rodará na 8080)
AsyncWebSocket ws("/ws");           // Socket para cleanup de conexões antigas perto do limite máximo de conexões simultâneas

const char* NTP_SERVER = "a.st1.ntp.br"; // Dados do Servidor NTP do Brasil
const char* TZ_INFO    = "<-03>3";  // Definição do Fuso

time_t acessos[MAX_TRILHA];        // Últimas 10 deteccções de acesso ao CPD
int topo=-1;                       // Topo das lista de detecções
time_t startup;                    // hora do startup
time_t varredura;                  // hora da última varredura
int lastTemp = 0;                  // última Temp lida
int lastUmid = 0;                  // última Umid lida
bool primeiraVez = true;           // para forçar atualizar o LED antes do timer disparar
bool estadoEmergencia = false;     // se está no estado de emergência por fogo ou gás

IPAddress ip (1, 1, 1, 1);         // The remote ip to ping, DNS do Google
unsigned long semInternet;         // Momento da queda da Internet
bool lastInternet;                 // Última verificação da internet
bool atualInternet;                // Se tem internet no momento
unsigned long lastCleanUp;         // Última limpeza de conexões perdidas de navegadores para não estourar o http server

bool shouldSaveConfig = false;     // Flag se deve persistir os parãmetros
char cpdID[50] = "CPD-1";          // Nome default do Nó a ser monitorado (pode vir da parametrição na configuração)
int intervaloTimer = 60;           // Para receber o Intervalo default do timer (60 seg ou pode vir da parametrição na configuração)

bool nivelGas=HIGH;                // Se há presença de Gás  HIGH=Ausente
bool nivelFogo=HIGH;               // Se há presença de Fogo HIGH=Ausente

String nivelMsg[2]   = {"Presente", "Ausente"}; // Estados do Gás e Fogo
String emergencia[2] = {"Fogo", "Fumaça"};      // Qual emergência

bool nivel = BUZZER_ON;            // Nível do Buzzer na situação de emergência
unsigned long ultimaTroca = 0;     // Última troca do nível na emergência

WiFiManager wm;                    // Define o Objeto WiFiManager

LiquidCrystal_I2C lcd(0x27, 16, 2);// Define o objeto LCD

// Definição do HTML Principal

const char index_html[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html lang="pt-br">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta http-equiv="refresh" content="%ciclo%">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>Dados dos sensores</title>
<style>
html { font-family: Helvetica; display: inline-block; margin: 0px auto; text-align: center;}
body{margin-top: 50px;}
h1 {color: #444444; margin: 50px auto 30px;}
p {font-size: 24px; color: #444444; margin-bottom: 10px;}
table, th, td {border: 1px solid black;}
th, td {padding: 15px; font-size: 24px; color: #444444;}
table {margin-left: auto; margin-right: auto;}  
th {background-color: #4CAF50; color: white;}  
</style>
</head>
<body>
<div id="webpage">
<h1>Dados dos Sensores - %cpuid%</h1>
<p>Temperatura: <b>%temperatura%</b></p>
<p>Umidade: <b>%umidade%&#37;</b></p>
<p>Luminosidade: <b>%luminosidade%&#37;</b></p>
<p>Fogo: <b>%nivelfogo%</b></p>
<p>Gás: <b>%nivelgas%</b></p>
<p>Data/Hora: <b>%timestamp%</b></p>
<p><b>Top %maxtrilha% Acessos</b></p>
<table>
  <tr>
      <th>Ordem</th>
      <th>Data/Hora</th>
  </tr>
%acessos%
</table>
</div>
</body>
</html>
)rawliteral";

// Prototipação de Funções

void alarme(int tempo);                                     // Soa o alarme (Buzzer) de forma sincrona   (com delay)
void disparaSirene(byte pin, int intervalo, int index);     // Soa o alarme (Buzzer) de forma assincrona (sem delay)
void desligaSirene(byte pin);                               // Cancela o alarme assincrono (Buzzer)
void saveConfigFile();                                      // Persiste CPUID e Intervalo no SPIFFS do ESP32
bool loadConfigFile();                                      // Recupera CPUID e Intervalo do SPIFFS do ESP32
void saveConfigCallback();                                  // Callback para informação do processo de configuração WiFi
void configModeCallback(WiFiManager *myWiFiManager);        // Callback para WifiManager 
bool getNTPtime(int sec);                                   // Sincroniza o horário do ESP32 com  NTP server brasileiro
void WiFiEvent(WiFiEvent_t event);                          // Evento chamado no processo de conexão do Wifi
String getTimeStamp();                                      // Devolve o localtime dd/mm/aaaa hh:mm:ss
int temp2index(int temp);                                   // Classifica o valor da temperatura (faixas)
int getTemperatura();                                       // Devolve a temperatura lendo o DHT 22
int getUmidade();                                           // Devolve a umidade lendo o DHT 22
int getLuminosidade();                                      // Devolve a luminosidade lendo o LDR
void IRAM_ATTR onTimer();                                   // Trata a interrupção do Timer
void IRAM_ATTR resetModule();                               // Trata a interrupção para WatchDog
void setColor(int redValue, int greenValue, int blueValue); // Rotina para definir a cor RGB para o LED
void Acende_Alerta(int nivel);                              // Acende o LED RGB de acordo com a temperatura
void nao_encontrado(AsyncWebServerRequest *request);        // Responde a URL inválida 
String colorirTemp(int temp);                               // Dar a cor adequada ao valor da Temperatura para o HTML
String colorirNivel(bool nivel);                            // Dar a cor adequada ao valor do Nível Fogo/Gás para o HTML
String html(int temperatura, int umidade, int luminosidade);// Monta o html da resposta para a URI / (home do site)
void Check_WiFiManager(bool forceConfig);                   // Inicialização/Configuração  WiFi Manager no ESP32
void registraDeteccao();                                    // Registra a deteção de presença na lista
void displayRequest(AsyncWebServerRequest *request);        // Mostra informações da requisição http na Console

//------------------------------------------------
// Inicialização da Aplicação no ESP32
//------------------------------------------------

void setup()
{

  // Inicializa a serial

  Serial.begin(115200);
  while (!Serial) ;

  // Inicializa LED_BUILTIN

  pinMode(LED_BUILTIN, OUTPUT);

  // Inicializa o Botão interno do ESP32

  pinMode(TRIGGER_PIN, INPUT_PULLUP);

  // Inicializa o Buzzer 

  pinMode(pinBuzzer, OUTPUT); 
  
  // Inicializa o Sensor Fogo

  pinMode(pinFogo, INPUT);

  // Inicializa o Sensor de Gás

  pinMode(pinGas, INPUT);

  // Inicializa o PIR 

  pinMode(pinPIR, INPUT);   
  
  // Inicializa o display LCD

  lcd.init();
  lcd.backlight();

  // Inicializa a lista das últimas deteccões de acesso ao CPD

  memset(acessos, 0, sizeof(acessos));

  // 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(8080);
  
  // Chama Wifi_Manager para conectar no Wifi ou entrar em modo de configuração
  // caso os parãmetros SSID, Senha, CPIID e Intervalo do TIMER não estejam persistidos

  Check_WiFiManager(false);
 
  // Se chegamos até aqui é porque estamos conectados

  Serial.println("WiFi conectado...");
  Serial.print("IP address: ");
  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");  

  // Definições do LED RGB

  ledcSetup(PWM_R_ledChannel, PWM_freq, PWM_resolution);
  ledcAttachPin(RedPin, PWM_R_ledChannel);

  ledcSetup(PWM_G_ledChannel, PWM_freq, PWM_resolution);
  ledcAttachPin(GreenPin, PWM_G_ledChannel);

  ledcSetup(PWM_B_ledChannel, PWM_freq, PWM_resolution);
  ledcAttachPin(BluePin, PWM_B_ledChannel);

  // Verifica se está navegando pela internet pois às vezes fica conectado no AP porém sem internet

  lastInternet = Ping.ping(ip,4);
  if (!lastInternet)
  {
     semInternet = millis();
     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(NTP_SERVER);
  Serial.print(" com TimeZone ");
  Serial.println(TZ_INFO);

  configTime(0, 0, NTP_SERVER);
  setenv("TZ", TZ_INFO, 1);
  tzset();

  if (getNTPtime(10))
  { // wait up to 10sec to sync
    Serial.println("NTP Server sincronizado");
  } 
  else 
  {
    Serial.println("Time not set");
    ESP.restart();
  }

  // Pega a hora do startup

  time(&startup);
  localtime(&startup);

  // Inicialização do DHT22

  dht.begin();

  // Inicializa o Timer 0 de HW para a varredura dos sensores

  timerSensores = timerBegin(0, 80, true);
  timerAttachInterrupt(timerSensores, &onTimer, true);
  timerAlarmWrite(timerSensores, intervaloTimer*1000000, true);
  timerAlarmEnable(timerSensores);

  // Inicializa o Timer 1 de HW para implementação de WatchDog
  
  timerWatchDog = timerBegin(1, 80, true);
  timerAttachInterrupt(timerWatchDog, &resetModule, true);
  timerAlarmWrite(timerWatchDog, intervaloTimer*1000000, true);
  timerAlarmEnable(timerWatchDog);

  // Inicializa a resposta para /

  sv.on("/",  HTTP_GET, [](AsyncWebServerRequest * request)
  {
    // Responde a Página Principal mostrando os dados dos sensores                                

    displayRequest(request);

    request->send_P(200, "text/html", index_html, processor);  

  });

  // Inicializa a resposta para /sensores

  sv.on("/sensores",  HTTP_GET, [](AsyncWebServerRequest * request)
  {
     // Devolve JSON com as informações dos sensores
    
     displayRequest(request);

    // Buffer para JSON

    StaticJsonDocument<250> jsonDocument;
    char buffer[250];

    jsonDocument.clear();

    jsonDocument["Local"] = cpdID;

    sprintf(buffer, "%d ℃", getTemperatura());
    jsonDocument["Temperatura"] = buffer;

    sprintf(buffer, "%d %c", getUmidade(), '%');
    jsonDocument["Umidade"] = buffer;

    sprintf(buffer, "%d %c", getLuminosidade(), '%');
    jsonDocument["Luminosidade"] = buffer;

    sprintf(buffer, "%s", nivelMsg[nivelFogo]);
    jsonDocument["Fogo"] = buffer;    

    sprintf(buffer, "%s", nivelMsg[nivelGas]);
    jsonDocument["Gas"] = buffer;        

    jsonDocument["TimeStamp"] = getTimeStamp();

    serializeJson(jsonDocument, buffer);
    request->send(200, "application/json", buffer);

    jsonDocument.clear();

  });

  // Inicializa a resposta para /info

  sv.on("/info",  HTTP_GET, [](AsyncWebServerRequest * request)
  {

    // Devolve JSON com as informações do servidor
    
    displayRequest(request);

    // JSON data buffer

    StaticJsonDocument<250> jsonDocument;
    char buffer[250];

    jsonDocument.clear();

    jsonDocument["Local"] = cpdID;
    jsonDocument["IP"] = WiFi.localIP();
    jsonDocument["MAC"] = WiFi.macAddress();
    jsonDocument["SSID"] = WiFi.SSID();
    jsonDocument["IntervaloTimer"] = intervaloTimer;

    char timestamp[30];
    strftime(timestamp, 30, "%d/%m/%Y %T", localtime(&startup));

    jsonDocument["Startup"] = timestamp;

    serializeJson(jsonDocument, buffer);
    request->send(200, "application/json", buffer);

    jsonDocument.clear();

  });

 // Inicializa a resposta para /acessos

  sv.on("/acessos",  HTTP_GET, [](AsyncWebServerRequest * request)
  {

    // Devolve JSON com as informações dos Top Acessos
    
    displayRequest(request);

    // JSON data buffer

    StaticJsonDocument<400> jsonDocument;
    JsonArray jsonArray = jsonDocument.to<JsonArray>();    
    char buffer[400];
    char timestamp[30];

    jsonArray.clear();

    if (topo!=-1)
    {

       // Adiciona da posição do Topo até a posição 0
       
       for (int ind1=topo;ind1>-1;ind1--)
       {
          strftime(timestamp, 30, "%d/%m/%Y %T", localtime(&acessos[ind1]));
          jsonArray.add(timestamp);
       }

       // Adiciona da posição final do array até a posição depois do Topo
       // Isso para manter em ordem decrescente de data/hora os acessos
       
       for (int ind2=MAX_TRILHA-1;ind2>topo;ind2--)
       {
          if (acessos[ind2]>0)
          {
             strftime(timestamp, 30, "%d/%m/%Y %T", localtime(&acessos[ind2]));
             jsonArray.add(timestamp);
          }
       }
    }

    serializeJson(jsonArray, buffer);
    request->send(200, "application/json", buffer);

  });

  // Inicializa a resposta para endereço inválido (não suportados)

  sv.onNotFound(nao_encontrado);

  // Inicia o servidor propriamente dito

  sv.begin();

  // Olá na console

  Serial.print("Monitoramento do CPD V1.0 Ago/2023 - Online - ");
  Serial.println(cpdID);

  // Soa o alarme para avisar que o setup completou
  
  alarme(alarmeLeve);   // Avisa o início  

  // Seta a condição inicial de varredura do CleanUp de conexões perdidas

  lastCleanUp = millis();

}

//--------------------------------------------------------
// Loop principal para atender 6 blocos de funcionalidades
//--------------------------------------------------------

void loop()
{

  //--------------------------------------------------------------------------------------------------
  // Bloco 1 : Reseta o Timer 1 do WatchDog
  //--------------------------------------------------------------------------------------------------  

  timerWrite(timerWatchDog, 0); 
  
  //--------------------------------------------------------------------------------------------------
  // Bloco 2 : Verifica se o timer acionou para pegar os dados dos sensores e checar se há internet ativa
  //--------------------------------------------------------------------------------------------------

  if (interruptCounter > 0 || primeiraVez)
  {

     // Timer tratado
  
     if (interruptCounter > 0)
     {
        portENTER_CRITICAL(&timerMux);
        interruptCounter--;
        portEXIT_CRITICAL(&timerMux);
     }
     else primeiraVez = false;

     // Pega a hora da varredura

     time(&varredura);

     // Área de trabalho para formatção de texto
     
     char buf[30];
  
     // Lê o sensor de Luminosidade

     int L = getLuminosidade();
     Serial.print("Luminosidade: ");
     Serial.print(L);
     Serial.print("%");
  
     // Lê o sensor de temperatura
  
     int t = getTemperatura();
     sprintf(buf, " - Temperatura: %d℃", t);
     Serial.print(buf);
  
     // Lê o sensor de umidade

     int u = getUmidade();
     sprintf(buf, " - Umidade: %d%c", u, '%');
     Serial.println(buf);

     Serial.print("Detecção Fogo: ");
     Serial.print(nivelMsg[nivelFogo]);
     Serial.print("\tGás: ");
     Serial.println(nivelMsg[nivelGas]);

     // Atualiza o Display LCD com as informações

     sprintf(buf, "%d%cC U:%d%c L:%d%c", t, 0xDF, u, '%', L, '%');
     
     lcd.clear();
     lcd.setCursor(0, 0);
     lcd.print(buf);

     strftime(buf, 30, "%d/%m/%Y %T", localtime(&varredura));
     
     lcd.setCursor(0, 1);
     lcd.print(buf);
  
     // Acende o LED RGB de acordo com a Temperatura
  
     Acende_Alerta(temp2index(t));
  
     // Checa se há internet navegando
  
     atualInternet = Ping.ping(ip,4);
     digitalWrite(LED_BUILTIN, atualInternet);
      
     if (!lastInternet && atualInternet)
     { 
        // Voltou a Internet
        Serial.print("Internet voltou depois de ");
        Serial.print(millis()-semInternet);
        Serial.println(" msec");    
     }
     else if (lastInternet && !atualInternet)
     {
        // Internet caiu
        semInternet = millis();
        Serial.println("Internet caiu");
     }
     else if (!lastInternet && !atualInternet)
     {
        // Internet permanece fora
        Serial.println("Internet continua fora");
     }
     else 
     {
        // Internet permanece ativa
        Serial.print("Internet continua ativa com média de ");    
        Serial.print(Ping.averageTime());
        Serial.println(" ms");   
     }

     //alarme(alarmeBreve);
     
     lastInternet = atualInternet;

  }

  //--------------------------------------------------------------------------------------------------
  // Bloco 3 : Verifica se o botão 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. 
  //--------------------------------------------------------------------------------------------------

  if ( digitalRead(TRIGGER_PIN) == LOW)
  {

     // Força a entrada em modo de configuração

     Check_WiFiManager(true);  
      
  }

  //--------------------------------------------------------------------------------------------------
  // Bloco 4 : Verifica os sensores de gás e fogo e independente do timer e aciona o alarme de 
  //           forma assíncrona, caso necessário 
  //--------------------------------------------------------------------------------------------------
  
  nivelGas  = digitalRead(pinGas);
  nivelFogo = digitalRead(pinFogo);

  if ( nivelGas == GAS) disparaSirene(pinBuzzer, alarmeGas, 1);
  else if ( nivelFogo == FOGO) disparaSirene(pinBuzzer, alarmeFogo, 0);
  else if (estadoEmergencia) desligaSirene(pinBuzzer);

  // --------------------------------------------------------------------------------------------------
  // Bloco 5 : Verifica o sensor de presença PIR e tenta registrar na lista das 10 últimas ocorrências
  //---------------------------------------------------------------------------------------------------

  if (digitalRead(pinPIR))
  {
     registraDeteccao();
  }

  // -------------------------------------------------------------------------------------------------
  // Bloco 6 : Faz o CleanUp de conexões antigas do navegador na situação de limite de usuários
  //           para evitar o crash
  //--------------------------------------------------------------------------------------------------

  if (millis()-lastCleanUp > cleanUpCycle)
  {
     ws.cleanupClients(); 
     lastCleanUp = millis(); 
  }
 
}

//-----------------------------------------------
// Soa o buzzer por um tempo fornecido (com delay)
//-----------------------------------------------

void alarme(int tempo)
{
   digitalWrite(pinBuzzer,BUZZER_ON);
   delay(tempo);
   digitalWrite(pinBuzzer,BUZZER_OFF);  
}

//-------------------------------------------------
// Soa o buzzer por um tempo fornecido (sem delay)
//-------------------------------------------------

void disparaSirene(byte pin, int intervalo, int index)
{

  if (millis() - ultimaTroca > intervalo) 
  {
    nivel = !nivel;
    ultimaTroca = millis();
    if (!estadoEmergencia)
    {
       Serial.print("Emergência detectada: ");
       Serial.println(emergencia[index]);
       estadoEmergencia = true;
    }

  }
  
  digitalWrite(pin, nivel);  
  
}

//-------------------------------------------------
// Cancela o buzzer acionado de forma sem delay
//-------------------------------------------------

void desligaSirene(byte pin) 
{
  digitalWrite(pin, BUZZER_OFF);
  nivel = BUZZER_OFF;
  if (estadoEmergencia)
  {
     Serial.println("Emergência encerrada...");
     estadoEmergencia = false;
  }
}

//------------------------------------------------
// Persiste CPUID e Intervalo no SPIFFS
//------------------------------------------------

void saveConfigFile()
// O arquivo de Config é salvo no formato JSON
{
  Serial.println(F("Persistindo a configuração..."));
  
  // Cria um documento JSON
  StaticJsonDocument<512> json;
  json["cpdID"] = cpdID;
  json["intervaloTimer"] = intervaloTimer;
  json["topo"] = topo;

  JsonArray lista = json.createNestedArray("acessos");
  for (int ind=0;ind<MAX_TRILHA;ind++)
  {
     lista.add(acessos[ind]);
  }
 
  // Abre o arquivo de configuração
  File configFile = SPIFFS.open(JSON_CONFIG_FILE, "w");
  if (!configFile)
  {
    // Erro, arquino não foi aberto
    Serial.println("Erro ao persistir a configuração");
  }
 
  // Serializa os dados do JSON no arquivo
  serializeJsonPretty(json, Serial);
  Serial.println();
  if (serializeJson(json, configFile) == 0)
  {
    // Erro ai gravar o arquivo
    Serial.println(F("Erro ao gravar o arquivo de configuração"));
  }
  // Fecha o Arquivo
  configFile.close();
}

//------------------------------------------------
// Recupera CPUID e Intervalo do SPIFFS
//------------------------------------------------
 
bool loadConfigFile()
// Carrega o arquivo de Configuração
{

  // Verifica se o SPIFFS já foi inicializado
  
  if (!SPIFFS.begin(true))
  {
     SPIFFS.format();
     Serial.println("Sistema de Arquivo no SPIFFS foi formatado");
  }
 
  // Lê as configurações no formato JSON
  Serial.println("Montando o FileSystem...");
 
  // Força a entrada na primeira vez
  if (SPIFFS.begin(true))
  {
    Serial.println("FileSystem montado...");
    if (SPIFFS.exists(JSON_CONFIG_FILE))
    {
      // o arquivo existe, vamos ler
      Serial.println("Lendo o arquivo de configuração");
      File configFile = SPIFFS.open(JSON_CONFIG_FILE, "r");
      if (configFile)
      {
        Serial.println("Arquivo de configuração aberto...");
        StaticJsonDocument<512> json;
        DeserializationError error = deserializeJson(json, configFile);
        serializeJsonPretty(json, Serial);
        Serial.println();
        
        if (!error)
        {
          Serial.println("Recuperando o JSON...");
 
          strcpy(cpdID, json["cpdID"]);
          intervaloTimer = json["intervaloTimer"].as<int>();

          // Verifica se o array de Acessos existe no JSON recuperado

          int cont=0;
          if (json.containsKey("acessos") && json.containsKey("topo"))
          {
             topo = json["topo"].as<int>(); 
             JsonArray lista = json["acessos"];
             for (int ind=0;ind<lista.size(), ind<MAX_TRILHA;ind++)
             {
                cont++;
                acessos[ind] = lista[ind].as<time_t>();
             }
             Serial.print("Recuperado Topo=");
             Serial.println(topo);
             Serial.print("Acessos recuperados=");
             Serial.println(cont);
          }
          else Serial.println("Não encontrada persistência dos Acessos no SPIFFS...");
          
          return true;
        }
        else
        {
          // Erro ao ler o JSON
          Serial.println("Erro ao carregar o JSON da configuração...");
        }
      }
    }
  }
  else
  {
    // Erro ao montar o FileSystem
    Serial.println("Erro ao montar o FileSystem");
  }
 
  return false;
}
 
//----------------------------------------------------------
// Callback para informação do processo de configuração WiFi
//----------------------------------------------------------
 
void saveConfigCallback()
// Callback para nos lembrar de salvar o arquivo de configuração
{
  Serial.println("Persistência necessária...");
  shouldSaveConfig = true;
}

//----------------------------------------------------------
// 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.println(WiFi.softAPIP());
}

//---------------------------------------------------------
// 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;
}

//------------------------------------------------
// 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, HIGH);
      break;
    case SYSTEM_EVENT_STA_DISCONNECTED:
      Serial.println("Desconectado do AP WiFi");
      digitalWrite(LED_BUILTIN, LOW);
      Check_WiFiManager(false);
      break;
    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;
  }
}

//------------------------------------------------
// 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);
}

//------------------------------------------------
// Classifica o valor da temperatura (faixas)
//------------------------------------------------

int temp2index(int temp)
{

  if (temp < 10) return Temp_Gelado;
  else if (temp < 20) return Temp_Frio;
  else if (temp < 25) return Temp_Normal;
  else if (temp < 30) return Temp_Quente;
  else return Temp_Fervendo;

}

//------------------------------------------------
// Devolve a temperatura lendo o DHT 22
//------------------------------------------------

int getTemperatura()
{

  // Lê o sensor de temperatura

  int t = dht.readTemperature();
  if (!isnan(t) && t != INT_MAX)
  {
    lastTemp = t;
    return t;
  }
  else
  {
    Serial.print("\nErro ao ler a Temperatura => última leitura: ");
    Serial.println(lastTemp);
    return lastTemp;
  }

}

//------------------------------------------------
// Devolve a umidade lendo o DHT 22
//------------------------------------------------

int getUmidade()
{

  // Lê o sensor de umidade

  int u = dht.readHumidity();
  if (!isnan(u) && u != INT_MAX)
  {
    lastUmid = u;
    return u;
  }
  else
  {
    Serial.print("\nErro ao ler a Umidade => última leitura: ");
    Serial.println(lastUmid);
    return lastUmid;
  }

}

//------------------------------------------------
// Devolve a luminosidade lendo o LDR
//------------------------------------------------

int getLuminosidade()
{

  // Lê o sensor de Luminosidade

  return  map(analogRead(pinLDR), 0, 4095, 0, 100);

}

//------------------------------------------------
// Trata a interrupção do Timer 0 - Sensores
//------------------------------------------------

void IRAM_ATTR onTimer()
{
  portENTER_CRITICAL_ISR(&timerMux);
  interruptCounter++;
  portEXIT_CRITICAL_ISR(&timerMux);
}

//------------------------------------------------
// Trata a interrupção do Timer 1 - WatchDog
//------------------------------------------------

void IRAM_ATTR resetModule()
{
    ets_printf("(watchdog) reiniciar\n"); //imprime no log
    //esp_restart_noos();                 //reinicia o chip
    ESP.restart();                        //reinicia o chip
}

//----------------------------------------------------
// Rotina para definir a cor RGB para o LED
//----------------------------------------------------

void setColor(int redValue, int greenValue, int blueValue)
{

  // Acende o LED com as cores fornecidas

  ledcWrite(PWM_R_ledChannel, redValue);
  ledcWrite(PWM_G_ledChannel, greenValue);
  ledcWrite(PWM_B_ledChannel, blueValue);

  // Atualiza o valor RED na console

  Serial.print("R=");
  Serial.print(redValue);
  Serial.print("\t");

  // Atualiza o valor GREEN na console

  Serial.print("G=");
  Serial.print(greenValue);
  Serial.print("\t");

  // Atualiza o valor BLUE na console

  Serial.print("B=");
  Serial.println(blueValue);

}

//------------------------------------------------
// Acende o LED RGB de acordo com a temperatura
//------------------------------------------------

void Acende_Alerta(int nivel)
{
  setColor(Alerta[nivel][0], Alerta[nivel][1], Alerta[nivel][2]);
}

//------------------------------------------------
// Responde a URL inválida
//------------------------------------------------

void nao_encontrado(AsyncWebServerRequest *request)
{

  // Sub-rotina para caso seja retornado um erro

  Serial.print("Get NotFound from ");
  Serial.println(request->client()->remoteIP());

  // Retorna a mensagem de erro em caso de um retorno 404

  request->send(404, "text/html", "<h1>Erro: N&atilde;o encontrado</h1>");
}

//-------------------------------------------------------
// Dar a cor adequada ao valor da Temperatura para o HTML
//-------------------------------------------------------

String colorirTemp(int temp)
{
  int index = temp2index(temp);
  String cd = "<span style=\"color:RGB(";
  cd += Alerta[index][0];
  cd += ",";
  cd += Alerta[index][1];
  cd += ",";
  cd += Alerta[index][2];
  cd += ")\">";
  cd += temp;
  cd += "℃</span>";

  // retorna o HTML da cor

  return cd;
}

//--------------------------------------------------------
// Dar a cor adequada ao valor do Nível Fogo/Gás para Html
//--------------------------------------------------------

String colorirNivel(bool nivel)
{
   String result = "<span style=\"color:";
   if (nivel==LOW) result += "red"; 
   else result += "green"; 
   result += "\">";
   result += nivelMsg[nivel];
   result += "</span>";   
   return result;
}

//----------------------------------------------------
// Inicialização/Configuração do WiFi Manager no ESP32
//----------------------------------------------------

void Check_WiFiManager(bool forceConfig)
{

  // Tenta carregar os parâmetros do SPIFFS
  
  bool spiffsSetup = loadConfigFile();
  if (!spiffsSetup)
  {
    Serial.println(F("Forçando o modo de configuração..."));
    forceConfig = true;
  }

  // Define o modo AP
  
  WiFi.mode(WIFI_STA);

  // Seta HostName

  String hostname = "ESP32-"+String(cpdID);
  WiFi.config(INADDR_NONE, INADDR_NONE, INADDR_NONE, INADDR_NONE);
  WiFi.setHostname(hostname.c_str()); 
 
  // Define o handle para tratar os eventos do Wifi

  WiFi.onEvent(WiFiEvent);

  // Verifica se tem que resetar os parâmetros anteriores
  
  if (forceConfig) wm.resetSettings();
 
  // Define o CALLBACK do modo CONFIG com alteração
  
  wm.setSaveConfigCallback(saveConfigCallback);
 
  // Define o CALLBACK do modo CONFIG
  
  wm.setAPCallback(configModeCallback);
 
  // Definição de dois campos de parâmteros esperados
 
  // cpuID (String) - 50 careteres no máximo
  
  WiFiManagerParameter custom_cpdID("CpdID", "Informe o Id do CPD (< 50)", cpdID, 50);
  
  // Converte o inteiro em alpha para mostra a interface
  
  char convertedValue[6];
  sprintf(convertedValue, "%d", intervaloTimer); 
  
  // intervaloTimer(Number) - 4 caracteres no m[aximo
  
  WiFiManagerParameter custom_intervaloTimer("IntervaloTimer", "Informe o intervalo de varredura em seg (< 9999)", convertedValue, 4); 
 
  // Adiciona os campos de parâmetros no MENU do WifiManager
  
  wm.addParameter(&custom_cpdID);
  wm.addParameter(&custom_intervaloTimer);

  // Monta o SSID do modo AP para permitir a configuração
  
  char ssid[50];
  sprintf(ssid, "CPD_AP_%x",(uint32_t)ESP.getEfuseMac());

   // Verifica se entra no modo AP/Configuração ou no modo cliente normal
 
  if (forceConfig)
    // Entra no modo de AP de configuração ... com senha fixa
  {
    if (!wm.startConfigPortal(ssid, "password"))
    {
      Serial.println("Erro na conexão com timeout...");
      delay(3000);
      // Reseta para já tentar entrar no modo Normal
      ESP.restart();
      delay(5000);      
    }

  }
  else
  {
    // Entra no modo de conexão normal recuperando o SSID/Senha anteriores
    if (!wm.autoConnect(ssid, "password"))
    {
      Serial.println("Erro na conexão com timeout...");
      delay(3000);
      // if we still have not connected restart and try all over again
      ESP.restart();
      delay(5000);
    }
  }

  // Recupera o campo cpuId preenchido na interface
  
  strncpy(cpdID, custom_cpdID.getValue(), sizeof(cpdID));
  if (strlen(cpdID)==0) strcpy(cpdID,"CPD_NÃO_DEFINIDO");
  Serial.print("cpdID: ");
  Serial.println(cpdID);
 
  // Recupera o campo intervaloTimer preenchido na interface convertendo para inteiro
  
  intervaloTimer = atoi(custom_intervaloTimer.getValue());
  if (intervaloTimer < 60) intervaloTimer = 60;
  Serial.print("intervaloTimer: ");
  Serial.println(intervaloTimer);
  
  // Salva os parâmetros no FileSystem FLASH -> não perde quando desligado
  
  if (shouldSaveConfig)
  {
    saveConfigFile();
  }
  
}

//----------------------------------------------------
// Registra a detecção de presença desde que tenha uma
// diferença de PIR_MIN_TIME (5 min.) para a detecção
// anterior. O sensor do tipo PIR quando detecta a 
// presença ele mantém HIGH no pino de 5 seg a 2,5 min.
// Estamos partindo do princípio que o PIR está ajustado
// para o mínimo.
//----------------------------------------------------

void registraDeteccao()
{
   time_t agora;
   bool alterou=false;

   // Pega a data/hora do momento

   time(&agora);

   // Empilha a data/hora

   if (topo==-1) 
   {
      topo++;
      acessos[topo]=agora;
      alterou = true;
      Serial.print("Detecção de presença[");
      Serial.print(topo);
      Serial.println("]");      
   }
   else if (agora-acessos[topo] > PIR_MIN_TIME)
   {
      topo = (topo+1) % MAX_TRILHA;
      acessos[topo]=agora;
      alterou = true;
      Serial.print("Detecção de presença[");
      Serial.print(topo);
      Serial.println("]");
   }

   // Verifica se deve persistir no SPIFFS

   if (alterou) saveConfigFile();
   
}

//------------------------------------------------
// Expande a Página Principal
//------------------------------------------------

String processor(const String& var)
{
  
  if (var == "ciclo")
  {
     return String(intervaloTimer); 
  }
  else if (var.equalsIgnoreCase("cpuid"))
  {
     return cpdID;
  }
  else if (var.equalsIgnoreCase("temperatura"))
  {
     return colorirTemp(getTemperatura());
  }  
  else if (var.equalsIgnoreCase("umidade"))
  {
     return String(getUmidade());
  }   
  else if (var.equalsIgnoreCase("luminosidade"))
  {
     return String(getLuminosidade());
  }     
  else if (var.equalsIgnoreCase("nivelfogo"))
  {
     return colorirNivel(nivelFogo);
  }    
  else if (var.equalsIgnoreCase("nivelgas"))
  {
     return colorirNivel(nivelGas);
  }   
  else if (var.equalsIgnoreCase("timestamp"))
  {
     return getTimeStamp();
  }   
  else if (var.equalsIgnoreCase("maxtrilha"))
  {
     return String(MAX_TRILHA);
  }   
  else if (var.equalsIgnoreCase("acessos"))
  {
     String cd="";

     // Varre da posição do Topo até a posição 0
  
     int cont=0;
     char timestamp[30];
    
     for (int ind1=topo;ind1>-1;ind1--)
     {
        cont++;
        strftime(timestamp, 30, "%d/%m/%Y %T", localtime(&acessos[ind1]));
        cd += "  <tr>\n";
        cd += "    <td>";
        cd += cont;
        cd += "</td>\n";
        cd += "    <td>";
        cd += timestamp;
        cd += "</td>\n";
        cd += "  </tr>\n";
      }
  
     // Varre da posição final do array até a posição depois do Topo
     // Isso para manter em ordem decrescente de data/hora os acessos
     
     for (int ind2=MAX_TRILHA-1;ind2>topo;ind2--)
     {
        if (acessos[ind2]>0)
        {
           cont++;
           strftime(timestamp, 30, "%d/%m/%Y %T", localtime(&acessos[ind2]));
           cd += "  <tr>\n";
           cd += "    <td>";
           cd += cont;
           cd += "</td>\n";
           cd += "    <td>";
           cd += timestamp;
           cd += "</td>\n";
           cd += "  </tr>\n";        
        }
     }
    
     return cd; 
  } 
  return String();
}

//----------------------------------------------------
// Mostra as informações da requisição http na console
//----------------------------------------------------

void displayRequest(AsyncWebServerRequest *request)
{

   Serial.print("Método: ");
   Serial.print(request->methodToString());
   Serial.print("\t| URL: ");
   Serial.print(request->url());     
   Serial.print("\t| IP: ");
   Serial.println(request->client()->remoteIP());    
   
}

Demonstração

O vídeo de demonstração é conferido em https://youtu.be/12Ldjl_gXg0.


Sugestões

A seguir, relacionamos algumas possibilidades de extensão de funcionalidades que os MAKERS podem implementar a partir das ideias básicas deste projeto, lembrando que o céu é o limite:

  1. Mandar e-mail para destinatários-chave em caso de emergência (temperatura muito alta, fogo ou gás).
  2. Acionar diferentes Relés para atuar em caso de emergência: acionar uma sirene ou um sistema de combate a incêndio, ou um sistema de refrigeração/condicionamento de emergência, etc.
  3. Como este projeto foi imaginado em essência para acesso numa rede local, sua funcionalidade pode ser estendida para usar redes mundiais/globais da Arduino, RainMaker ou Blynk para permitir integração com celulares, etc.
  4. Da forma que este projeto foi implementado, como SERVIDOR HTTP, mais funcionalidades podem ser adicionadas na página principal. Ex: adição de botões para acionar Relés e atuar na emergência.
  5. Em caso de múltiplos sistemas como este em operação em vários CPDs, seria possível o desenvolvimento de uma aplicação centralizada para fazer polling nos diversos sistemas via requisição REST/JSON, e fazer persistência em banco de dados para permitir uma visão histórica dos parâmetros monitorados. Por isso implementamos as requisições REST/JSON “/sensores”, “/info” e “/acessos”.
  6. Com uma integração com FIREWALL especializado (Linux ou outro), este projeto poderia ser usado com NAT (NetWork Address Translation) para permitir acesso pela Internet em qualquer lugar do mundo sem mudar nada no código. Neste caso, alguma autenticação deveria ser implementada para aumentar a segurança.
  7. Registrar automaticamente o IP obtido no ESP32 num servidor DNS da rede para o usuário não ter a necessidade de descobrir o IP usando a biblioteca ESP-NETIF.

Conclusão

O ESP32 é um excelente microcontrolador com capacidade para suportar aplicações mais complexas e maiores em código e dados, de baixo custo, inclusive com serviços de Internet de forma assíncrona sem comprometer o fluxo da função “void loop()”. A característica de operar em 3,3V nos exige maior atenção na escolha dos sensores e, quando operam com saída em 5V, a redução de tensão é necessária através do uso de resistores.


Autores


Alberto de Almeida Menezes
tinho.menezes@gmail.com

Bacharel em Engenharia de Áudio e Produção Musical pela Berklee College of Music.


Dailton de Oliveira Menezes
dailton.menezes@gmail.com

Bacharel em Ciência da Computação pela Universidade Federal de Minas Gerais.


Eletrogate

28 de novembro de 2023

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.

Conheça a Metodologia Eletrogate e Lecione um Curso de Robótica nas Escolas da sua Região!

Eletrogate Robô

Cadastre-se e fique por
dentro de novidades!