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.

Table of Contents
In this guide, we’ll cover the following topics:
- Introducing the MAX30102 Pulse Oximeter and Heart Rate Sensor
- Wiring the MAX30102 Sensor to the ESP32
- Preparing Arduino IDE and Libraries
- 1) ESP32 with MAX30102: Get Heart Rate – Code
- 2) ESP32 with MAX30102: Get Oxygen Saturation (SpO2) – Code
- 3) ESP32 with MAX30102: Get Temperature – Code
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.

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.

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 Boards | SDA – GPIO 21 | SCL – GPIO 22 |
| ESP32S3 Boards | SDA – GPIO 8 | SCL – 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).

Learn more about the ESP32 pinout:
- ESP32 Pinout Reference: Which GPIO pins should you use?
- ESP32-S3 DevKitC Pinout Reference Guide: GPIOs Explained
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.

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");
}
}
}
How Does the MAX30102 Get the Heart Rate?
The sensor comes with two LEDs: an infrared LED and a red LED.

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.

The BPM values will be printed in the Serial Monitor.

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();
}
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.
| Parameter | Value | Meaning | Options |
| ledBrightness | 60 | LED brightness | 0 to 255 |
| sampleAverage | 4 | number of samples averaged by the sensor | 1, 2, 4, 8, 16, 32 |
| ledMode | 2 | which LEDs are active | 1 – only Red LED 2 – Red and infrared LED 3 – Red, infrared, and green LEDs (for other MAX3010x sensors) |
| sampleRate | 100 | sampling rate (samples per second) | 50, 100, 200, 400, 800, 1000, 16200, 3200 |
| pulseWidth | 411 | LED pulse width (in ms) | 69, 118, 215, 411 |
| adcRange | 4096 | ADC 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.

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);
}
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.

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:
- ESP32 OLED Display with Arduino IDE
- How to Use I2C LCD with ESP32 on Arduino IDE
- LVGL with ESP32 TFT LCD Touchscreen Display – 2.8 inch ILI9341 240×320 (Arduino IDE)
- ESP32 with TM1637 4-Digit LED 7-Segment Display (Arduino IDE)
- Building an ESP32 Web Server: The Complete Guide for Beginners
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.




Very well-made tutorial! Congratulations!