Módulos Wifi

Monitoramento da Atividade Elétrica do Coração

Eletrogate 2 de janeiro de 2025

Introdução

Este projeto tem como objetivo implementar um monitor de atividade elétrica do coração através do uso do sensor AD8232 numa aplicação Web Server para ESP32. O sensor permitirá fazer a medida da diferença de potencial elétrico entre os eletrodos colocados no corpo, que varia conforme o coração se contrai e relaxa, e mostrará num gráfico em forma de onda no Navegador transmitindo os dados via Web Socket numa rede Local.

Figura 1 – Componentes Montados


Bibliotecas Utilizadas

Antes de iniciar a programação, é preciso fazer a instalação das placas da Espressif e das seguintes bibliotecas:

  • ESPAsyncWebServer (disponível no GitHub do desenvolvedor);
  • WiFiManager
  • ElegantOTA
  • AsyncTCP
  • ESP32Ping
  • ESPmDNS;
  • FS
  • SPIFFS
  • WiFi;
  • time;
  • ArduinoJson

Com exceção da biblioteca ESPAsyncWebServer, você encontra as demais no próprio Gerenciador de Bibliotecas da Arduino IDE, sem contar que algumas, como as relacionadas ao WiFi, são instaladas juntamente com o pacote de placas da Espressif. Importante: utilizamos a versão 2.0.17 (última da versão 2) da placa ESP32 pois tivemos problemas de compatibilidade usando a versão 3.

Figura 2 – Biblioteca Principal

Se tem dúvidas na instalação das placas, siga nosso tutorial de instalação.

 


Objetivos Específicos

  • Implementar um servidor http para responder na porta 80 através da conexão WiFi respondendo às seguintes requisições:
    1. / mostrar a página principal para ativação/desativação do monitoramento.
    2. /config mostrar um FORM para definição de parâmetros que interferem na medição.
    3. /setConfig efetivar as mudanças dos parâmetros persistindo no SPIFFS.
    4. /update para atualizar o firmware via OTA
  • Atualizar o relógio interno do ESP32 sincronizado com o servidor NTP do Brasil.
  • Inserir um nome DNS para a estação para evitar ter que descobrir o IP e a URLhttp://<dnsname>.local poderá ser usada para acessar a página principal.
  • Usar o WiFi Manager para configurar as credenciais da Rede WiFi e parâmetros do programa.
  • Calcular a frequência cardíaca computando o número de batidas entre picos da onda e apresentando na interface Web.
  • Representar a frequência cardíaca num componente visual pulsante na mesma frequência na interface Web.
  • Reproduzir os batimentos cardíacos na placa de som do Navegador.
  • Representar também a frequência cardíaca através da luminosidade variável de um led no circuito durante a medição.

Contextualização

Apesar de ter aplicação no mundo real, o projeto apresentado aqui é para fins didáticos e não tem como objetivo interpretar os dados ou mesmo diagnosticar a partir das informações coletadas. Interpretar e diagnosticar cabe a um profissional da área médica. Para se ter uma ideia, o equipamento oficial usado para fazer o Eletrocardiograma possui 12 eletrodos, ao passo que, o AD8232 possui apenas três. Portanto, os equipamentos projetados especificamente para a área médica possuem uma amplitude maior no espectro da medição.

O AD8232 é um módulo de monitoramento de ECG (Eletrocardiograma) simplificado, projetado para aplicações portáteis e de baixo custo. Ele utiliza apenas 3 eletrodos para captar os sinais elétricos do coração, o que é suficiente para obter uma leitura básica do ECG.

Essa configuração é conhecida como derivação de Einthoven e permite a obtenção de uma única derivação do ECG, geralmente a derivação II, que é uma das mais comuns e úteis para monitoramento básico da atividade cardíaca.

Embora o AD8232 não forneça a mesma quantidade de informações detalhadas que um ECG de 12 derivações, ele é bastante eficaz para aplicações de monitoramento contínuo, dispositivos vestíveis e projetos de prototipagem, onde simplicidade e portabilidade são essenciais.


Sobre o Sensor AD8232

O AD8232 é um módulo de monitoramento de sinais cardíacos, projetado para extrair, amplificar e filtrar pequenos sinais biopotenciais na presença de ruído. Ele é amplamente utilizado em aplicações de eletrocardiograma (ECG) para medir a atividade elétrica do coração. O sensor é compacto e fácil de integrar com microcontroladores, tornando-o ideal para projetos de monitoramento de saúde e fitness. Além disso, o AD8232 possui uma excelente rejeição de ruído, o que garante leituras precisas e confiáveis.

Características do Sensor AD8232

  • Medida da Atividade Elétrica do Coração: Captura sinais elétricos do coração e os converte em dados analógicos.
  • Redução de Ruídos: Possui filtros integrados para minimizar ruídos indesejados.
  • Conexões Simples: Inclui pinos para conexão com eletrodos e placas de desenvolvimento como o Arduino e ESP32.
  • Saída Analógica: Fornece uma leitura analógica que pode ser processada por um microcontrolador.

Figura 3 – Módulo AD8232

Pinagem do Módulo

  • OUTPUT              -> é valor analógico representando a diferença de potencial elétrico entre os eletrodos colocados no corpo
  • 3.3V                      -> Nível da alimentação 3.3V
  • GND                     -> GND ou terra
  • LO+, LO-             -> (Lead-Off Positive/Negative): são usados para detectar se os eletrodos estão corretamente conectados ao corpo. Eles ajudam a identificar se algum eletrodo se soltou ou está mal posicionado, o que pode causar leituras incorretas.
  • SDN                      -> (Shutdown): é usado para colocar o AD8232 em modo de baixa potência. Quando ativado, ele desliga o sensor para economizar energia. Pode ser útil em aplicações onde a economia de energia é crítica, como em dispositivos portáteis alimentados por bateria.

Aplicações do Módulo

O sensor AD8232 é amplamente utilizado para medir a atividade elétrica do coração, mas também pode ser adaptado para outras aplicações que envolvem a medição de biopotenciais. Em um braço robótico, o AD8232 poderia ser utilizado para monitorar sinais biológicos do operador, como a frequência cardíaca, e ajustar o comportamento do braço com base nesses dados.

Por exemplo, em um cenário de reabilitação, o sensor poderia monitorar o estado fisiológico do paciente e ajustar a intensidade ou a velocidade dos movimentos do braço robótico para garantir um exercício seguro e eficaz. Além disso, em aplicações de controle remoto, os sinais biológicos do operador poderiam ser usados para ajustar a resposta do braço robótico, tornando a operação mais intuitiva e responsiva.

Podemos resumir:

  • Monitoramento da atividade cardíaca e fitness
  • ECG portátil e prático
  • Monitoramento da saúde remota
  • Usado em dispositivos de jogos
  • Aquisição de sinal biopotencial
  • biometria
  • estudos de fisiologia
  • Prototipagem de instrumentos biomédicos
  • Variabilidade da frequência cardíaca
  • Interação humano-computador
  • Psicofisiologia

Na Referência 1, você pode encontrar informações adicionais sobre aplicações do AD8232.

Posicionamento dos Eletrodos

Para obter leituras precisas com o sensor AD8232, é importante posicionar corretamente os eletrodos no corpo. Aqui estão algumas orientações:

  1. Eletrodo RA (Right Arm, vermelho):
    • Coloque o eletrodo no lado direito do peito, logo abaixo da clavícula.
  2. Eletrodo LA (Left Arm, amarelo):
    • Coloque o eletrodo no lado esquerdo do peito, logo abaixo da clavícula.
  3. Eletrodo RL (Right Leg, verde):
    • Coloque o eletrodo na parte inferior do abdômen, do lado direito. Observe que este eletrodo não possui um pino correspondente pois é usado no controle de ruído pelo módulo.

Dicas para Melhorar a Conexão

  • Limpeza da Pele: Limpe a área onde os eletrodos serão colocados com álcool para remover óleos e sujeira.
  • Fixação: Certifique-se de que os eletrodos estão bem fixados à pele para evitar leituras instáveis. A depilação no local dos eletrodos também ajuda a obter melhores resultados melhorando a fixação.
  • Evitar Movimentos: Tente minimizar os movimentos durante a medição para reduzir ruídos e interferências.
  • Qualidade dos Eletrodos: A qualidade dos eletrodos também pode interferir na precisão das leituras. Eles precisam ficar bem fixados na pele. Eletrodo sem a cola adesiva gera leituras ruins. O kit que vem com o módulo deixa um pouco a desejar. Sugerimos adquirir em farmácias um kit descartável de melhor qualidade. Evite a reutilização dos adesivos pois a cola vai perdendo a capacidade de fixação.

Figura 4 – Posicionamento dos Eletrodos

Detalhes do Valor Analógico

  • Eletrocardiograma (ECG): O valor analógico representa o sinal de ECG, que é um gráfico da atividade elétrica do coração ao longo do tempo.
  • Ondas do ECG: O sinal de ECG é composto por várias ondas (P, QRS, T) que correspondem a diferentes fases do ciclo cardíaco.
    • Onda P: Representa a despolarização dos átrios.
    • Complexo QRS: Representa a despolarização dos ventrículos.
    • Onda T: Representa a repolarização dos ventrículos.

Como Relacionar ao Batimento Cardíaco

  • Detecção de Picos: Para determinar a pulsação (batimentos por minuto), vamos detectar os picos do complexo QRS no sinal de ECG. Cada pico QRS corresponde a um batimento cardíaco.
  • Cálculo da Frequência Cardíaca: Contando o número de picos QRS em um intervalo de tempo, poderemos calcular a frequência cardíaca.

Utilização do WifiManager

Quando se trabalha com projetos baseados em ESP32 que requerem conectividade Wi-Fi, uma das principais preocupações é como gerenciar a configuração de rede de uma forma que seja ao mesmo tempo flexível e acessível para o usuário final. Aqui é onde a biblioteca WiFiManager brilha, oferecendo uma solução elegante para este desafio comum.

O que é WiFiManager?

