



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:
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
Figura 4 - Posicionamento dos Eletrodos
Detalhes do Valor Analógico
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 WiFiManagerFigura 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.
|
|
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.
Encontre tudo na Loja Eletrogate com frete grátis para compras acima de R$ 200