Skip to content

Commit e4fc31a

Browse files
committed
Updates the examples adding HR monitor
1 parent b9e2b93 commit e4fc31a

File tree

7 files changed

+313
-1
lines changed

7 files changed

+313
-1
lines changed
File renamed without changes.
File renamed without changes.

example/main.py examples/basic_usage/main.py

+23-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,26 @@
1-
# main.py
1+
""" BASIC USAGE EXAMPLE
2+
This example shows how to use the MAX30102 sensor to collect data from the RED and IR channels.
3+
4+
The sensor is connected to the I2C bus, and the I2C bus is scanned to ensure that the sensor is connected.
5+
The sensor is also checked to ensure that it is a MAX30102 or MAX30105 sensor.
6+
7+
The sensor is set up with the following parameters:
8+
- Sample rate: 400 Hz
9+
- Averaged samples: 8
10+
- LED brightness: medium
11+
- Pulse width: 411 µs
12+
- Led mode: 2 (RED + IR)
13+
14+
The temperature is read at the beginning of the acquisition.
15+
16+
Then, in a loop the data is printed to the serial port, so that it can be plotted with a Serial Plotter.
17+
Also the real acquisition frequency (i.e. the rate at which samples are collected from the sensor) is computed
18+
and printed to the serial port. It differs from the sample rate, because the sensor processed the data and
19+
averages the samples before putting them into the FIFO queue (by default, 8 samples are averaged).
20+
21+
Author: n-elia
22+
"""
23+
224
# Some ports need to import 'sleep' from 'time' module
325
from machine import sleep, SoftI2C, Pin
426
from utime import ticks_diff, ticks_us

examples/heart_rate/README.md

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# Heart Rate Monitor Example
2+
3+
## Overview
4+
5+
The `HeartRateMonitor` class is designed to calculate heart rate from the raw sensor readings of a MAX30102 pulse oximeter and heart-rate sensor, tailored for use in a MicroPython environment on an ESP32 board. It continuously processes a stream of raw integer readings from the sensor, identifies heartbeats by detecting peaks in the signal, and calculates the heart rate based on the intervals between consecutive peaks.
6+
7+
## How It Works
8+
9+
- Input: The class expects individual raw sensor readings (integer values) as input, provided to it through the `add_sample` method. These readings should come from the IR or green LEDs of the MAX30102 sensor, continuously polled at a consistent rate.
10+
11+
- Signal Processing:
12+
13+
- **Smoothing**: The input signal is first smoothed using a moving average filter to reduce high-frequency noise. This step is crucial for accurate peak detection.
14+
15+
- **Peak Detection**: The algorithm then identifies peaks in the smoothed signal using a dynamic thresholding method. A peak represents a heartbeat.
16+
17+
- Heart Rate Calculation: Once peaks are identified, the class calculates the heart rate by averaging the time intervals between consecutive peaks. The result is expressed in beats per minute (BPM).
18+
19+
## Parameters
20+
21+
- `sample_rate` (int): Defines the rate at which samples are collected from the sensor, in samples per second (Hz). This rate should match the polling frequency of the sensor in your application.
22+
23+
- `window_size` (int): Determines the number of samples over which to perform peak detection and heart rate calculation. A larger `window_size` can improve accuracy by considering more data but may also increase computation time and reduce responsiveness to changes in heart rate. Typically set based on the expected range of heart rates and the sample rate.
24+
25+
- `smoothing_window` (int): Specifies the size of the moving average filter window for signal smoothing. A larger window will produce a smoother signal but may also dilute the signal's peaks, potentially affecting peak detection accuracy. The optimal size often depends on the level of noise in the signal and the sample rate.
26+
27+
### Setting the Parameters
28+
29+
- `sample_rate`: Set this to match the frequency at which you're polling the MAX30102 sensor. Common values are 50, 100, or 200 Hz, depending on your application's requirements for data granularity and responsiveness.
30+
31+
- `window_size`: Start with a value that covers 1 to 2 seconds of data, based on your sample_rate. For example, at 100 Hz, a window size of 100 to 200 samples might be appropriate. Adjust based on testing, considering the balance between accuracy and responsiveness.
32+
33+
- `smoothing_window`: Begin with a small window, such as 5 to 10 samples, and adjust based on the noise level observed in your sensor data. The goal is to smooth out high-frequency noise without significantly delaying the detection of true heartbeats.
34+
35+
## Expected Input and Results
36+
37+
- Input: Continuous integer readings from the MAX30102 sensor, added one at a time via the add_sample method.
38+
39+
- Output: The heart rate in BPM, calculated periodically by calling the calculate_heart_rate method. This method returns None if not enough data is present to accurately calculate the heart rate.
40+
41+
## Example Usage
42+
43+
python
44+
Copy code
45+
hr_monitor = HeartRateMonitor(sample_rate=100, window_size=150, smoothing_window=10)
46+
47+
```python
48+
# Add samples in a loop (replace the sample polling with actual sensor data retrieval)
49+
for _ in range(1000): # Example loop
50+
sample = ... # Poll the MAX30102/5 sensor to get a new sample
51+
hr_monitor.add_sample(sample)
52+
# Optionally, sleep or wait based on your polling frequency
53+
54+
# Calculate and print the heart rate
55+
heart_rate = hr_monitor.calculate_heart_rate()
56+
if heart_rate is not None:
57+
print(f"Heart Rate: {heart_rate:.2f} BPM")
58+
else:
59+
print("Not enough data to calculate heart rate")
60+
```