WiFiManager é uma biblioteca para ESP8266/ESP32 que abstrai os detalhes de conexão a redes Wi-Fi. Ela é particularmente útil para projetos onde os detalhes da rede Wi-Fi não podem ser hardcoded no dispositivo. A biblioteca fornece um ponto de acesso (AP) e uma interface web que permite ao usuário inserir as credenciais de sua própria rede Wi-Fi.

Benefícios do WiFiManager

  1. Configuração Dinâmica: O maior benefício do WiFiManager é permitir que o dispositivo seja configurado em novas redes Wi-Fi sem necessidade de alterar e reenviar o código. Isso é ideal para dispositivos que serão usados em diferentes ambientes ou comercializados para uma gama mais ampla de usuários.
  2. Interface de Usuário Amigável: Através de uma interface web gerada automaticamente, o usuário pode facilmente conectar o ESP32 a redes Wi-Fi disponíveis, sem a necessidade de conhecimento técnico sobre programação do dispositivo.
  3. Zero Hardcoding: As credenciais de Wi-Fi não precisam ser hardcoded no firmware. Isso aumenta a segurança, pois as informações de rede não estão expostas no código, e adiciona conveniência, permitindo ajustes sem a necessidade de recompilação e upload do firmware.
  4. Conectividade Robusta: Se as credenciais de Wi-Fi mudarem ou o dispositivo não conseguir se conectar à rede, ele automaticamente reconfigura o ponto de acesso para permitir ao usuário inserir novas credenciais, garantindo que o dispositivo nunca fique inacessível.
  5. Customização e Extensão: WiFiManager é altamente customizável, permitindo adicionar parâmetros e configurações específicas do projeto na página de configuração, o que amplia ainda mais sua utilidade.

Passo a passo de como utilizar:

  • Na primeira execução do programa, o WifiManager não estará configurado e entrará no AP MODE, onde poderá ser visto na lista de AP’s da Rede WiFi (Figura 5 – ESP32 na Lista de AP’s da Rede).
  • Deve-se selecionar o ESP32_XXXX (onde XXXX é o serial do ESP32). Veja a Figura 6 – Conectado no ESP32 AP MODE.
  • Uma vez conectado no ESP32, deve-se acessar a URL http://192.168.4.1:8080 para a tela de entrada do WiFiManager (Figura 7 – Acessando http://192.168.4.1:8080).
  • Uma tela aparecerá com os parâmetros a serem definidos antes de pressionar SAVE (Figura 8 – Parâmetros a serem definidos). Quando os parâmetros estiverem todos definidos e conferidos (inclusive o SSID da Rede WiFi e a senha), deve-se pressionar o botão SAVE mas abaixo na tela. O WiFiManager salvará os parâmetros no SPIFFS e o ESP32 entrará com a aplicação ativa, conectado na Rede WiFi selecionada e poderá ser acessado via a URL http://ecg.local , supondo que o DNSNAME definido seja ECG.

Figura 5 – ESP32 na Lista de AP’s da Rede

Figura 6 – Conectado no ESP32 AP MODE

Figura 7 – Acessando http://192.168.4.1:8080

Figura 8 – Parâmetros a serem definidos

 


Utilização da ElegantOTA

Uma das grandes vantagens de utilizar o ESP32 é a possibilidade de realizar atualizações de firmware Over-the-Air (OTA). Com a biblioteca ElegantOTA, esse processo se torna ainda mais simples e intuitivo. A ElegantOTA oferece uma interface de usuário elegante e fácil de usar, permitindo que o usuário atualize o firmware do seu dispositivo sem a necessidade de conectá-lo fisicamente a um computador. Utilizaremos a biblioteca OTA no Async Mode para ter compatibilidade com o AsyncWebServer. Veja a Referência 6 para maiores detalhes.

Benefícios do ElegantOTA:

  • Interface Intuitiva: A ElegantOTA fornece uma interface web amigável para realizar as atualizações.
  • Status em Tempo Real: Acompanhe o progresso da atualização em tempo real, garantindo que tudo esteja ocorrendo conforme o esperado.
  • Facilidade de Implementação: Com apenas algumas linhas de código, você pode integrar a ElegantOTA ao seu projeto.

Figura 9 – Tela de Definição da Imagem para a atualização

Figura 10 – tela de Autenticação para a atualização


Detalhes da Implementação

  • Utilizaremos um CASE de plástico para acomodar o circuito na protoboard e o SHIELD com a bateria 18650. A protoboard de 400 pontos foi adaptada retirando-se uma das fileiras de positivo/negativo para ser possível acomodar no CASE juntamento com o SHIELD da bateria 18650.

Figura 11 – Visão Lateral do Case

Figura 12 – Visão de Cima do Case

  • O LED BUILTIN do ESP32 será usado para sinalizar a conectividade com a Internet.
  • Utilizaremos um LED VERDE variando o brilho de acordo com o valor lido na porta VP (A0) do ESP32 referente ao OUTPUT do AD8232.
  • Utilizaremos a compilação condicional para o ESP32 30 Pinos ou o ESP32C3 Mini.
  • Utilizaremos uma bateria 18650 para alimentar o ESP32 e o AD8232. Recomendamos atenção no manuseio desse tipo de bateria por causa de risco de superaquecimento que pode gerar potencialmente incêndio. Nesse projeto usamos uma bateria 18650 de 4,2V 9800 mAh. Supondo um consumo médio constante de 400mA, teremos uma autonomia de cerca de 24 h.
  • Utilizaremos um SHIELD para a bateria 18650 para permitir alimentar o ESP32 em 5V e o AD8232 será alimentado através da porta de 3,3V do ESP32. O SHIELD permitirá recarregar a bateria sem a necessidade de remoção e interrupção do funcionamento do circuito. Adicionalmente, o SHIELD tem um circuito de proteção para controlar a carga excessiva ou a descarga excessiva aumentando a segurança e a vida útil da bateria. Existe a opção de usar um POWER BANK que poderá gerar uma autonomia muito maior que a bateria 18650. Existem modelos com até 20.000 mAh.
  • O firmware do ESP32 pode ser atualizado over-the-air através da OTA. Basta usar a URL /update para cair no FORM de atualização (o botão ATUALIZAR no HTML faz este acionamento). Será necessário ter a usuário/senha para atualizar (veja o default no código fonte). O arquivo .bin do sketch pode ser obtido pelo IDE do Arduino através do Menu Sketch | Exportar Binário Compilado. Isso facilita atualizações de versão do programa sem a necessidade de se ter a placa conectada fisicamente no computador para a carga. Muitas vezes a placa pode estar num lugar de difícil remoção para ligar no computador. Além disso, o desenvolvedor pode enviar o arquivo .bin de uma nova versão para o usuário/cliente fazer a carga via OTA, podendo até publicar o .bin num site para o download pelo usuário/cliente. Portanto, isso facilita a atualização em qualquer lugar do mundo. Utilizaremos a biblioteca ElegantOTA com Async Mode para a coexistência estável com o Async Web Server. Veja a Referência 5 para maiores detalhes.
  • A figura a seguir mostra os pré-requisitos importantes para a compilação do aplicativo que precisam ser ajustados antes da compilação. A biblioteca ElegantOTA precisa de espaço na partição SPIFFS.

Figura 13 – Parâmetros para Compilação

  • A figura a seguir mostra como gerar o arquivo .bin no IDE do Arduino para ser usado na atualização OTA. Importante: a primeira compilação deve ser com o ESP32 ligado direto na USB da estação de trabalho. A partir daí, as próximas cargas podem ser feitas via a interface OTA uma vez que o código OTA já está carregado na partição do ESP32.

Figura 14 – Geração de Imagem

  • Não é recomendado usar a alimentação do AD8232 de uma fonte ligada à energia da Concessionária por causa de ruído que afeta a qualidade do sinal lido e, potencialmente, aumenta o risco de exposição elétrica no coração uma vez que os eletrodos estão ligados ao corpo do paciente. Por isso, estamos alimentando através de uma bateria 18650 com SHIELD. A biblioteca OTA ajuda a atualização, principalmente na fase de teste do programa quando estamos com os eletrodos ligados ao corpo e frequentemente ajustando a programação. Encontramos diversas recomendações de uso de notebook durante o desenvolvimento (ao invés de desktop), com a fonte desconectada da energia para minimizar o risco mencionado.
  • O AD8232 não nos fornece o BPM diretamente, teremos que determinar achando-se o intervalo de tempo entre picos e calculando-se a média móvel entre um número fixo de leituras (MEDIA_MOVEL_SIZE), isso para suavizar a interpolação. Para achar o início de um pico característico, utilizaremos a parametrização (GATILHO_PICO) que pode ser ajustada no FORM no html. O GATILHO_PICO será mostrado no gráfico numa linha vermelha. Adicionalmente, respeitaremos um intervalo de tempo em mseg após a detecção do pico (INTERVALOPEAKTIME) para não considerarmos picos intermediários que normalmente a curva apresenta. A Referência 2 mostra um artigo da área médica onde a utilização do AD8232 é comparada com equipamentos de ECG de hospitais.
  • O acesso ao aplicativo pode ser através do IP adquirido pelo ESP32 na Rede, mas para isso é necessário usar algum aplicativo de rede para listar os IP’s dos equipamentos conectados num dado momento (ferramentas como o FING). Como forma alternativa para facilitar, o programa adiciona um nome DNS usando as API’s do mDNS. O nome desejado deve ser definido como parâmetro no WiFiManager descrito anteriormente. O ideal que seja um nome sem acentuação e sem caracteres especiais. A URL http://<nome definido>.local pode ser utilizada para acesso à página principal. No MAC, iOS e Windows o uso de mDNS funciona perfeitamente, mas no Android ainda não, embora haja uma promessa da Google em implementar esta funcionalidade. Uma solução de contorno para o acesso via Android, seria usar um desktop para dar um ping <nome definido>.local e descobrir o IP que poderá ser usado no Android. Há uma menção em alguns sites da Internet dizendo que nas versões mais atualizadas do Android o mDNS já está funcionando. Não pude comprovar pois o meu celular não é o último modelo. O nome DNS default que a aplicação utiliza é ecg ou http://ecg.local.
  • O botão ECG ON é usado para ativar a coleta de valores do sensor e plotagem do gráfico. Depois de acionado, ele passa a ter a função de ECG OFF para terminar a coleta. O tempo de duração da coleta fica à critério do usuário que pode se orientar pelo tempo em segundos do Eixo X para decidir quando interromper a coleta.
  • O botão Config permite a definição de parâmetros que afetam a medição (GATILHO_PICO, INTERVALOPEAKTIME, MAX_PONTOS e MUTE), conforme explicado anteriormente. Existe o terceiro parâmetro que é usado para o gráfico com um número fixo de pontos na resolução horizontal do Navegador. No desktop, o gráfico fica mais bem apresentado por causa da maior resolução horizontal. O quarto parâmetro é o MUTE. O usuário deve desmarcar caso queira ouvir o som correspondente com a variação da curva ECG no Navegador. A seguir, apresentamos telas do aplicativo:

