ESP32 with MAX30102: Pulse Oximeter and Heart Rate Sensor (Arduino IDE)

This tutorial is a getting started guide to interface the MAX30102 pulse oximeter and heart rate sensor with the ESP32 programmed with the Arduino IDE. This sensor can measure heart rate, blood oxygen saturation (SpO2), and body temperature. It communicates using the I2C communication protocol.

We’ll cover how to wire the sensor and provide code examples to get heart rate, oxygen saturation, and body temperature.

ESP32 with MAX30102: Pulse Oximeter and Heart Rate Sensor

Table of Contents

In this guide, we’ll cover the following topics:

Introducing the MAX30102 Pulse Oximeter and Heart Rate Sensor

This module includes the MAX30102 IC, which is an optical sensor that can measure blood oxygen saturation (SpO2) and heart rate in a non-invasive way. It also includes a temperature sensor used for temperature compensation and calibration.

MAX30102 Heart Rate Sensor Pulse Oximeter SpO2 Part

The MAX30102 has a red LED, an infrared LED, and a photodetector. The LEDs emit light into the skin, and the photodetector measures the reflected light. The amount of reflected light changes with blood flow as the heart beats. By processing these changes, we can calculate the heart rate and SpO2.

How Does MAX30102 Work?

Here’s how the sensor works to get SpO2 and heart rate measurements.

MAX30102 sensor how it works HR and SpO2

1) Light emission: the red and infrared LEDs emit light into the body tissue. Each LED operates at a different wavelength.

2) Detection of reflected light: the emitted light penetrates the body tissue, and some is reflected. The amount of reflected light depends on the blood volume and the oxygenation level. The sensor’s photodetector measures the reflected light.

3) Changes in light intensity: by measuring variations in reflected light intensity at the two wavelengths, red and infrared, the sensor can differentiate between oxygenated and deoxygenated haemoglobin. These measurements can then be used to calculate the heart rate and SpO2.

  • Oxygenated haemoglobin: absorbs more infrared light
  • Deoxygenated haemoglobin: absorbs more red light

Based on the ratio of reflected light intensities at red and infrared wavelengths, the sensor can calculate SpO2. The changes in blood volume with each heartbeat allow us to measure the heart rate (BPM).

Where to Buy?

You can check our Maker Advisor Tools Page to compare the MAX30102 module price in different stores.

Wiring the MAX30102 Sensor to the ESP32

The MAX30102 sensor communicates using I2C communication protocol. We’ll use the ESP32 default I2C pins to wire the sensor.

ESP32 BoardsSDA – GPIO 21SCL – GPIO 22
ESP32S3 BoardsSDA – GPIO 8SCL – GPIO 9

The sensor module can be powered via the VIN pin using 5V or 3V3.

Wire the sensor to the ESP32 as shown in the following diagram (adjust for your specific ESP32 board model).

ESP32 with MAX30102 Oximeter and heart rate sensor - wiring diagram

Learn more about the ESP32 pinout:

Preparing Arduino IDE

We’ll program the ESP32 board using Arduino IDE. So, make sure you have the ESP32 add-on installed. Follow the next tutorial:

If you prefer using VSCode + PlatformIO, follow the next tutorial instead:

Installing the SparkFun MAX3010x Library

There are several libraries you can use to interface the MAX30102 sensor module with the ESP32. We’ll use the SparkFun MAX3010X library that works well for this sensor and is easy to use.

Open the Arduino IDE Library Manager, search for SparkFun MAX3010x, and install the library by SparkFun.

Install heart rate sensor library sparkfun Arduino IDE

1) ESP32 with MAX30102: Get Heart Rate – Code

The following code shows how to get the heart rate from the MAX30102 sensor with the ESP32. It gets the heart rate and displays the results on the Serial Monitor.

/*
  Rui Santos & Sara Santos - Random Nerd Tutorials
  Complete project details at https://RandomNerdTutorials.com/esp32-max30102-oximeter-heart-rate-sensor/
  Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files.
  The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*/