examples/heart_rate/boot.py

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# This file is executed on every boot (including wake-boot from deep sleep)
2+
3+
def do_connect(ssid: str, password: str):
4+
import network
5+
wlan = network.WLAN(network.STA_IF)
6+
wlan.active(True)
7+
if not wlan.isconnected():
8+
print('connecting to network...')
9+
wlan.connect(ssid, password)
10+
while not wlan.isconnected():
11+
pass
12+
print('network config:', wlan.ifconfig())
13+
14+
15+
if __name__ == '__main__':
16+
# Put yor Wi-Fi credentials here
17+
my_ssid = "my_ssid"
18+
my_pass = "my_password"
19+
20+
# Check if the module is available in memory
21+
try:
22+
from max30102 import MAX30102
23+
except ImportError as e:
24+
# Module not available. Try to connect to Internet to download it.
25+
print(f"Import error: {e}")
26+
print("Trying to connect to the Internet to download the module.")
27+
do_connect(my_ssid, my_pass)
28+
try:
29+
# Try to leverage upip package manager to download the module.
30+
import upip
31+
upip.install("micropython-max30102")
32+
except ImportError:
33+
# upip not available. Try to leverage mip package manager to download the module.
34+
print("upip not available in this port. Trying with mip.")
35+
import mip
36+
mip.install("github:n-elia/MAX30102-MicroPython-driver")

examples/heart_rate/lib/README.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Note
2+
3+
To manually install the library, copy the files `max30102/circular_buffer.py`, `max30102/max30102.py` and past them here.
4+
5+
Then, load the content of `example` directory into the board.

examples/heart_rate/main.py