Figura 15 – Tela Principal do ECG

Figura 16 – Tela de Definição de Parâmetros

 

Figura 17 – Tela do Gráfico ECG no Celular

Figura 18 – Tela do Gráfico ECG no Desktop

  • Os parâmetros são persistidos no formato JSON no filesystem do SPIFFS conforme estrutura a seguir:
{
   "dnsName": "ECG",
   "gatilhoPico": "2000", 
   "usuarioOTA": "admin",
   "senhaOTA": "esp32@ecg",
   "autorebootOTA": true,
   "mute": true,
   "peakTime": "700",
   "maxPontos": "200"
}
  • O botão EXPORTAR CSV permite salvar os dados coletados (tempo em seg e o valor do sensor) que ficam armazenados num GRID usando o recurso de LocalStorage do JavaScript para otimizar o consumo de RAM e melhorar o desempenho do Navegador. Portanto, no grid todos os dados coletados ficam armazenados. Com esta funcionalidade de EXPORTAR para CSV é possível usar os dados em outras ferramentas como o EXCEL ou MATLAB etc. O LocalStorage tem um tamanho máximo de 5MB que corresponde a cerca de 4 horas de coleta contínua.
  • A comunicação entre o ESP32 e o Navegador é feita através de mensagens no formato JSON:

 

JSON Descrição
{
“dnsName”: “ECG”,
“gatilhoPico”: “2000”,
“usuarioOTA”: “admin”,
“senhaOTA”: “esp32@ecg”,
“autorebootOTA”: true,
“mute”: true,
“peakTime”: “700”,
“maxPontos”: “200”
}
Usado para persistir os parâmetros do programa no arquivo config.json no filesystem do SPIFFS.
{

“ecgXValue”:0,

“ecgValue”:1634,

“loPlusState”:1,

“loMinusState”:1,

“heartBeat”:65

}

Usado para enviar os dados da medição do ESP32 para o Navegador.
{

“command”:”ON”

}

Enviado para o ESP32 pelo Navegador para início da medição.
{

“command”:”OFF”

}

Enviado para o ESP32 pelo Navegador para terminar a medição.
{

“success”: true,

“message”: “Configurações recebidas”

}

Enviado do ESP32 para o Navegador quando o FORM de Configuração é confirmado e os dados são recebidos com sucesso.
{

“success”: false,

“message”: “Parâmetros faltando”

}

Enviado do ESP32 para o Navegador quando o FORM de Configuração é confirmado e os dados não estão corretos.

Código Mínimo para usar o Sensor

Para você que deseja iniciar a aventura usando o sensor AD8232, sugerimos o código mínimo a seguir para rodar no ambiente Arduino UNO ou NANO. Desta forma, você pode ir se familiarizando com o sensor e a fixação do eletrodos.

Código Mínimo para o Arduino UNO ou NANO

//-------------------------------------------------------------------
// Código Mínimo para tratar o Módulo AD8232 no Arduino UNO/NANO
// Observação: use Plotter Serial para visualizar a forma da onda
//-------------------------------------------------------------------

#define pinLOmais    10 
#define pinLOmenos   11 
#define pinOutput    A0  

void setup()
{

  // Inicializa a Serial

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

  // Define o modo dos pinos

  pinMode(pinLOmais, INPUT);
  pinMode(pinLOmenos, INPUT);
  
}

void loop()
{

  // Faz as leituras

  if (digitalRead(pinLOmais) == HIGH || digitalRead(pinLOmenos) == HIGH) 
  {
    Serial.println("!");
  }
  else 
  {
    Serial.println(analogRead(pinOutput));
  }
 
  // Breve Delay
   
  delay(10);

}

Figura 19 – Diagrama para o Código Mínimo

Figura 20 – Forma da Onda no Plotter Serial


Diagrama do Circuito Principal

Figura 21 – Diagrama do Circuito Principal


Demonstração do Projeto


Código Principal

//------------------------------------------------------------------------------------------------
// Função    : Este programa tem como objetivo implementar um monitor de ECG (Eletrocardiograma) 
//             utilizando o sensor AD8232 através de um servidor Web escutando na porta 80.
//
// Objetivos Específicos : 
//
//    1) Implementar um servidor http para responder na porta 80 através da conexão WiFi respondendo
//       às seguintes requisições:
//
//       /          => mostrar a página principal para ativação/desativação do monitoramento
//       /config    => mostrar um FORM para definição de parâmetros que interferem na medição.
//       /setConfig => efetivar as mudanças dos parâmetros persistindo no SPIFFS.
//       /update    => para atualizar o firmware via OTA
//
//    2) Atualizar o relógio interno do ESP32CAM sincronizado com o servidor NTP do Brasil.
//
//    3) Inserir um nome DNS para a estação para evitar ter que descobrir o IP e a URL
//       http://<dnsname>.local poderá ser usada para acessar a página principal.
//
//    4) Usar o WiFi Manager para configurar as credenciais da Rede WiFi e parâmetros do programa.
//
//    5) Calcular a frequência cardíaca computando o número de batidas entre picos da onda e 
//       apresentando na interface Web.  
//   
//    6) Representar a frequência cardíaca num componente visual pulsante na mesma frequência na 
//       interface Web.
//
//    7) Reproduzir os batimentos cardíacos na placa de som do Navegador.
//
//    8) Representar também a frequência cardíaca através da luminosidade variável de um led no 
//       circuito durante a medição.
//    
//    9) Armazenar os dados coletados num GRID no html e permitir exportar para CSV.
//
// Componentes : 1) 1 x Placa ESP32CAM 30 pinos
//               2) 1 x Módulo Pulso Cardíaco com Eletrodos AD8232
//               3) 1 x Protoboard 470 pontos
//               4) 1 x Bateria 18650
//               5) 1 x Shield para uma bateria 18650
//               6) 1 x Led Difuso 5mm Verde
//               7) 1 x Resistor 100 Ohms (10 unidades)
//               8) 1 x Kit com 140 Jumpers Rígidos
//
// Autor       : Dailton Menezes         
//
// Versão      : 1.0 Set/2024
//------------------------------------------------------------------------------------------------

//-------------------------------------
// Define das bibliotecas usadas
//-------------------------------------

#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 (deve anteceder a ESPAsyncWebServer)
#include <ESPAsyncWebServer.h>           // Biblioteca Asynch Web Server
#include <time.h>                        // Biblioteca Time para manipulação de data/hora
#include <ArduinoJson.h>                 // Biblioteca JSON para comunicação e parâmetros
#include <ESPmDNS.h>                     // Biblioteca para inclusão do hostname no mDNS 
#include <ElegantOTA.h>                  // Biblioteca para atualização via Web   

//-------------------------------------
// Definições usadas: pinos/constantes
//-------------------------------------

#if defined(CONFIG_IDF_TARGET_ESP32C3)
  #define Output_Pin          A1         // Pino analógico do ECG (VP)
  #define loPlus_LAYellow_Pin 3          // Pino LO+
  #define loMinus_RARed_Pin   2          // Pino LO-
  #define sdn_Pin             4          // Pino SDN
  #define led_Beat_Pin        7          // Pino do LED de Pulsação 
  #define Wifi_Pin            8          // Pino do Led BuiltIn interno para indicar Wifi ON/OFF
  #define Led_Wifi_ON         LOW        // No ESP32C3 LOW ativa
  #define Led_Wifi_OFF        HIGH       // No ESP32C3 HIGH desativa  
  #define Boot_Pin            9          // Pino do botão para forçar a entrada no modo de configuração do WiFi
#elif defined(ESP32)
  #define Output_Pin          A0         // Pino analógico do ECG (VP)
  #define loPlus_LAYellow_Pin 25         // Pino LO+
  #define loMinus_RARed_Pin   26         // Pino LO-
  #define sdn_Pin             27         // Pino SDN
  #define led_Beat_Pin        14         // Pino do LED de Pulsação 
  #define Wifi_Pin            2          // Pino do Led BuiltIn interno para indicar Wifi ON/OFF
  #define Led_Wifi_ON         HIGH       // No ESP32C3 LOW ativa
  #define Led_Wifi_OFF        LOW        // No ESP32C3 HIGH desativa  
  #define Boot_Pin            0          // Pino do botão para forçar a entrada no modo de configuração do WiFi
#endif

#define defaultDNSNAME      "ecg"        // Nome default para DNSNAME/HOSTNAME
#define defaultTituloGraf   "ECG"        // Título default para o Gráfico ECG
#define TEMPO_CLEANUP       15000        // Tempo para limpas possíveis socket perdidos
#define cleanupInterval     10           // Intervalo para saber se os clientes receberam os dados
#define MAX_PONTOS_GRAF     200          // Máximo n. de pontos do gráfico no html
#define GATILHO_PICO        2000         // Valor que caracteriza subida de um Pico
#define ATIVAR              HIGH         // Sinal que ativa o modo normal AD8232
#define DESATIVAR           LOW          // Sinal que coloca AD8232 em standby
#define USER_UPDATE         "admin"      // Usuário para atualização via OTA
#define PASS_UPDATE         "esp32@ecg"  // Senha para atualização via OTA
#define MAX_EDIT_LEN        30           // Tamanho máximo de campos de EDIT
#define MAX_NUM_LEN         4            // Tamanho máximo de campos NUMÈRICO
#define ESP_DRD_USE_SPIFFS  true         // Uso com SPIFFS
#define JSON_CONFIG_FILE "/config.json"  // Arquivo JSON de configuração
#define ESP_getChipId() ((uint32_t)ESP.getEfuseMac() // Simular ID da placa ESP
#define DEFAULT_PASS_AP     "12345678"   // Senha default do modo AP WifiManager
#define MEDIA_MOVEL_SIZE    10           // N. de BPM's a entrar na média móvel
#define VARREDURA           25           // Tempo em mseg para varredura do AD8232
#define INTERVALOPEAKTIME   600          // Tempo em mseg para evitar ruído nos picos

