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

Antes de iniciar a programação, é preciso fazer a instalação das placas da Espressif e das seguintes bibliotecas:
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.
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.
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
Figura 3 – Módulo AD8232

Pinagem do Módulo
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:
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:
Dicas para Melhorar a Conexão
Figura 4 – Posicionamento dos Eletrodos

Detalhes do Valor Analógico
Como Relacionar ao Batimento Cardíaco
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
Passo a passo de como utilizar:
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

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:
Figura 9 – Tela de Definição da Imagem para a atualização

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

Figura 11 – Visão Lateral do Case

Figura 12 – Visão de Cima do Case

Figura 13 – Parâmetros para Compilação

Figura 14 – Geração de Imagem

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

{
"dnsName": "ECG",
"gatilhoPico": "2000",
"usuarioOTA": "admin",
"senhaOTA": "esp32@ecg",
"autorebootOTA": true,
"mute": true,
"peakTime": "700",
"maxPontos": "200"
}
| 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. |
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

Figura 21 – Diagrama do Circuito 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ção nã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;
}
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.
|
|
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!