This tutorial covers how to use ESP32 to monitor the heart rate and to measure the pulse oximetry. This is an interesting project because it shows how we can use the ESP32 with different sensors. To measure the heart rate and pulse oximetry the ESP32 is connected to the MAX30102 sensor. Moreover, we will use the Server Sent Event with ESP32 to update the Web interface. By the way, pulse oximetry is a noninvasive method to measure the oxygen concentration in our blood. In more detail, the pulse oximetry measures the oxygen saturation of the hemoglobin. Often this value is indicated as SPO2. The pulse oximeter is the tool we use to measure the pulse oximetry and it uses our finger to detect the heart-rate and the SPO2. Be aware this project can’t be used to monitor your health status.
Let’s start!
Table of contents
MAX30102 sensor overview
This sensor is a pulse oximetry and heart-rate monitor sensor. It is very small sensor that can be used in several scenario. MAX30102 has an integrated red LED with an infrared LED. It has an I2C interface so it is very easy to use. This sensor has several applications:
- Heart rate monitoring
- SPO2 detection
How to connect ESP32 to heart-rate sensor MAX30102
As said before, this sensor has an I2C interface. Therefore, we need to use 4 wires.This sensor has several pins anyway we don’t need to use them. To meausre the heart-rate and the pulse oximetry, the ESP32 must connect to the MAX30102 in this way:
ESP32 | MAX30102 |
3V3 | Vcc |
GND | GND |
SDA | 23 |
CLK | 21 |
We can connect this sensor to 3V or to you 5V. In this project we will connect the heart-rate sensor to the 3V ESP32 pin.
More useful readings:
How to measure the air quality using ESP32
How to use Websocket with ESP32
If you want to know more about ESP32 and ESP8266 you can read this tutorial:
How to measure the heart rate with ESP32
There are several examples showing how to develop a sketch. You can use this link to view all the possible examples. In this project, we modify one of those examples to add a Web interface to add the heart-rate and the pulse oximetry. The code is shown below:
#include <Wire.h>
#include "MAX30105.h"
#include "spo2_algorithm.h"
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
MAX30105 particleSensor;
#define MAX_BRIGHTNESS 255
// WiFi config
const char *SSID = "your_wifi_ssid";
const char *PWD = "wifi_pwd";
// Web server running on port 80
AsyncWebServer server(80);
// Async Events
AsyncEventSource events("/events");
const char index_html[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1"> <title>ESP32 Heart</title>
<script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script>
<script language="javascript">
google.charts.load('current', {'packages':['gauge']});
google.charts.setOnLoadCallback(drawChart);
var chartHR;
var chartSPOO2;
var optionsHR;
var optionsSPO2;
var dataHR;
var dataSPO2;
function drawChart() {
dataHR = google.visualization.arrayToDataTable([
['Label', 'Value'],
['HR', 0]
]);
optionsHR = {
min:40, max:230,
width: 400, height: 120,
greenColor: '#68A2DE',
greenFrom: 40, greenTo: 90,
yellowFrom: 91, yellowTo: 150,
redFrom:151, redTo:230,
minorTicks: 5
};
chartHR = new google.visualization.Gauge(document.getElementById('chart_div_hr'));
dataSPO2 = google.visualization.arrayToDataTable([
['Label', 'Value'],
['SPO2', 0]
]);
optionsSPO2 = {
min:0, max:100,
width: 400, height: 120,
greenColor: '#68A2DE',
greenFrom: 0, greenTo: 100,
minorTicks: 5
};
chartSPO2 = new google.visualization.Gauge(document.getElementById('chart_div_spo2'));
chartHR.draw(dataHR, optionsHR);
chartSPO2.draw(dataSPO2, optionsSPO2);
}
if (!!window.EventSource) {
var source = new EventSource('/events');
source.addEventListener('open', function(e) {
console.log("Events Connected");
}, false);
source.addEventListener('error', function(e) {
if (e.target.readyState != EventSource.OPEN) {
console.log("Events Disconnected");
}
}, false);
source.addEventListener('message', function(e) {
console.log("message", e.data);
}, false);
source.addEventListener('hr', function(e) {
dataHR.setValue(0,1, e.data);
chartHR.draw(dataHR, optionsHR);
}, false);
source.addEventListener('spo2', function(e) {
dataSPO2.setValue(0,1, e.data);
chartSPO2.draw(dataSPO2, optionsSPO2);
}, false);
}
</script>
<style>
.card {
box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2);
transition: 0.3s;
border-radius: 5px; /* 5px rounded corners */
}
/* On mouse-over, add a deeper shadow */
.card:hover {
box-shadow: 0 8px 16px 0 rgba(0,0,0,0.2);
}
/* Add some padding inside the card container */
.container {
padding: 2px 16px;
}
</style>
</head>
<body>
<h2>ESP32 Heart</h2>
<div class="content">
<div class="card">
<div class="container">
<div id="chart_div_hr" style="width: 400px; height: 120px;"></div>
<div id="chart_div_spo2" style="width: 400px; height: 120px;"></div>
</div>
</div>
</div>
</body>
</html>
)rawliteral";
uint32_t irBuffer[100]; //infrared LED sensor data
uint32_t redBuffer[100]; //red LED sensor data
int32_t bufferLength; //data length
int32_t spo2; //SPO2 value
int8_t validSPO2; //indicator to show if the SPO2 calculation is valid
int32_t heartRate; //heart rate value
int8_t validHeartRate; //indicator to show if the heart rate calculation is valid
byte pulseLED = 11; //Must be on PWM pin
byte readLED = 13; //Blinks with each data read
// last time SSE
long last_sse = 0;
void connectToWiFi() {
Serial.print("Connecting to ");
Serial.println(SSID);
WiFi.begin(SSID, PWD);
while (WiFi.status() != WL_CONNECTED) {
Serial.print(".");
delay(500);
// we can even make the ESP32 to sleep
}
Serial.print("Connected. IP: ");
Serial.println(WiFi.localIP());
}
void configureEvents() {
events.onConnect([](AsyncEventSourceClient *client){
if(client->lastId()){
Serial.printf("Client connections. Id: %u\n", client->lastId());
}
// and set reconnect delay to 1 second
client->send("hello from ESP32",NULL,millis(),1000);
});
server.addHandler(&events);
}
void setup()
{
Serial.begin(115200); // initialize serial communication at 115200 bits per second:
pinMode(pulseLED, OUTPUT);
pinMode(readLED, OUTPUT);
// Initialize sensor
if (!particleSensor.begin(Wire, I2C_SPEED_FAST)) //Use default I2C port, 400kHz speed
{
Serial.println(F("MAX30105 was not found. Please check wiring/power."));
while (1);
}
byte ledBrightness = 60; //Options: 0=Off to 255=50mA
byte sampleAverage = 4; //Options: 1, 2, 4, 8, 16, 32
byte ledMode = 2; //Options: 1 = Red only, 2 = Red + IR, 3 = Red + IR + Green
byte sampleRate = 100; //Options: 50, 100, 200, 400, 800, 1000, 1600, 3200
int pulseWidth = 411; //Options: 69, 118, 215, 411
int adcRange = 4096; //Options: 2048, 4096, 8192, 16384
particleSensor.setup(ledBrightness, sampleAverage, ledMode, sampleRate, pulseWidth, adcRange); //Configure sensor with these settings
connectToWiFi();
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
request->send_P(200, "text/html", index_html, NULL);
});
configureEvents();
server.begin();
}
void loop()
{
bufferLength = 100; //buffer length of 100 stores 4 seconds of samples running at 25sps
//read the first 100 samples, and determine the signal range
for (byte i = 0 ; i < bufferLength ; i++)
{
while (particleSensor.available() == false) //do we have new data?
particleSensor.check(); //Check the sensor for new data
redBuffer[i] = particleSensor.getRed();
irBuffer[i] = particleSensor.getIR();
particleSensor.nextSample(); //We're finished with this sample so move to next sample
//Serial.print(F("red="));
// Serial.print(redBuffer[i], DEC);
// Serial.print(F(", ir="));
// Serial.println(irBuffer[i], DEC);
}
//calculate heart rate and SpO2 after first 100 samples (first 4 seconds of samples)
maxim_heart_rate_and_oxygen_saturation(irBuffer, bufferLength, redBuffer, &spo2, &validSPO2, &heartRate, &validHeartRate);
//Continuously taking samples from MAX30102. Heart rate and SpO2 are calculated every 1 second
while (1)
{
//dumping the first 25 sets of samples in the memory and shift the last 75 sets of samples to the top
for (byte i = 25; i < 100; i++)
{
redBuffer[i - 25] = redBuffer[i];
irBuffer[i - 25] = irBuffer[i];
}
//take 25 sets of samples before calculating the heart rate.
for (byte i = 75; i < 100; i++)
{
while (particleSensor.available() == false) //do we have new data?
particleSensor.check(); //Check the sensor for new data
digitalWrite(readLED, !digitalRead(readLED)); //Blink onboard LED with every data read
redBuffer[i] = particleSensor.getRed();
irBuffer[i] = particleSensor.getIR();
particleSensor.nextSample(); //We're finished with this sample so move to next sample
}
if (millis() - last_sse > 2000) {
if (validSPO2 == 1) {
events.send(String(spo2).c_str(), "spo2", millis());
Serial.println("Send event SPO2");
Serial.print(F("SPO2="));
Serial.println(spo2, DEC);
}
if (validHeartRate == 1) {
events.send(String(heartRate).c_str(), "hr", millis());
Serial.println("Send event HR");
Serial.print(F("HR="));
Serial.println(heartRate, DEC);
}
last_sse = millis();
}
//After gathering 25 new samples recalculate HR and SP02
maxim_heart_rate_and_oxygen_saturation(irBuffer, bufferLength, redBuffer, &spo2, &validSPO2, &heartRate, &validHeartRate);
}
}
Code language: C++ (cpp)
To measure the pulse oximetry and the heart-rate, the code uses 25 samples before calculating the heart rate. In this code, while the ESP32 measures the heart-rate and the pulse oximetry, it sends the value measured to a web interface so that we can visualize the results. The final result is:

To visualize this interface you have to open your browser and point to the ESP32 Web server. The page is updated automatically.
Wrapping up
This brief tutorial showed how to connect the ESP32 to MAX30102 to measure the heart rate and the pulse oximetery. Using a simple biosensor, we can retrieve information about our body. There are several applications where we can apply this project and it can further developed.
Good project, but there is one thing I really don’t understand: why on earth should I send these sensitive health data to the web? I want to keep them locally, of course!
Thank you! By the way, the project doesn’t send data to the Web. It is the ESP32 that has an internal Web Server with a UI to show the results.
por que no se puede utilizar para controlar el estado de salud
Well it isn’t a medical device so you can’t use it for this purpose
Good project. Have you try this max30102 module in esphome? I want to but I don’t know how to intergrade into esphome.
hi , i try to impliment your code in my arduino IDE but i can’t see the esp32 IP address to see the web server, what should i do?