//---------------------------------------------
// Variáveis para controle do OTA
//---------------------------------------------

bool autoRebootOTA     = true;              // Se deve fazer autoreboot após a atualização OTA
char user_OTA[MAX_EDIT_LEN+1]= USER_UPDATE; // Usuário para atualização OTA
char pass_OTA[MAX_EDIT_LEN+1]= PASS_UPDATE; // Senha para atualização OTA
char val_autoreboot[2] = "1";               // AutoRebbot Default 

//-------------------------------
// Definições Gerais do Programa
//-------------------------------

AsyncWebServer server(80);               // Servidor http na porta 80
AsyncWebSocket ws("/ws");                // Socket para fazer comunicação com o html
bool sensorActive = false;               // Estado do sensor
unsigned long lastCleanup=0;             // Momento do último cleanup
int numero_medicoes=0;                   // Número de medições enviadas
int sensorValue=0;                       // Valor lido do sensor AD8232
float ecgXValue=0.0;                     // Valor da abcissa para a leitura
int loPlusState=0;                       // Valor lido da porta LO+
int loMinusState=0;                      // Valor lido da porta LO-
float BPM=0;                             // Valor calculado do BTM
int nBPM=0;                              // N. de ciclos entre picos
bool mute=true;                          // Estado do Som no html
unsigned long currentMillis=0;           // Momento da atual leitura
unsigned long lastMillis=0;              // Momento da leitura anterior a atual 
unsigned long lastVarredura=0;           // Momento da última varredura do AD8232
unsigned long lastPeakTime=0;            // Última detecção de PeakTime
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
char esp_id[50];                         // Id do ESP32
time_t startup;                          // Horário da inicialização
volatile bool buttonState = false;       // Estado do botão Boot para Reconfiguração do WiFi
JsonDocument dbParm;                     // JSON de dados dos Parãmetros

//-------------------------------
// Definições para Média Móvel
//-------------------------------
  
float beats[MEDIA_MOVEL_SIZE];           // Vetor para cálculo da média móvel
int beatIndex=0;                         // Índice para o vetor 
float totalMovel=0;                      // Valor total para a Média Móvel

//-------------------------------
// Definições para o Servidor NTP
//-------------------------------

const char* NTP_SERVER = "a.st1.ntp.br";               // Dados do Servidor NTP do Brasil
//const char* TZ_INFO  = "BRST+3BRDT+2,M10.3.0,M2.3.0";// Informações do Timezone do Brasil
const char* TZ_INFO    = "<-03>3";                     // Fuso Horário do Brasil em relação ao GNT

//---------------------------------------------
// Variáveis para controle do WifiManager
//---------------------------------------------

WiFiManager wm;                             // Define o Objeto WiFiManager
bool shouldSaveConfig = false;              // Flag se deve persistir os parãmetros
char dnsName[MAX_EDIT_LEN+1]=defaultDNSNAME;// Nome default para DNS NAME
char txtGatilho[MAX_NUM_LEN+1]="2000";      // Valor gatilho da Interface 
char txtPeakTime[MAX_NUM_LEN+1]="700";      // Valor intervalo da Interface 
char txtMaxPontos[MAX_NUM_LEN+1]="200";     // Valor so Máximo N. de Pontos do Gráfico
int gatilhoPico=GATILHO_PICO;               // Valor gatilho numérico
int peakTime=INTERVALOPEAKTIME;             // Intervalo entre Picos em mseg
int maxPontos=MAX_PONTOS_GRAF;              // Percentaul da resolução horizontal para n. de pontos do gráfico
char val_mute[2]="1";                       // Valor do mute na Interface (true)
char ssid_config[MAX_EDIT_LEN+1];           // SSID para o modo  AP de Configuração 
char pass_config[] = DEFAULT_PASS_AP;       // Senha para o modo AP de Configuração
WiFiManagerParameter custom_dnsname("DNS", "Informe o Nome DNS (< 30)", dnsName, MAX_EDIT_LEN);                       // Parâmetro Nome DNS
WiFiManagerParameter custom_gatilho("Gatilho", "Informe o valor do GATILHO (< 4095)", txtGatilho, MAX_NUM_LEN);       // Parâmetro Gatilho 
WiFiManagerParameter custom_peaktime("PeakTime", "Informe o intervalo entre Picos (< 1000 mseg)", txtPeakTime, MAX_NUM_LEN);// Parâmetro IntervaloPeakTime 
WiFiManagerParameter custom_maxpontos("MaxPontos", "Informe Max N. de Pontos (< 1000)", txtMaxPontos, MAX_NUM_LEN);   // Parâmetro Max N. de Pontos
WiFiManagerParameter custom_mute("Mute", "Som ativo ou mudo (0 ou 1)", val_mute, 1);                                  // Parâmetro Mute
WiFiManagerParameter custom_user_ota("Usuario", "Informe o Usuário para Atualizações (< 30)", user_OTA, MAX_EDIT_LEN);// Parâmetro Nome  do Usuário OTA
WiFiManagerParameter custom_pass_ota("SenhaOTA", "Informe a Senha para Atualizações (< 30)", pass_OTA, MAX_EDIT_LEN); // Parâmetro Senha do Usuário OTA
WiFiManagerParameter custom_autoreboot_ota("AutoReboot", "AutoReboot após Atualizações (0 ou 1)", val_autoreboot, 1); // Parâmetro AutoReboot OTA

//-------------------------------
// Definição do HTML do ECG
//-------------------------------

