ESP32 Heart Rate and Pulse oximetry with MAX30102

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!

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:


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

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=""></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 ( != EventSource.OPEN) { console.log("Events Disconnected"); } }, false); source.addEventListener('message', function(e) { console.log("message",; }, false); source.addEventListener('hr', function(e) { dataHR.setValue(0,1,; chartHR.draw(dataHR, optionsHR); }, false); source.addEventListener('spo2', function(e) { dataSPO2.setValue(0,1,; 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:

ESP32 Heart rate monitoring with pulse oximetry measurement
The value in the SPO2 is not real

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.

    1. Christoph Schreiber March 18, 2021
      • Francesco Azzola March 19, 2021
    2. Anonymous March 25, 2021
      • Francesco Azzola March 26, 2021

    Add Your Comment