#include <Wire.h>
#include "MAX30105.h"
#include "heartRate.h"

MAX30105 sensor;

// Settings
const byte RATE_SIZE = 5;   // How many beats we average
byte rates[RATE_SIZE];      // Store last few valid BPM values
byte rateSpot = 0;
byte validBeats = 0;
long lastBeat = 0;
float currentBPM = 0;
int averageBPM = 0;

unsigned long lastPrint = 0;

// Only accept realistic heart rates (adjust for your case)
const float MIN_BPM = 45;
const float MAX_BPM = 180;

void setup() {
  Serial.begin(115200);
  Serial.println("MAX30102 Heart Rate Sensor");
  Serial.println();
  Serial.println("Place your finger on the sensor...");
  Serial.println();

  // Initialize the sensor
  if (!sensor.begin(Wire, I2C_SPEED_FAST)) {
    Serial.println("Error initializing the sensor...");
    while (1);
  }

  sensor.setup();                    // Use default settings
  sensor.setPulseAmplitudeRed(0);    // Turn off red LED
}

void loop() {
  long irValue = sensor.getIR();      // Read infrared value

  // Check if a heartbeat was detected
  if (checkForBeat(irValue)) {
    long timeBetweenBeats = millis() - lastBeat;
    lastBeat = millis();

    currentBPM = 60.0 / (timeBetweenBeats / 1000.0);

    // Only keep realistic values
    if (currentBPM >= MIN_BPM && currentBPM <= MAX_BPM) {
      rates[rateSpot] = (byte)currentBPM;
      rateSpot++;
      if (rateSpot >= RATE_SIZE){
        rateSpot = 0;
      }
      // Count valid beats (until buffer is full)
      if (validBeats < RATE_SIZE) {
        validBeats++;
      }
      
      // Only calculate average when buffer is full
      if (validBeats >= RATE_SIZE) {
        // Calculate the average
        averageBPM = 0;
        for (byte i = 0; i < RATE_SIZE; i++) {
          averageBPM += rates[i];
        }
        averageBPM /= RATE_SIZE;
      }
    }
  }

  // Print once every second
  if (millis() - lastPrint >= 1000) {
    lastPrint = millis();

    if (irValue < 50000) {
      // No finger on the sensor
      Serial.println("Waiting for finger...");
    }
    else if (averageBPM == 0) {
      // Finger is present but not enough valid beats yet
      Serial.println("Measuring... keep your finger still");
    }
    else {
      // We have a valid average
      Serial.print("Heart Rate: ");
      Serial.print(averageBPM);
      Serial.println(" BPM");
    }
  }
}

View raw code

How Does the MAX30102 Get the Heart Rate?

The sensor comes with two LEDs: an infrared LED and a red LED.

MAX30102 Heart Rate Sensor Pulse Oximeter SpO2 Component

These LEDs emit light that goes through your fingertip. The flowing blood absorbs light. When the heart beats, more blood is pushed into the vessels of your finger, and more light is absorbed.

To detect heart rate, we take into account the amount of reflected infrared light. The sensor has a photodetector to measure that:

  • Heartbeat: more blood means more infrared light is absorbed, so less light reaches the photodetector.
  • Between heartbeats: less blood means less infrared light is absorbed, so more light reaches the photodetector

The changes in light intensity create a waveform. By measuring the time between its peaks, you can calculate the heart rate.

Taking this into account, it’s now easy to understand how the code works.

How Does the Code Work?

Let’s take a quick look at how the code works.

Libraries

First, include the required libraries:

#include <Wire.h>
#include "MAX30105.h"
#include "heartRate.h"

Sensor Object

Create a MAX30105 object called sensor.

MAX30105 sensor;

Global Variables

We need a few global variables to store multiple heart rate readings in an array and calculate the average BPM.

const byte RATE_SIZE = 5;  
byte rates[RATE_SIZE]; 
byte rateSpot = 0;
byte validBeats = 0;
long lastBeat = 0;
float currentBPM = 0;
int averageBPM = 0;

unsigned long lastPrint = 0;