const char index_html[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html lang='pt-br'>
<head>
  <title>Heart Beat Monitor</title>
  <meta charset='UTF-8'>
  <meta http-equiv='X-UA-Compatible' content='IE=edge'>
  <meta name='viewport' content='width=device-width, initial-scale=1.0, user-scalable=no'>
  <style>
     header { background-color: #333; color: #fff; text-align: center; padding: 1px; font-size: 16px;}
     button {width: 140px; margin: 5px; padding: 10px; font-size: 16px; border: none; border-radius: 5px; cursor: pointer;}
     .btn-toggle { background-color: #4CAF50; color: white; padding: 10px 20px; border: none; cursor: pointer; }
     .btn-update { background-color: #0074E4; color: white; padding: 10px 20px; border: none; cursor: pointer; }
     .btn-export { background-color: #0074E4; color: white; padding: 10px 20px; border: none; cursor: pointer; } /* Botão Exportar menor */
     .heart { width: 100px; height: 100px; background-color: red; position: relative; margin: 20px auto; border-radius: 50%; display: flex; align-items: center; justify-content: center; color: white; font-weight: bold; font-size: 16px; animation: none; }
     .alert { color: red; font-weight: bold; margin-top: 20px; }
     @keyframes pulse { 0% { transform: scale(1); } 50% { transform: scale(1.1); } 100% { transform: scale(1); } }
     table { margin-top: 20px; width: 100%; border-collapse: collapse; }
     th, td { border: 1px solid #333; padding: 8px; text-align: center; }
     th { background-color: #f2f2f2; }
     .scroll-table { height: 150px; overflow-y: auto; border: 1px solid #333; }
     canvas { margin-top: 20px; }
  </style>
  <script src='https://cdn.jsdelivr.net/npm/chart.js'></script>
  <script src='https://cdn.jsdelivr.net/npm/chartjs-plugin-annotation@1.0.2'></script>
</head>
<body>
  <header><h2>Eletrocardiograma Web</h2></header>
  <canvas id='ecgChart' width='400' height='200'></canvas>
  <div class='heart'>-- BPM</div>
  <center>
  <div id='alert' class='alert'></div>
  <br>
  <button id='toggleButton' class='btn-toggle'>ECG ON</button>
  <button id='configButton' onclick="acaoBotao('/config')" class='btn-toggle'>Config</button>
  <button id='updateButton' onclick="acaoBotao('/update')" class='btn-update'>Atualizar</button>
  <button id='exportCSV' class='btn-export'>Exportar CSV</button>
  
  <div class="scroll-table">
    <table id="dataTable">
      <thead>
        <tr><th>Tempo (s)</th><th>Valor Lido</th></tr>
      </thead>
      <tbody id="table-body"></tbody>
    </table>
  </div>
  </center>
  
  <script>
    var MAX_PONTOS = %maxpontos%;
    let db;

    // Função para limpar o Grid/Tbody
    function clearGrid()
    {
       // Seleciona o elemento tbody pelo ID
       var tbody = document.getElementById('table-body');
      
       // Remove todos os filhos do tbody
       while (tbody.firstChild) 
       {
          tbody.removeChild(tbody.firstChild);
       }       
    }

    // Insere o ponto (x,y) no LocalStorage
    function savePointToLocalStorage(time, value) {
        let ecgData = JSON.parse(localStorage.getItem('ecgData')) || [];
        
        console.log("Salvando ponto no LocalStorage:", time, value);  // Log de depuração
    
        ecgData.push({ time: parseFloat(time), value: parseFloat(value) });
    
        localStorage.setItem('ecgData', JSON.stringify(ecgData));
    
        console.log("LocalStorage atual:", ecgData);  // Log de depuração
    }

    // Recupera os pontos do gráfico salvos no LocalStorage
    function getPointsFromLocalStorage() {
        let ecgData = JSON.parse(localStorage.getItem('ecgData')) || [];
        console.log("Recuperando dados do LocalStorage:", ecgData);  // Log de depuração
        return ecgData;
    }

    // Limpa p LocalStorage
    function clearLocalStorage() {
        console.log("Limpando LocalStorage no carregamento da página."); // Log para verificar
        localStorage.removeItem('ecgData');
    }

    // Exibe os pontos em uma tabela
    function addRowToTable(time, value) {
        let tableBody = document.querySelector("#dataTable tbody");
        let row = document.createElement("tr");
        //row.innerHTML = `<td>${time.toFixed(2)}</td><td>${value}</td>`;
        row.innerHTML = `<td>${time.toFixed(2)}</td><td>${value.toFixed(2)}</td>`;
        tableBody.appendChild(row);
    }

    // Exporta os pontos armazenados no LocalStorage para um CSV
    function exportToCSV() {
        let ecgData = getPointsFromLocalStorage();
    
        console.log("Exportando dados do LocalStorage:", ecgData);  // Log de depuração
    
        if (ecgData.length === 0) {
            alert("Nenhum dado disponível para exportação.");
            return;
        }
    
        let csvContent = "Tempo (s);Valor Lido\n";
        
        ecgData.forEach(function(row) {
            if (row.time !== undefined && row.value !== undefined) {
                console.log("Exportando ponto:", row);  // Log de depuração por ponto
                csvContent += row.time.toFixed(2).replace('.', ',') + ";" + row.value.toFixed(2).replace('.', ',') + "\n";
            } else {
                console.log("Ponto inválido detectado:", row);  // Log para ponto inválido
            }
        });
    
        let fileName = prompt("Digite o nome do arquivo CSV:", "ecg_data.csv");
        if (fileName) {
            let encodedUri = encodeURI("data:text/csv;charset=utf-8," + csvContent);
            let link = document.createElement("a");
            link.setAttribute("href", encodedUri);
            link.setAttribute("download", fileName);
            document.body.appendChild(link);
            link.click();
            document.body.removeChild(link);
        }
    }

    var ws;
    var sensorActive = false;
    var audioCtx = new (window.AudioContext || window.webkitAudioContext)();
    var beepTimeout;
    var isMuted = %valMute%; // Variável para controlar o estado de mute

    // Função para configurar o gráfico
    var ctx = document.getElementById('ecgChart').getContext('2d');
    var ecgChart = new Chart(ctx, {
        type: 'line',
        data: {
          labels: [],
          datasets: [{
            label: 'ECG',
            data: [],
            borderColor: 'rgba(75, 192, 192, 1)',
            borderWidth: 1,
            pointRadius: 0
          }]
        },
        options: {
          animation: {
              duration: 0  // Define a duração da animação como 0 para desativá-la
          },          
          scales: {
            x: { 
                 display: true, 
                 type: 'linear', 
                 position: 'bottom', 
                 ticks: { 
                   stepSize: 1,       // Define o intervalo constante entre as labels
                 },                 
                 title: { display: true, text: 'Tempo (segundos)' }
            },
            y: { beginAtZero: false }
          },
          plugins: {
            annotation: {
              annotations: {
                line1: {
                  type: 'line',
                  yMin: %gatilho%,  // Substitua %gatilho% pelo valor desejado
                  yMax: %gatilho%,
                  borderColor: 'rgb(255, 99, 132)',
                  borderWidth: 3
                }
              }
            }
          }
        }
    });

    // Função para tratar a inicialização do WebSocket
    function initWebSocket() {
      ws = new WebSocket('ws://%iplocal%/ws');
      ws.onopen = function() {
        ws.send(JSON.stringify({ command: 'ON' }));
        console.log('WebSocket conectado');
        //initDatabase();  // Inicializa o banco de dados após a conexão com o WebSocket
        clearLocalStorage();  // Limpa os dados anteriores
      };
      
      ws.onmessage = function(event) {
        var data = JSON.parse(event.data);
        console.log('Dados recebidos do WebSocket:', data);
        if (data.command) {
          if (data.command === "OFF") {
            manuallyStop();
          }
        } else {
          handleECGData(data); // Chamando a função de manipulação de dados
        }
      };

      ws.onerror = function(event) {
        console.error('Erro no WebSocket:', event);
      };
      
      ws.onclose = function(event) {
        console.log('WebSocket fechado:', event);
      };
    }

    // Função para controlar o número de pontos máximo do gráfico
    function handleECGData(data) {
        if (parseInt(data.loPlusState)==1 || parseInt(data.loMinusState)==1)
        {
           updateAlert(parseInt(data.loPlusState), parseInt(data.loMinusState));
           return;
        }
        if (sensorActive) {
            console.log('Processando dados:', data);
            if (ecgChart.data.labels.length >= MAX_PONTOS) {
                ecgChart.data.labels.shift();
                ecgChart.data.datasets[0].data.shift();
            }
            ecgChart.data.labels.push(data.ecgXValue.toFixed(2));
            ecgChart.data.datasets[0].data.push({
                x: parseFloat(data.ecgXValue),
                y: parseFloat(data.ecgValue)
            });
            ecgChart.update();
            console.log('Gráfico atualizado');

            // Atualiza o Grid
            addRowToTable(parseFloat(data.ecgXValue), parseFloat(data.ecgValue));
            
            // Armazena os dados no LocalStorage
            savePointToLocalStorage(parseFloat(data.ecgXValue), parseFloat(data.ecgValue));            

            if (data.heartBeat > 0)
            {
              document.querySelector('.heart').textContent = data.heartBeat.toFixed(0) + ' BPM';
              if (!isMuted) 
              {
                 clearTimeout(beepTimeout); // Limpa o timeout anterior
                 playBeep(data.heartBeat); // Toca o beep com o novo BPM            
              }          
            }
        }
    }

    // Função para parar manualmente a coleta de dados
    function manuallyStop() {
      setTimeout(function() {
        var btn = document.getElementById('toggleButton');
        btn.innerHTML = 'ECG ON'; // Ajusta o texto do botão para 'Ativar Sensor'
        sensorActive = false;
        var heart = document.querySelector('.heart');
        heart.style.animation = 'none'; // Para a animação do pulsante
        if (ws) ws.close(); // Fecha a conexão WebSocket, se estiver aberta
        console.log('Sensor parado');
      }, 0); // Atraso mínimo para forçar atualização do DOM
    }

    // Função para tratar a atualização da mensagem sobre os eletrodos
    function updateAlert(loPlusState, loMinusState) {
      // Log os valores no console do navegador
      console.log("loPlusState:", loPlusState, "loMinusState:", loMinusState);         
      var alertDiv = document.getElementById('alert');
      alertDiv.innerHTML = '';
      if (loPlusState === 1 && loMinusState === 1) {
        alertDiv.innerHTML = 'LO+ e LO- estão desconectados';
      } else if (loPlusState === 1) {
        alertDiv.innerHTML = 'Eletrodo LO+ desconectado';
      } else if (loMinusState === 1) {
        alertDiv.innerHTML = 'Eletrodo LO- desconectado';
      }
    }

    // Função para tocar o BEEP a partir do BPM
    function playBeep(bpm) {
      if (isMuted) return; // Não toca o beep se estiver mutado
      
      if (audioCtx.state === 'suspended') {
          audioCtx.resume();
      }
  
      var oscillator = audioCtx.createOscillator();
      oscillator.type = 'sine'; // Tipo de onda do som
      oscillator.frequency.setValueAtTime(440, audioCtx.currentTime); // Frequência em Hz
  
      var gainNode = audioCtx.createGain();
      gainNode.gain.setValueAtTime(0, audioCtx.currentTime); // Inicia mudo
      gainNode.gain.linearRampToValueAtTime(1, audioCtx.currentTime + 0.01); // Aumenta o volume rapidamente
      gainNode.gain.linearRampToValueAtTime(0, audioCtx.currentTime + 0.2); // Desvanece rapidamente
  
      oscillator.connect(gainNode);
      gainNode.connect(audioCtx.destination);
  
      oscillator.start();
      oscillator.stop(audioCtx.currentTime + 0.2); // Para o som após 200 ms
  
      // Calcula o próximo intervalo de beep com base no BPM
      var interval = 60 / bpm * 1000; // Converte BPM para intervalo em milissegundos
      beepTimeout = setTimeout(function() {
          playBeep(bpm); // Toca o próximo beep baseado no BPM atual
      }, interval);
    }

    // Função para tratar a ação de botões
    function acaoBotao(acao) {
       if (ws && ws.readyState === WebSocket.OPEN) {
          ws.send(JSON.stringify({ command: 'OFF' }));
          manuallyStop();  
       }
       window.location.href = acao;
    }

    // Adicionando event listener para o botão toggleButton
    document.getElementById('toggleButton').addEventListener('click', function() {
      sensorActive = !sensorActive;
      if (sensorActive) {
        initWebSocket();
        document.querySelector('.heart').style.animation = 'pulse 1s infinite';
        this.innerHTML = 'ECG OFF'; // Muda o texto para Desativar quando ativo
        // Zera o gráfico antes de começar a coletar novos dados
        ecgChart.data.labels = []; // Limpa os rótulos do gráfico
        ecgChart.data.datasets.forEach((dataset) => {
            dataset.data = []; // Limpa os dados do gráfico
        });
        ecgChart.update();     // Atualiza o gráfico para refletir a limpeza  
        clearGrid();           // Limpa o grid/tbody
        clearLocalStorage();   // Limpa o -LocalStorage  
      } else {
        ws.send(JSON.stringify({ command: 'OFF' }));
        manuallyStop();
      }
    });

    // Adicionando event listener para o botão ExportCSV
    document.getElementById("exportCSV").addEventListener("click", function() {
        // Exporta para CSV
        exportToCSV();
    });

    // Adicionando event para o Load da Página
    window.onload = function() {
        clearLocalStorage();  // Limpa qualquer dado antigo do LocalStorage
    };

    // Adicionando event para a desativação da página
    window.addEventListener('beforeunload', function() {
      if (ws) ws.close();
    });
    
  </script>
</body>
</html>
)rawliteral";

//-------------------------------
// Definição do HTML do Config
//-------------------------------

const char config_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 name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
    <title>Configurações do Monitor Cardíaco</title>
    <style>
        body { font-family: Arial, sans-serif; margin: 0; padding: 0; background-color: #f4f4f4;}
        header { background-color: #333; color: #fff; text-align: center; padding: 1px; font-size: 16px;}
        main { display: flex; flex-direction: column; align-items: center; padding: 20px;}
        form {font-size: 16px; color: #444444;}
        label { display: block; margin-top: 20px; }
        input[type="text"], input[type="number"] { width: 280px; font-size: 16px; color: #444444; }
        input[type="checkbox"] { font-size: 16px; }
        button {width: 280px; margin: 5px; padding: 10px; font-size: 16px; background-color: #4CAF50; color: #fff; border: none; border-radius: 5px; cursor: pointer;}
        .response { margin-top: 20px; color: #d32f2f; font-weight: bold; }
    </style>
</head>
<body>
    <div class="container">
        <header>
            <h2>Configurações do ECG</h2>
        </header>
        <center>
            <main>
                <form id="settingsForm" action="/setConfig" method="POST">
                    <label for="gatilho">Gatilho:</label>
                    <input type="number" id="gatilho" name="gatilho" value="%gatilho%" required min="1" max="4095">
                    <label for="peaktime">PeakTime (ms):</label>
                    <input type="number" id="peaktime" name="peaktime" value="%peaktime%" required min="1" max="1000">
                    <label for="maxpontos">Max. N. Pontos do Gráfico:</label>
                    <input type="number" id="maxpontos" name="maxpontos" value="%maxpontos%" required min="1" max="1000">
                    <label><input type="checkbox" id="mute" name="mute" %mute%> Mute</label>
                    <br>
                    <button type="submit">Confirmar</button>
                    <br>
                    <button type="button" onclick="location.href='/'">Retornar</button>
                    <div class="response" id="response"></div>
                </form>
            </main>
        </center>
    </div>

    <script>
        document.getElementById('settingsForm').addEventListener('submit', function(event) {
            event.preventDefault();
    
            // Obter o checkbox e ajustar o valor conforme necessário
            var muteCheckbox = document.getElementById('mute');
            muteCheckbox.value = muteCheckbox.checked ? "true" : "false";
    
            // Prepara o formData com o valor atualizado do checkbox
            const formData = new FormData(this);
            
            // Adiciona manualmente o valor do checkbox, porque FormData não inclui checkbox desmarcado
            formData.set('mute', muteCheckbox.value);
    
            fetch(this.action, {
                method: this.method,
                body: formData
            })
            .then(response => response.json()) // Assume que a resposta é um JSON
            .then(data => {
                if (data.success) {
                    document.getElementById('response').textContent = "Configurações atualizadas com sucesso!";
                    document.getElementById('response').style.color = "green";
                } else {
                    document.getElementById('response').textContent = "Erro ao atualizar configurações: " + data.message;
                    document.getElementById('response').style.color = "red";
                }
            })
            .catch(error => {
                document.getElementById('response').textContent = "Falha na comunicação com o servidor.";
                document.getElementById('response').style.color = "red";
                console.error('Error:', error);
            });
        });
    </script>

</body>
</html>
)rawliteral";

//-------------------------------------
// Define o JSON Default dos Parâmetros
//-------------------------------------

const char dbDefault[] PROGMEM = R"(
{
   "dnsName": "ECG",
   "gatilhoPico": "2000", 
   "usuarioOTA": "admin",
   "senhaOTA": "esp32@ecg",
   "autorebootOTA": true,
   "mute": true,
   "peakTime": "700",
   "maxPontos": "200"
})";

//--------------------------------
// Prototipação das funções usadas
//--------------------------------

void WiFiEvent(WiFiEvent_t event);   // Trata os eventos do Wifi
bool getNTPtime(int sec);            // Sincroniza o relógio interno com o servidor NTP
String timeToString(time_t tempo);   // Formata uma variável time para string
String getTimeStamp();               // Obtém a data no formato dd/mm/yyyy hh:mm:ss
void displayRequest(AsyncWebServerRequest *request);  // Mostra informações da requisição na Console
bool setDNSNAME(String nome);        // Define o HostName como DNSNAME
String expandeHtml(String html);     // Expande o HTML de uma forma personalizada
void sendData(float ecgValue, int loPlusState, int loMinusState, int heartBeat); // Envia dados em JSON via WebSockect
void sendCommand(String command);    // Envia comando ON ou OFF em JSON via WebSockect
void saveConfigFile();               // Persiste parâmetros no SPIFFS do ESP32   
bool loadConfigFile();               // Recupera parâmetros do SPIFFS do ESP32
void saveConfigCallback();           // Callback para informação do processo de configuração WiFi
void configModeCallback(WiFiManager *myWiFiManager); // Callback para WifiManager 
void Check_WiFiManager(bool forceConfig); // Inicialização/Configuração  WiFi Manager no ESP32
void buttonISR();                    // Rotina de Tratamento da Interrupção do Botão Boot
float movelBPM(float bpm);           // Rotina para calcular a média móvel do BPM 

//---------------------------------------------
// Inicialização do Programa e recursos usados
//---------------------------------------------

void setup() 
{
   // Inicializa a Serial
   
   Serial.begin(115200);
   while (!Serial);

   // Inicializa os Pinos usados
   
   pinMode(loPlus_LAYellow_Pin, INPUT);
   pinMode(loMinus_RARed_Pin, INPUT);
   pinMode(sdn_Pin, OUTPUT);
   pinMode(led_Beat_Pin, OUTPUT); 
   pinMode(Wifi_Pin, OUTPUT);
   pinMode(Boot_Pin, INPUT_PULLUP);

   // Inicializa o estado do sensor ECG (inativo)
   
   digitalWrite(sdn_Pin, DESATIVAR); // Desativa o sensor inicialmente

   // Define o CALLBACK do modo CONFIG com alteração
  
   wm.setSaveConfigCallback(saveConfigCallback);
 
   // Define o CALLBACK do modo CONFIG
  
   wm.setAPCallback(configModeCallback);
 
   // Adiciona os campos de parâmetros no MENU do WifiManager
  
   wm.addParameter(&custom_dnsname);
   wm.addParameter(&custom_gatilho); 
   wm.addParameter(&custom_peaktime); 
   wm.addParameter(&custom_maxpontos); 
   wm.addParameter(&custom_mute); 
   wm.addParameter(&custom_user_ota);
   wm.addParameter(&custom_pass_ota); 
   wm.addParameter(&custom_autoreboot_ota);

   // Define o handle para tratar os eventos do Wifi

   WiFi.onEvent(WiFiEvent);   

   // Configura a interrupção para detectar a borda de descida do botão Boot

   attachInterrupt(digitalPinToInterrupt(Boot_Pin), buttonISR, FALLING); 

   // 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(!wm.getWiFiIsSaved());

   // Verifica se está conectado na Internet

   if (WiFi.status() == WL_CONNECTED)
   { 
      // 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");  
  
      // 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("\nTimer interno não foi sincronizado");
         //ESP.restart();
      }

      // Define o HostName para o servidor web para facilitar o acesso na rede local
      // sem conhecer o IP previamente
  
      Serial.print("Adicionando " + String(dnsName) + " no MDNS... ");
      if (setDNSNAME(dnsName))
      {
          Serial.println("adicionado corretamente no MDNS!");
      }
      else 
      {
         Serial.println("Erro ao adicionar no MDNS!");
      }     

   }
   
   // Define uma página inicial com links para listagem, transferência e acionamento do buzzer.
  
   server.on("/", HTTP_GET, [](AsyncWebServerRequest *request)
   {
      // Atende a requisição principal

      displayRequest(request); 
      request->send(200, "text/html", expandeHtml(index_html));
   });

   // Define uma página de configuração dos parâmetros do ECG.
  
   server.on("/config", HTTP_GET, [](AsyncWebServerRequest *request)
   {
      // Atende a requisição principal

      displayRequest(request); 
      request->send(200, "text/html", expandeHtml(config_html));
   });

   // Configura o endpoint para receber os dados de configuração
   
   server.on("/setConfig", HTTP_POST, [](AsyncWebServerRequest *request) {
     // Verifica se todos os parâmetros esperados foram recebidos

     mute = false; 
     if (request->hasParam("mute", true)) 
     {
        AsyncWebParameter* p = request->getParam("mute", true);
        mute = p->value().equalsIgnoreCase("true");
     }
     
     if (request->hasParam("gatilho", true) && request->hasParam("peaktime", true) && request->hasParam("maxpontos", true)) 
     {
        AsyncWebParameter* p = request->getParam("gatilho", true);
        gatilhoPico = p->value().toInt();

        p = request->getParam("peaktime", true);
        peakTime = p->value().toInt();

        p = request->getParam("maxpontos", true);
        maxPontos = p->value().toInt();

        // Processa os dados recebidos
        
        Serial.println("Recebido via POST:");
        Serial.println("Gatilho: " + String(gatilhoPico));
        Serial.println("PeakTime: " + String(peakTime));
        Serial.println("Max Pontos: " + String(maxPontos));
        Serial.println("Mute: " + String(mute));

        // Persiste no SPIFFS
        
        saveConfigFile();

        // Responde ao cliente
        
        request->send(200, "application/json", "{\"success\":true, \"message\":\"Configurações recebidas\"}");
     } 
     else 
     {
        request->send(400, "application/json", "{\"success\":false, \"message\":\"Parâmetros faltando\"}");
     }
  });

   // Define uma página para links não encontrados
  
   server.onNotFound([](AsyncWebServerRequest *request)
   {
      // Atende a requisição NOT FOUND
  
      Serial.println("Requisição não encontrada");
      displayRequest(request);
  
      // Retorna a mensagem de erro em caso de um retorno 404
  
      request->send(404, "text/html", "<h1>Erro: Requisi&ccedil;&atilde;o n&atilde;o encontrada</h1>");
   });

   // Inicializa o WebSocket Event para conectar/desconectar clientes de streaming
  
   ws.onEvent([](AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len) 
   {
      // Registra na Console a conexão e desconexão de clientes socket's 

      if (type == WS_EVT_CONNECT) 
      {
        Serial.printf("Cliente Socket Id=%d conectado no IP %s\n",client->id(),client->remoteIP().toString());
      } 
      else if (type == WS_EVT_DISCONNECT) 
      {
         Serial.printf("Cliente Socket Id=%d desconectado do IP %s\n",client->id(),client->remoteIP().toString());
         Serial.println(client->remoteIP()); 
      }
      else if (type == WS_EVT_DATA) 
      {
         data[len] = '\0';
         String msg = String((char*)data);
         Serial.print("Recebido: ");
         Serial.println(msg);

         analogWrite(led_Beat_Pin, 0); // Desativa o LED 

         // Deserializar o JSON
         
         JsonDocument doc;
         DeserializationError error = deserializeJson(doc, msg);
    
         if (error) {
            Serial.println("Failed to parse JSON!");
            return;
         }
    
         // Verificar se o comando está presente
         
         if (doc.containsKey("command")) {
            String command = doc["command"].as<String>();
    
            if (command.equalsIgnoreCase("ON")) 
            {
               // Zera todos os elementos do vetor
               memset(beats, 0, sizeof(beats));
               beatIndex=0;
               totalMovel=0.0;
               ecgXValue=0.0;
               lastMillis=0;
               Serial.println("Activating sensor...");
               sensorActive = true;
               digitalWrite(sdn_Pin, ATIVAR); // Ativa o sensor
            }
            else if (command.equalsIgnoreCase("OFF")) 
            {
               sensorActive = false;
               Serial.println("Deactivating sensor...");
               digitalWrite(sdn_Pin, DESATIVAR);  // Desativa o sensor
               digitalWrite(led_Beat_Pin, LOW);   // Desativa o LED
               sendCommand("OFF");
            }
         }

      }   

   });  

   // Credenciais para atualizações via OTA
   
   ElegantOTA.setAuth(user_OTA,pass_OTA);

   // Habilita/Desabilita AutoRebbot após a atualização
   
   ElegantOTA.setAutoReboot(autoRebootOTA);   

   // Inicia o OTA para atualização via Web

   ElegantOTA.begin(&server);      

   // Inicializa o Serviço Web
     
   server.addHandler(&ws);
   server.begin();

   // Pega a hora do startup

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

   // Obtém o id do ESP32CAM

   sprintf(esp_id, "%X",(uint32_t)ESP.getEfuseMac());

   // Mostra o status de algumas variáveis no startup

   Serial.print("Esp32 Serial  = "); Serial.println(esp_id);
   Serial.print("Inicialização = "); Serial.println(timeToString(startup));
   Serial.print("Data/Hora     = "); Serial.println(getTimeStamp());

   // Aguardando request http na porta 80

   Serial.println("\nAguardando requisições http na porta 80...");
   Serial.println("Use http://" + String(dnsName) + ".local no seu navegador...");  
   Serial.println("Ou opcionalmente...");      
   Serial.println("Use http://" + WiFi.localIP().toString() + " no seu navegador...\n");  
   
}

//----------------------------------------------
// Loop principal esperando mensagens web socket 
// para ECG, Configuração do WifiManager ou OTA
//----------------------------------------------

void loop() 
{

   // Verifica se deve fazer um cleanup das conexões perdidas

   if (millis() - lastCleanup > TEMPO_CLEANUP)
   {
      ws.cleanupClients();
      if (ws.count()==0) sensorActive = false; 
      lastCleanup = millis();
   }

   // Verifica se deve coletar dados do AD8232 e enviar via Web Socket

   if (ws.count() > 0 && sensorActive && (millis()-lastVarredura>VARREDURA))
   {
 
     // Atualiza o número de mediçoes feitas 

     ++numero_medicoes %= MAX_PONTOS_GRAF;         

     // Faz a medição e Mostra valor medido na console

     currentMillis = millis();
     if (lastMillis == 0) lastMillis = currentMillis;
     
     sensorValue = analogRead(Output_Pin);

     ecgXValue += float(currentMillis - lastMillis)/1000.0; 
     lastMillis = currentMillis; 
     Serial.printf("Value[%d]: %d -> BPM=%.2f\n",numero_medicoes,sensorValue,BPM);
     //Serial.printf("%d\t%.2f\n",sensorValue,BPM);
     //Serial.println(sensorValue);

    // Lê o status da portas que controlam os eletrodos

     loPlusState  = digitalRead(loPlus_LAYellow_Pin);
     loMinusState = digitalRead(loMinus_RARed_Pin);

     // Controle do brilho LED interno
     
     int ledBrightness = map(sensorValue, 0, 4095, 0, 255);
     analogWrite(led_Beat_Pin, ledBrightness);

    // Calcula o BPM de forma contínua

//    unsigned long nBeatsCopy;
//
//    noInterrupts();      // Desabilita interrupções para garantir uma leitura segura de beats
//    nBeatsCopy = nBeats;
//    interrupts();        // Habilita interrupções novamente
//    
//    unsigned long elapsedTime = currentMillis - inicioMonitor;
//    //BPM = float(60000.0 * nBeatsCopy / elapsedTime);
//    BPM = float(60000.0 / nBeatsCopy);

     // Encia os dados para o Navegador

     sendData(ecgXValue, sensorValue,loPlusState,loMinusState,BPM); 
     
     // Aguarde a entrega dos dados para todos os clientes
     // Obs: Só chamaremos a rotina cleanupClients a cada cleanupInterval

     int cleanupCounter = 0;
     
     while (ws.count()>0 && !ws.availableForWriteAll())
     {
        cleanupCounter = (cleanupCounter+1) % cleanupInterval;
        if (cleanupCounter == 0) 
        {
           ws.cleanupClients();
        } 
     }       

    // Verifica se os Eletrodos estão bem posicionado

    if (loPlusState==1 || loMinusState==1)
    {
       // Problema com os eletrodos, avisa a interface para notificar/desativar

       nBPM=0;
       BPM=0;
       lastMillis=0;
       sensorActive=false;
       analogWrite(led_Beat_Pin, 0);
       sendCommand("OFF");
       Serial.println("Problema no(s) eletrodo(s)...");
    }
    else if (sensorValue > gatilhoPico) 
    {
       unsigned long currentTime = millis();
       if (currentTime - lastPeakTime > peakTime) {  // intervalo mínimo para evitar ruído
           // Calcula o intervalo entre os picos
           unsigned long peakInterval = currentTime - lastPeakTime;
           lastPeakTime = currentTime;

           // Atualiza o contador de picos
           nBPM++;

           // Calcula o BPM
           BPM = movelBPM(60000.0 / peakInterval);

           // Imprime o BPM para debug
           //Serial.print("Heart Rate: ");
           //Serial.println(heartRate);
       }
    }
    // Atualiza a última varredura

    lastVarredura = millis(); 

  }

  //--------------------------------------------------------------------------------------------------
  // 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;
     Serial.println("Botão BOOT foi pressionado. Entrando no WifiManager...");

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

     wm.resetSettings();   
     ESP.restart();
       
  }  

  // Verifica o OTA para saber se há atualização

  ElegantOTA.loop();   
  
}

//------------------------------------------------
// 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(Wifi_Pin,Led_Wifi_ON);   // Liga o LED BuitIn para mostrar a conexão com WiFi
      break;
    case SYSTEM_EVENT_STA_DISCONNECTED:
      Serial.println("Desconectado do AP WiFi");
      digitalWrite(Wifi_Pin,Led_Wifi_OFF); // Desliga o LED BuitIn para mostrar a desconexão com WiFi
      //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;
  }
}

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

//-------------------------------------------------------
// Formata um variável time_t para string
//-------------------------------------------------------

String timeToString(time_t tempo)
{
   char timestamp[30];
   strftime(timestamp, 30, "%d/%m/%Y %T", localtime(&tempo));  
   return String(timestamp);
}

//------------------------------------------------
// Devolve o localtime dd/mm/aaaa hh:mm:ss
//------------------------------------------------

String getTimeStamp()
{
  time_t now;
  time(&now);
  return String(timeToString(now));
}

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

//------------------------------------------------
// Mostra informações da Requisição 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());    
   
}

//-------------------------------------------------
// Expande o HTML à minha maneira pois o pré-
// processador do C++ usa % como delimitor e no
// HTML já outras ocorrência de % que gerariam erro 
//-------------------------------------------------


String expandeHtml(String html)
{
  html.replace("%iplocal%",WiFi.localIP().toString());
  html.replace("%mute%",mute ? "checked" : "");
  html.replace("%valMute%",mute ? "true" : "false");
  html.replace("%gatilho%",String(gatilhoPico)); 
  html.replace("%maxpontos%",String(maxPontos)); 
  html.replace("%peaktime%",String(peakTime)); 
  //html.replace("%titulograf%",defaultTituloGraf);
  return html;
}

//------------------------------------------------
// Envia os dados coletados via Web Socket e JSON
//------------------------------------------------

void sendData(float ecgXValue, float ecgValue, int loPlusState, int loMinusState, int heartBeat) 
{
  JsonDocument doc;
  doc["ecgXValue"] = ecgXValue;
  doc["ecgValue"] = ecgValue;
  doc["loPlusState"] = loPlusState;
  doc["loMinusState"] = loMinusState;
  if (heartBeat > 0) 
  {  
     doc["heartBeat"] = heartBeat;
  }
  String output;
  serializeJson(doc, output);
  Serial.println(output);
  ws.textAll(output);  
}

//------------------------------------------------
// Envia o comando via Web Socket e JSON
//------------------------------------------------

void sendCommand(String command) 
{
  JsonDocument doc;
  doc["command"] = command;
  String output;
  serializeJson(doc, output);
  ws.textAll(output);
}

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

void saveConfigFile()
// O arquivo de Config é salvo no formato JSON
{
  Serial.println(F("Persistindo a configuração..."));
  
  // Atualiza a base de software e parâmetros gerais
  
  dbParm["dnsName"] = dnsName;
  dbParm["gatilhoPico"] = gatilhoPico;
  dbParm["usuarioOTA"] = user_OTA;
  dbParm["senhaOTA"] = pass_OTA;
  dbParm["autorebootOTA"] = autoRebootOTA;
  dbParm["mute"]=mute;
  dbParm["peakTime"] = peakTime;
  dbParm["maxPontos"] = maxPontos;

  // 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(dbParm, Serial);
  Serial.println();
  if (serializeJson(dbParm, 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...");
    
    //Serial.println("Removendo o arquivo de configuração...");
    //SPIFFS.remove(JSON_CONFIG_FILE);
    
    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...");
        DeserializationError error = deserializeJson(dbParm, configFile);
        
        if (!error)
        {
          Serial.println("JSON do SPIFFS recuperado...");
          serializeJsonPretty(dbParm, Serial);
          Serial.println();          

          // Recupera DNSNAME da interface do WifiManager
 
          if (dbParm.containsKey("dnsName")) strcpy(dnsName, dbParm["dnsName"]);
          else strcpy(dnsName, defaultDNSNAME);

          // Recupera GATILHO da interface do WifiManager
          
          if (dbParm.containsKey("gatilhoPico")) 
          {
             gatilhoPico = dbParm["gatilhoPico"].as<int>();
             if (gatilhoPico<=0 || gatilhoPico >= 4095) 
             {
                gatilhoPico = GATILHO_PICO;
                strcpy(txtGatilho,"2000");
             }
          }
          else 
          {
             gatilhoPico = GATILHO_PICO;
             strcpy(txtGatilho,"2000");
          }

          // Recupera PEAKTIME da interface do WifiManager

          if (dbParm.containsKey("peakTime")) 
          {
             peakTime = dbParm["peakTime"].as<int>();
             if (peakTime<=0 || peakTime >= 1000) 
             {
                peakTime=INTERVALOPEAKTIME;
                strcpy(txtPeakTime,"700");
             }
          }
          else 
          {
             peakTime = INTERVALOPEAKTIME;
             strcpy(txtPeakTime,"700");
          }

          // Recupera MaxPontos da interface do WifiManager
          
          if (dbParm.containsKey("maxPontos")) 
          {
             maxPontos = dbParm["maxPontos"].as<int>();
             if (maxPontos<=0 || maxPontos > 1000) 
             {
                maxPontos = MAX_PONTOS_GRAF;
                strcpy(txtMaxPontos,"200");
             }
          }
          else 
          {
             maxPontos = MAX_PONTOS_GRAF;
             strcpy(txtMaxPontos,"200");
          }

          // Recupera Usuário da interface do WifiManager
          
          if (dbParm.containsKey("usuarioOTA")) strcpy(user_OTA, dbParm["usuarioOTA"]);
          else strcpy(user_OTA, USER_UPDATE);

          // Recupera Senha da interface do WifiManager
          
          if (dbParm.containsKey("senhaOTA")) strcpy(pass_OTA, dbParm["senhaOTA"]);
          else strcpy(pass_OTA, PASS_UPDATE);

          // Recupera AutoReboot da interface do WifiManager
          
          if (dbParm.containsKey("autorebootOTA")) 
          {
             autoRebootOTA = dbParm["autorebootOTA"];
             if (autoRebootOTA) strcpy(val_autoreboot,"1");
             else strcpy(val_autoreboot,"0"); 
          }
          else 
          {
             autoRebootOTA = true;
             strcpy(val_autoreboot,"1");     
          }

          return true;
        }
        else
        {
          // Erro ao ler o JSON
          Serial.println("Erro ao carregar o JSON da configuração...");
        }
      }
    }
    else
    {
       // Monta base default   

       DeserializationError error = deserializeJson(dbParm, dbDefault);
    
       // Verificar se há erro no parsing
       
       if (!error)
       {
          Serial.println("JSON default recuperado...");
          serializeJsonPretty(dbParm, Serial);
          Serial.println();                 
 
          strcpy(dnsName, dbParm["dnsName"]);
          gatilhoPico = dbParm["gatilhoPico"].as<int>();

          // Salva o default no SPIFFS

          saveConfigFile();

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

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

   // Copia os campos para o FORM do WifiManager
  
   custom_dnsname.setValue(dnsName, MAX_EDIT_LEN+1);
   custom_gatilho.setValue(String(gatilhoPico).c_str(), MAX_NUM_LEN+1); 
   custom_peaktime.setValue(String(peakTime).c_str(), MAX_NUM_LEN+1); 
   custom_maxpontos.setValue(String(maxPontos).c_str(), MAX_NUM_LEN+1); 
   strcpy(val_mute,mute ? "1" : "0");
   custom_mute.setValue(val_mute,sizeof(val_mute)); 
   custom_user_ota.setValue(user_OTA, MAX_EDIT_LEN+1);
   custom_pass_ota.setValue(pass_OTA, MAX_EDIT_LEN+1);
   custom_autoreboot_ota.setValue(val_autoreboot,sizeof(val_autoreboot));

   if (forceConfig) 
   { 
      // reseta configurações
      
      wm.resetSettings();   

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

      // Entra no modo de AP de configuração ... com senha fixa
     
      if (!wm.startConfigPortal(ssid_config, pass_config))
      {
         Serial.println("Erro na conexão com timeout no modo AP...");
         //setStateWifiEEPROM(true);
      }
      //else setStateWifiEEPROM(false);
    
   }
   else
   {
      // Entra no modo de conexão normal recuperando o SSID/Senha anteriores
      if (!wm.autoConnect())
      {
         Serial.println("Erro na conexão com timeout...");
      }
      //setStateWifiEEPROM(false);
   }

   // Recupera o campo DNSNAME preenchido na interface do WifiManager
  
   strncpy(dnsName, custom_dnsname.getValue(), sizeof(dnsName));
   if (strlen(dnsName)==0) strcpy(dnsName,defaultDNSNAME);
   Serial.print("dnsName: ");
   Serial.println(dnsName);
 
   // Recupera o campo GATILHO do WifiManager preenchido na interface convertendo para inteiro
  
   gatilhoPico = atoi(custom_gatilho.getValue());
   Serial.print("gatilhoPico: ");
   Serial.println(gatilhoPico);

   // Recupera o campo MAXPONTOS do WifiManager preenchido na interface convertendo para inteiro
  
   maxPontos = atoi(custom_maxpontos.getValue());
   Serial.print("maxPontos: ");
   Serial.println(maxPontos);   

   // Recupera o campo Mute do WifiManager

   strncpy(val_mute, custom_mute.getValue(), sizeof(val_mute));
   Serial.print("Mute: ");
   Serial.println(val_mute);   
   mute = (strcmp(val_mute, "1") == 0) ? true : false;   

   // Recupera o campo USER da Atualização do WifiManager

   strncpy(user_OTA, custom_user_ota.getValue(), sizeof(user_OTA));
   Serial.print("User_OTA: ");
   Serial.println(user_OTA);

   // Recupera o campo senha da Atualização do WifiManager

   strncpy(pass_OTA, custom_pass_ota.getValue(), sizeof(pass_OTA));
   Serial.print("Pass_OTA: ");
   Serial.println(pass_OTA);

   // Recupera o campo AutoReboot da Atualização do WifiManager

   strncpy(val_autoreboot, custom_autoreboot_ota.getValue(), sizeof(val_autoreboot));
   Serial.print("AutoReboot_OTA: ");
   Serial.println(val_autoreboot);   
   autoRebootOTA = (strcmp(val_autoreboot, "1") == 0) ? true : false;

   // Salva os parâmetros no FileSystem FLASH -> não perde quando desligado
  
   if (shouldSaveConfig)
   {
      saveConfigFile();
   } 
  
}

//--------------------------------------------------
// Rotina de Tratamento da Interrupção do Botão Boot
//--------------------------------------------------

void buttonISR() 
{
   buttonState = true;
}

//---------------------------------------------------
// Rotina para fazer a média móvel considerando os
// últimos MEDIA_MOVEL_SIZE=10 valores de BPM calculados
//---------------------------------------------------

float movelBPM(float bpm)
{
  totalMovel -= beats[beatIndex];
  beats[beatIndex] = bpm;
  totalMovel += bpm;
  ++beatIndex %= MEDIA_MOVEL_SIZE;
  float atualBPM = totalMovel / MEDIA_MOVEL_SIZE;
  //Serial.printf("Total=%.2f -> BPM=%.2f\n",totalMovel,atualBPM);
  return atualBPM;
}

Conclusão

Neste projeto, demonstramos como utilizar o sensor AD8232 em conjunto com o ESP32 para monitorar a atividade cardíaca e apresentar os dados em tempo real através de uma interface web interativa. A implementação do servidor web assíncrono permitiu uma comunicação eficiente e responsiva, enquanto o uso de WebSockets garantiu a atualização contínua dos dados no navegador do usuário.

Além disso, integramos funcionalidades visuais e auditivas para representar a frequência cardíaca, proporcionando uma experiência mais intuitiva e informativa. O LED com luminosidade variável e o som pulsante no navegador são exemplos de como os dados biológicos podem ser utilizados para criar feedbacks visuais e auditivos.

Este projeto não só ilustra a versatilidade do ESP32 e do sensor AD8232, mas também abre caminho para futuras expansões e melhorias. Possíveis aprimoramentos incluem a adição de mais sensores para monitoramento de outros sinais vitais (ex: oxímetro como os sensores MAX30100 e MAX30102), a integração com aplicativos móveis via bluetooth (ex: MIT App Inventor Referência 12) e/ou algum IoT Cloud (ex: Ubidots, ThingSpeak, Adafruit IO, Blynk, RainMaker, Google Cloud IoT e MQTT) para maior acessibilidade e a implementação de algoritmos de análise de dados para detectar anomalias cardíacas em conjunto com profissionais de cardiologia.

Esperamos que este projeto inspire outros a explorar o potencial dos sensores biológicos em aplicações de monitoramento de saúde e a desenvolver soluções inovadoras que possam melhorar a qualidade de vida das pessoas.


Sobre o Autor


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

2 de janeiro de 2025

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!

Eletrogate Robô

Assine nossa newsletter e
receba  10% OFF  na sua
primeira compra!