+189
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
# main.py
2+
# Some ports need to import 'sleep' from 'time' module
3+
from machine import sleep, SoftI2C, Pin
4+
from utime import ticks_diff, ticks_us, ticks_ms
5+
6+
from max30102 import MAX30102, MAX30105_PULSE_AMP_MEDIUM
7+
8+
9+
class HeartRateMonitor:
10+
"""A simple heart rate monitor that uses a moving window to smooth the signal and find peaks."""
11+
12+
def __init__(self, sample_rate=100, window_size=10, smoothing_window=5):
13+
self.sample_rate = sample_rate
14+
self.window_size = window_size
15+
self.smoothing_window = smoothing_window
16+
self.samples = []
17+
self.timestamps = []
18+
self.filtered_samples = []
19+
20+
def add_sample(self, sample):
21+
"""Add a new sample to the monitor."""
22+
timestamp = ticks_ms()
23+
self.samples.append(sample)
24+
self.timestamps.append(timestamp)
25+
26+
# Apply smoothing
27+
if len(self.samples) >= self.smoothing_window:
28+
smoothed_sample = (
29+
sum(self.samples[-self.smoothing_window :]) / self.smoothing_window
30+
)
31+
self.filtered_samples.append(smoothed_sample)
32+
else:
33+
self.filtered_samples.append(sample)
34+
35+
# Maintain the size of samples and timestamps
36+
if len(self.samples) > self.window_size:
37+
self.samples.pop(0)
38+
self.timestamps.pop(0)
39+
self.filtered_samples.pop(0)
40+
41+
def find_peaks(self):
42+
"""Find peaks in the filtered samples."""
43+
peaks = []
44+
45+
if len(self.filtered_samples) < 3: # Need at least three samples to find a peak
46+
return peaks
47+
48+
# Calculate dynamic threshold based on the min and max of the recent window of filtered samples
49+
recent_samples = self.filtered_samples[-self.window_size :]
50+
min_val = min(recent_samples)
51+
max_val = max(recent_samples)
52+
threshold = (
53+
min_val + (max_val - min_val) * 0.5
54+
) # 50% between min and max as a threshold
55+
56+
for i in range(1, len(self.filtered_samples) - 1):
57+
if (
58+
self.filtered_samples[i] > threshold
59+
and self.filtered_samples[i - 1] < self.filtered_samples[i]
60+
and self.filtered_samples[i] > self.filtered_samples[i + 1]
61+
):
62+
peak_time = self.timestamps[i]
63+
peaks.append((peak_time, self.filtered_samples[i]))
64+
65+
return peaks
66+
67+
def calculate_heart_rate(self):
68+
"""Calculate the heart rate in beats per minute (BPM)."""
69+
peaks = self.find_peaks()
70+
71+
if len(peaks) < 2:
72+
return None # Not enough peaks to calculate heart rate
73+
74+
# Calculate the average interval between peaks in milliseconds
75+
intervals = []
76+
for i in range(1, len(peaks)):
77+
interval = ticks_diff(peaks[i][0], peaks[i - 1][0])
78+
intervals.append(interval)
79+
80+
average_interval = sum(intervals) / len(intervals)
81+
82+
# Convert intervals to heart rate in beats per minute (BPM)
83+
heart_rate = (
84+
60000 / average_interval
85+
) # 60 seconds per minute * 1000 ms per second
86+
87+
return heart_rate
88+
89+
90+
def main():
91+
# I2C software instance
92+
i2c = SoftI2C(
93+
sda=Pin(8), # Here, use your I2C SDA pin
94+
scl=Pin(9), # Here, use your I2C SCL pin
95+
freq=400000,
96+
) # Fast: 400kHz, slow: 100kHz
97+
98+
# Examples of working I2C configurations:
99+
# Board | SDA pin | SCL pin
100+
# ------------------------------------------
101+
# ESP32 D1 Mini | 22 | 21
102+
# TinyPico ESP32 | 21 | 22
103+
# Raspberry Pi Pico | 16 | 17
104+
# TinyS3 | 8 | 9
105+
106+
# Sensor instance
107+
sensor = MAX30102(i2c=i2c) # An I2C instance is required
108+
109+
# Scan I2C bus to ensure that the sensor is connected
110+
if sensor.i2c_address not in i2c.scan():
111+
print("Sensor not found.")
112+
return
113+
elif not (sensor.check_part_id()):
114+
# Check that the targeted sensor is compatible
115+
print("I2C device ID not corresponding to MAX30102 or MAX30105.")
116+
return
117+
else:
118+
print("Sensor connected and recognized.")
119+
120+
# Load the default configuration
121+
print("Setting up sensor with default configuration.", "\n")
122+
sensor.setup_sensor()
123+
124+
# Set the sample rate to 400: 400 samples/s are collected by the sensor
125+
sensor_sample_rate = 400
126+
sensor.set_sample_rate(sensor_sample_rate)
127+
128+
# Set the number of samples to be averaged per each reading
129+
sensor_fifo_average = 8
130+
sensor.set_fifo_average(sensor_fifo_average)
131+
132+
# Set LED brightness to a medium value
133+
sensor.set_active_leds_amplitude(MAX30105_PULSE_AMP_MEDIUM)
134+
135+
# Expected acquisition rate: 400 Hz / 8 = 50 Hz
136+
actual_acquisition_rate = int(sensor_sample_rate / sensor_fifo_average)
137+
138+
sleep(1)
139+
140+
print(
141+
"Starting data acquisition from RED & IR registers...",
142+
"press Ctrl+C to stop.",
143+
"\n",
144+
)
145+
sleep(1)
146+
147+
# Initialize the heart rate monitor
148+
hr_monitor = HeartRateMonitor(
149+
# Select a sample rate that matches the sensor's acquisition rate
150+
sample_rate=actual_acquisition_rate,
151+
# Select a significant window size to calculate the heart rate (2-5 seconds)
152+
window_size=int(actual_acquisition_rate * 3),
153+
)
154+
155+
# Setup to calculate the heart rate every 2 seconds
156+
hr_compute_interval = 2 # seconds
157+
ref_time = ticks_ms() # Reference time
158+
159+
while True:
160+
# The check() method has to be continuously polled, to check if
161+
# there are new readings into the sensor's FIFO queue. When new
162+
# readings are available, this function will put them into the storage.
163+
sensor.check()
164+
165+
# Check if the storage contains available samples
166+
if sensor.available():
167+
# Access the storage FIFO and gather the readings (integers)
168+
red_reading = sensor.pop_red_from_storage()
169+
ir_reading = sensor.pop_ir_from_storage()
170+
171+
# Add the IR reading to the heart rate monitor
172+
# Note: based on the skin color, the red, IR or green LED can be used
173+
# to calculate the heart rate with more accuracy.
174+
hr_monitor.add_sample(ir_reading)
175+
176+
# Periodically calculate the heart rate every `hr_compute_interval` seconds
177+
if ticks_diff(ticks_ms(), ref_time) / 1000 > hr_compute_interval:
178+
# Calculate the heart rate
179+
heart_rate = hr_monitor.calculate_heart_rate()
180+
if heart_rate is not None:
181+
print("Heart Rate: {:.0f} BPM".format(heart_rate))
182+
else:
183+
print("Not enough data to calculate heart rate")
184+
# Reset the reference time
185+
ref_time = ticks_ms()
186+
187+
188+
if __name__ == "__main__":
189+
main()

0 commit comments

Comments
 (0)