The RATE_SIZE defines how many readings we’ll gather to calculate the average BPM. You can increase this value for more stable results. The rates variable creates an array with 5 values (RATE_SIZE).

The rateSpot will be used to indicate the current position in the rates array. The validBeat counts how many valid beats have been collected so far. It increases until it reaches RATE_SIZE (5).

The lastBeat stores the time of the last valid beat; the currentBPM stores the current BPM value; and the averageBPM stores the average of all values stored in the rates array.

To make sure we only account for valid readings, we exclude heartbeat values that are too low or too high, which can be caused by improper placement of the finger on the sensor. You can adjust these values for your scenario, or don’t include them at all.

const float MIN_BPM = 45;
const float MAX_BPM = 180;

setup()

In the setup(), we initialize the Serial Monitor at a baud rate of 115200 and initialize the sensor.

void setup() {
  Serial.begin(115200);
  Serial.println("MAX30102 Heart Rate Sensor");
  Serial.println();
  Serial.println("Place your finger on the sensor...");
  Serial.println();

  // Initialize the sensor
  if (!sensor.begin(Wire, I2C_SPEED_FAST)) {
    Serial.println("Error initializing the sensor...");
    while (1);
  }

To measure the heart rate, the algorithm only uses the infrared LED light. So, we turn off the red one and leave all the other default settings.

sensor.setup(); 
sensor.setPulseAmplitudeRed(0);

loop()

In the loop(), we start by getting an infrared value (the infrared light that was reflected and read by the photodetector).

long irValue = sensor.getIR(); 

Then, the checkForBeat() function detects whether a heartbeat has just occurred, based on the infrared signal. If we have a heartbeat, we calculate the time elapsed since the last beat and based on the time between beats, we can estimate the heart beat (BPM – beats per minute).

// Check if a heartbeat was detected
if (checkForBeat(irValue)) {
  long timeBetweenBeats = millis() - lastBeat;
  lastBeat = millis();

  currentBPM = 60.0 / (timeBetweenBeats / 1000.0);

We go a little bit further, and we only save a BPM value if it is within the range we defined previously. If it is, we add it to a spot in our rates array.

// Only keep realistic values
if (currentBPM >= MIN_BPM && currentBPM <= MAX_BPM) {
  rates[rateSpot] = (byte)currentBPM;
  rateSpot++;
  if (rateSpot >= RATE_SIZE){
    rateSpot = 0;
  }

After the rates array is full, we calculate the average heart rate based on the last 5 readings (RATE_SIZE)

// Count valid beats (until buffer is full)
if (validBeats < RATE_SIZE) {
  validBeats++;
}
      
// Only calculate average when buffer is full
if (validBeats >= RATE_SIZE) {
  // Calculate the average
  averageBPM = 0;
  for (byte i = 0; i < RATE_SIZE; i++) {
    averageBPM += rates[i];
  }
  averageBPM /= RATE_SIZE;
}

Still in the loop(), we can have one of the following scenarios.

If the irValue is lower than 50000, it means there isn’t a finger on the sensor. We print a message to the Serial Monitor.

if (irValue < 50000) {
  // No finger on the sensor
  Serial.println("Waiting for finger...");
}

If the current average BPM is 0, it means we don’t have enough readings in the array. The user must keep the finger on the sensor to get valid readings.

else if (averageBPM == 0) {
  // Finger is present but not enough valid beats yet
  Serial.println("Measuring... keep your finger still");
}

If none of the previous scenarios occur, it means we have a valid heart rate value, and we print the results in the Serial Monitor.

else {
  // We have a valid average
  Serial.print("Heart Rate: ");
  Serial.print(averageBPM);
  Serial.println(" BPM");
}

Demonstration

Upload the code to your ESP32 board. After uploading, open the Serial Monitor at a baud rate of 115200.

Place your finger on the sensor. For better and more accurate results, the finger must be kept still with constant pressure. You can attach the sensor to your finger using a rubber band.

ESP32 MAX30102 Pulse Oximeter Heart Rate Sensor Demonstration Arduino IDE

The BPM values will be printed in the Serial Monitor.

ESP32 Get BPM Heart Rate with MAX30102 Arduino IDE

2) ESP32 with MAX30102: Get Oxygen Saturation (SpO2) – Code

The following code shows how to get the blood oxygen saturation from the MAX30102 sensor with the ESP32. The results are printed in the Serial Monitor.

/*
  Rui Santos & Sara Santos - Random Nerd Tutorials
  Complete project details at https://RandomNerdTutorials.com/esp32-max30102-oximeter-heart-rate-sensor/
  Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files.
  The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*/
#include <Wire.h>
#include "MAX30105.h"
#include "spo2_algorithm.h"

MAX30105 sensor;

// Buffers needed by the algorithm
#define BUFFER_SIZE 100
uint32_t irBuffer[BUFFER_SIZE];
uint32_t redBuffer[BUFFER_SIZE];

int32_t spo2;          // Oxygen saturation value
int8_t  validSPO2;     // 1 = valid reading, 0 = not valid yet
int32_t heartRate;     // Not used, but required by the function
int8_t  validHeartRate;

void setup() {
  Serial.begin(115200);
  Serial.println("MAX30102 Oxygen Saturation");
  Serial.println();
  Serial.println("Place your finger on the sensor...");
  Serial.println();

  // Initialize the sensor
  if (!sensor.begin(Wire, I2C_SPEED_FAST)) {
    Serial.println("Error initializing the sensor...");
    while (1);
  }

  byte ledBrightness = 60;  // 0=Off to 255=50mA
  byte sampleAverage = 4;   // 1, 2, 4, 8, 16, 32
  byte ledMode = 2;         // 1 = Red only, 2 = Red + IR, 3 = Red + IR + Green
  byte sampleRate = 100;    // 50, 100, 200, 400, 800, 1000, 1600, 3200
  int pulseWidth = 411;     // 69, 118, 215, 411
  int adcRange = 4096;      // 2048, 4096, 8192, 16384

  // Configure sensor with these settings
  sensor.setup(ledBrightness, sampleAverage, ledMode, sampleRate, pulseWidth, adcRange);
}

void loop() {
  // Collect 100 samples (takes a few seconds)
  for (byte i = 0; i < BUFFER_SIZE; i++) {
    while (sensor.available() == false) {
      sensor.check();
    }

    redBuffer[i] = sensor.getRed();
    irBuffer[i]  = sensor.getIR();
    sensor.nextSample();
  }

  // Calculate SpO2
  maxim_heart_rate_and_oxygen_saturation(irBuffer, BUFFER_SIZE, redBuffer, &spo2, &validSPO2, &heartRate, &validHeartRate);

  // Display oxygen saturation
  if (validSPO2) {
    Serial.print("Oxygen Saturation: ");
    Serial.print(spo2);
    Serial.println(" %");
  } else {
    Serial.println("Measuring... keep your finger still");
  }

  Serial.println();
}

View raw code

How Does the MAX30102 Get the SpO2?

As we’ve seen previously, the MAX30102 comes with a red LED and an infrared LED.

Those LEDs emit light that goes through your fingertips. Some of the light is absorbed, and some is reflected. The reflected light is measured by the photodetector on the module.

From the variations in the reflected light intensity (red and infrared), we can calculate SpO2.

  • Oxygenated haemoglobin: absorbs more infrared light
  • Deoxygenated haemoglobin: absorbs more red light

Based on the ratio of reflected light intensities at red and infrared wavelengths, the sensor can calculate SpO2.

How Does the Code Work?

Let’s take a quick look at how the code works.

Libraries

First, include the required libraries:

#include <Wire.h>
#include "MAX30105.h"
#include "heartRate.h"

Sensor Object

Create a MAX30105 object called sensor.

MAX30105 sensor;

Global Variables

We create a few global variables that are used throughout the code. We need a buffer of 100 infrared and red readings for the algorithm to calculate SpO2. The variables related to the heart rate are also required for the library’s algorithm to calculate SpO2.

// Buffers needed by the algorithm
#define BUFFER_SIZE 100
uint32_t irBuffer[BUFFER_SIZE];
uint32_t redBuffer[BUFFER_SIZE];

int32_t spo2;             // Oxygen saturation value
int8_t  validSPO2;     // 1 = valid reading, 0 = not valid yet
int32_t heartRate;     // Not used, but required by the function
int8_t  validHeartRate;

setup()

In the setup(), initialize the Serial Monitor and the sensor.

void setup() {
  Serial.begin(115200);
  Serial.println("MAX30102 Oxygen Saturation");
  Serial.println();
  Serial.println("Place your finger on the sensor...");
  Serial.println();

  // Initialize the sensor
  if (!sensor.begin(Wire, I2C_SPEED_FAST)) {
    Serial.println("Error initializing the sensor...");
    while (1);
  }

We set up the sensor with the recommended settings for SpO2 measurements.

byte ledBrightness = 60;
byte sampleAverage = 4;
byte ledMode = 2;
byte sampleRate = 100;
int pulseWidth = 411;
int adcRange = 4096;

sensor.setup(ledBrightness, sampleAverage, ledMode, sampleRate, pulseWidth, adcRange);

The following table shows what each parameter is and which values they can have.

ParameterValueMeaningOptions
ledBrightness60LED brightness0 to 255
sampleAverage4number of samples averaged by the sensor1, 2, 4, 8, 16, 32
ledMode2which LEDs are active1 – only Red LED
2 – Red and infrared LED
3 – Red, infrared, and green LEDs (for other MAX3010x sensors)
sampleRate100sampling rate (samples per second)50, 100, 200, 400, 800, 1000, 16200, 3200
pulseWidth411LED pulse width (in ms)69, 118, 215, 411
adcRange4096ADC range (sensitivity)2048, 4096, 8192, 16384

loop()

In the loop(), we get 100 samples of reflected infrared and red light and save them to the redBuffer and irBuffer variables.

// Collect 100 samples (takes a few seconds)
for (byte i = 0; i < BUFFER_SIZE; i++) {
  while (sensor.available() == false) {
    sensor.check();
  }

  redBuffer[i] = sensor.getRed();
  irBuffer[i]  = sensor.getIR();
  sensor.nextSample();
}

Finally, we call the maxim_heart_rate_and_oxygen_saturation() function with the following parameters to get the value of SpO2.

maxim_heart_rate_and_oxygen_saturation(irBuffer, BUFFER_SIZE, redBuffer, &spo2, &validSPO2, &heartRate, &validHeartRate);

The oxygen saturation value is saved in the spo2 variable, and validSPO2 tells us if we have a valid reading. We print the results to the Serial Monitor.

// Display oxygen saturation
if (validSPO2) {
  Serial.print("Oxygen Saturation: ");
  Serial.print(spo2);
  Serial.println(" %");
} else {
  Serial.println("Measuring... keep your finger still");
}

Serial.println();

Demonstration

Upload the code to your ESP32 board. After uploading, open the Serial Monitor at a baud rate of 115200.

Place your finger on the sensor. Wait a few seconds until you have valid SpO2 readings.

ESP32 Get SpO2 with MAX30102 Arduino IDE

3) ESP32 with MAX30102: Get Temperature – Code

The MAX30102 sensor also comes with a temperature sensor required for calibration of the BPM and SpO2 values. We can also use that sensor to get the body temperature value (in this case, the fingertip temperature).

The following code shows how to get body temperature from the MAX30102 sensor with the ESP32.

/*
  Rui Santos & Sara Santos - Random Nerd Tutorials
  Complete project details at https://RandomNerdTutorials.com/esp32-max30102-oximeter-heart-rate-sensor/
  Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files.
  The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*/
#include <Wire.h>
#include "MAX30105.h"

MAX30105 sensor;

void setup() {
  Serial.begin(115200);
  Serial.println("MAX30102 Temperature Sensor");
  Serial.println();

  // Initialize the sensor
  if (!sensor.begin(Wire, I2C_SPEED_FAST)) {
    Serial.println("Error initializing the sensor...");
    while (1);
  }
  
  // Turn off the LEDs so they don't heat the sensor
  sensor.setup(0);            // 0 = LEDs off
  sensor.enableDIETEMPRDY();  // Enable temperature ready signal
}

void loop() {
  // Read temperature in Celsius
  float tempC = sensor.readTemperature();

  // Convert to Fahrenheit
  float tempF = tempC * 1.8 + 32.0;

  // Print the temperature readings
  Serial.print("Temperature: ");
  Serial.print(tempC, 2);
  Serial.print(" °C   |   ");
  Serial.print(tempF, 2);
  Serial.println(" °F");

  delay(1000);
}

View raw code

How Does the Code Work?

Let’s take a quick look at how the code works.

Libraries

Start by including the required libraries.

#include <Wire.h>
#include "MAX30105.h"

Sensor Object

Create a MAX30105 object called sensor.

MAX30105 sensor;

setup()

In the setup(), initialize the Serial Monitor at a baud rate of 115200.

Serial.begin(115200);
Serial.println("MAX30102 Temperature Sensor");
Serial.println();

Initialize the sensor.

// Initialize the sensor
if (!sensor.begin(Wire, I2C_SPEED_FAST)) {
  Serial.println("Error initializing the sensor...");
  while (1);
}

Turn off the sensor’s LEDs so they don’t interfere with the temperature readings, and enable the signal to read the temperature.

// Turn off the LEDs so they don't heat the sensor
sensor.setup(0); 
sensor.enableDIETEMPRDY();

loop()

Finally, in the loop(), we continuously read the temperature every second.

Getting the temperature in Celsius is as easy as calling the readTemperature() function on the sensor object.

float tempC = sensor.readTemperature();

We can then convert the temperature to Fahrenheit as follows.

// Convert to Fahrenheit
float tempF = tempC * 1.8 + 32.0;

Print the results in the Serial Monitor.

// Print the temperature readings
Serial.print("Temperature: ");
Serial.print(tempC, 2);
Serial.print(" °C   |   ");
Serial.print(tempF, 2);
Serial.println(" °F");

Demonstration

Upload the code to your board. Then, open the Serial Monitor at a baud rate of 115200.

Attach the sensor to your finger using a rubber band.

It will start displaying the temperature readings every second. Wait a few seconds for the results to stabilize.

ESP32 Get temperature with MAX30102 Arduino IDE

Wrapping Up

In this tutorial, you learned how to interface the MAX30102 sensor module with the ESP32 to get heart rate, blood oxygen concentration, and body temperature (fingertip). This sensor can be used in a wide variety of wearable/health projects.

You can take this project further and add a display module to show the results, or build a web server to display the data. You may find it helpful to take a look at the following guides to choose a display module or build a web server:

You can also create a BLE peripheral with those characteristics and a web or mobile app to show the results.

We’ll create more projects using this sensor, so stay tuned.



Learn how to build a home automation system and we’ll cover the following main subjects: Node-RED, Node-RED Dashboard, Raspberry Pi, ESP32, ESP8266, MQTT, and InfluxDB database DOWNLOAD »
Learn how to build a home automation system and we’ll cover the following main subjects: Node-RED, Node-RED Dashboard, Raspberry Pi, ESP32, ESP8266, MQTT, and InfluxDB database DOWNLOAD »

Recommended Resources

Build a Home Automation System from Scratch » With Raspberry Pi, ESP8266, Arduino, and Node-RED.

Home Automation using ESP8266 eBook and video course » Build IoT and home automation projects.

Arduino Step-by-Step Projects » Build 25 Arduino projects with our course, even with no prior experience!

What to Read Next…


Enjoyed this project? Stay updated by subscribing our newsletter!

1 thought on “ESP32 with MAX30102: Pulse Oximeter and Heart Rate Sensor (Arduino IDE)”

Leave a Comment

Download Our Free eBooks and Resources

Get instant access to our FREE eBooks, Resources, and Exclusive Electronics Projects by entering your email address below.