You are on page 1of 11

NEW!

 BOOKS ▾ INVENT ▾ T U TO R I A L S ▾ SERVICES ▾  COURSES  FORUM

 ▾  ▾

INVENT  BIOLOGY

Building a MicroPython heart rate monitor


Finding the beat in HR sensor data
by Martin Fitzpatrick  Read time 10:08

Pulse sensors have become popular due to their use in health-monitors like the Fitbit. The sensors used are cheap,
simple and pretty reliable at getting a reasonable indication of heart rate in daily use. They work by sensing the
change in light absorption or re ection by blood as it pulses through your arteries — a technique jauntily named
photoplethysmography (PPG). The rising and falling light signal can be used to identify the pulse, and subsequently
calculate heart rate.

Requirements

Wemos D1
amazon
v2.2+ or good imitations.
Pulsesensor.com sensor
amazon
Other types may work, but might need a amplifier/filter circuit.
Something to hold sensor against your finger
Velcro straps work well for this.
Wires
Loose ends, or jumper leads.
0.91in OLED Screen
amazon
128x32 pixels, I2c interface.

Most commercial sensors (Fitbit, etc.) use green-light based sensors and similar sensors are available for use in your
own projects. In this project we're taking a Pulsesensor.com sensor and using it to build a working heart monitor
with OLED pulse, BPM and trace display, using MicroPython on a Wemos D1.

Wiring up the sensor


In this project we're using an Wemos D1 and a Pulsesensor.com heart rate sensor, but other boards and sensors will
also work ne. Wire up the sensor as follows, with the signal (S) pin connected to your board's analoge input.

Pulsesensor.com Wemos D1 Type


- GND

+ 3.3V

S A0 Analog input

Once your sensor is wired to this pin you can use following MicroPython code to read the value from the sensor:

PYTHON

import machine
adc = machine.ADC(0)

>>> adc.read()
550

Note: The values output by this ADC are integers in the range 0-1023. A reading of 550 is equivalent to 0.54 on a 0-1
scale.

Detecting a beat
To get some data to work with I set up an loop to print out the above data to terminal while measuring my

own pulse. The output was logged to an out le using screen -L <device> 115200

Below is a plot showing the resulting output showing a rising peak for each beat.
Detecting a peak by sight is straightforward: the signal rises on a beat, and falls between beats, repeatedly reaching
maximum and minimum values. However, biological variability makes things doing this automatically a little trickier.
Firstly, the maximum and minimum values are affected by multiple things including ambient light, skin colour,
depth and size of blood vessels. Secondly, the magnitude of the peaks is affected by the strength of the pulse, and
depth of any arteries. Thirdly, the distance between peaks is non-constant: even for a perfectly healthy person, the
heart will occasionally skip beats, or the sensor will miss them.

To detect the beat we have a couple of options —

1. Detect the signal being over/under a threshold. Simple to implement, but the threshold must adjust to
account for use variation.
2. Detect the signal rising or falling (for N number of ticks) Bit trickier to implement, less affected by threshold
issues, more affected by noise (transient dips).

Here we're going to use the rst method, accounting for variation by using a pair of auto-adjusting threshold. We will
count a pulse when the value rises 3/4 of the way to the current maximum and a pulse ends when the value falls
below 1/2 of the current maximum.

Optimization
To understand why these values were selected, see the following plots. Below is a plot of pulse data (blue) alongside
maxima and minima (purple, red) and the current threshold for the given window (grey). This uses a windowsize of
50 samples, and as you can see the maxima & minima bounce around, pulling the threshold all over.

If you see better data from your sensor don't be surprised, these were selected to be noisy.

Extending the window size to 200 gives us a much more stable maxima and minima measurement, although it's still
bobbling a little. Notice also that the mid-point (50%) is crossed occasionally where there is no beat.
Extending the window size to 250 eliminates most of the bobble for a "normal" heart rate. Here the threshold line
(grey) has been moved to 2/3rds which moves it clear of most of the noise. However, again on the very rst peak
there is a transient dip in the signal that brings it back below the threshold.

To protect against transient icker around the cutoff, we can use two cutoff values with separation between them.
Here we use a beat on threshold of 75%, and a beat off threshold of 50%. The LED will light once the signal has risen
above 75% of maximum, but will not turn off until it falls back below 50%.

You can apply this same approach to sensing triggers on many fuzzy analogue signals.

Detecting a beat
The full code for detecting a beat using MicroPython on a Wemos D1 is given below. In this example, we ash the
built-in LED each time a beat is detected.

PYTHON

from machine import Pin, Signal, ADC


adc = ADC(0)

# On my board on = off, need to reverse.


led = Signal(Pin(2, Pin.OUT), invert=True)

MAX_HISTORY = 250

# Maintain a log of previous values to


# determine min, max and threshold.
history = []
while True:
v = adc.read()

history.append(v)

# Get the tail, up to MAX_HISTORY length


history = history[-MAX_HISTORY:]

minima, maxima = min(history), max(history)

threshold_on = (minima + maxima * 3) // 4 # 3/4


threshold_off = (minima + maxima) // 2 # 1/2

if v > threshold_on:
led.on()

if v < threshold_off:
led.off()

The animation below shows the pulse sensor in action, with the LED ashing on each beat.

Calculating HR
Maximum heart rate is around 220 BPM (1 beat every 0.27 seconds), and the lowest ever recorded was 26

bpm (1 beat every 2.3 seconds). The normal healthy range is usually considered to be around 60-80 bpm.

Now we can detect the beat, calculating the heart rate is simply a case of counting the number of beats we see
within a certain time frame. This window needs to be large enough to ensure we capture at least 2 beats for even the
slowest hearts. Here we've used 5 seconds (2.3 x 2 = 4.6). Longer durations will give more accurate heart rates, but be
slower to refresh.

Timer based
We can calculate a very rough heart rate using interrupts. MicroPython providers timers, which can be con gured to
re repeatedly at a given interval. We can use these, together with running counter to calculate the number of beats
between each interval, and from there the number of beats in a minute. The following code will calculate this BPM
and write it to the console.

PYTHON

from machine import Pin, Signal, ADC, Timer


adc = ADC(0)

# On my board on = off, need to reverse.


led = Signal(Pin(2, Pin.OUT), invert=True)

MAX_HISTORY = 250

# Maintain a log of previous values to


# determine min, max and threshold.
history = []
beat = False
beats = 0

def calculate_bpm(t):
global beats
print('BPM:', beats * 6) # Triggered every 10 seconds, * 6 = bpm
beats = 0

timer = Timer(1)
timer.init(period=10000, mode=Timer.PERIODIC, callback=calculate_bpm)

while True:
v = adc.read()

history.append(v)

# Get the tail, up to MAX_HISTORY length


history = history[-MAX_HISTORY:]

minima, maxima = min(history), max(history)

threshold_on = (minima + maxima * 3) // 4 # 3/4


threshold_off = (minima + maxima) // 2 # 1/2

if not beat and v > threshold_on:


beat = True
beats += 1
led.on()

if beat and v < threshold_off:


beat = False
led.off()

We have a timer set to 5 seconds, which when called sums up the total of beats since the last calculation, then
multiplies this by 6 to give the BPM. The only other change required is the addition of a lock to prevent beats being
re-registered once we've already seen one. We do this by toggling beat between True and False — a new beat is
only registered if the last beat has ended.

The limitation of this approach is we can only calculate heart rates to multiples of 60/timer_seconds . With a timer at
5 seconds for example, the calculated heart rate can only be 12, (1 beat in the 5 seconds), 24 (2 beats in the 5 seconds),
36 (3...), 48 (4...), 60, 72, 84, 96, 108 or 120 etc.

Queue-based
We can avoid this limitating using a queue. On each beat, we push the current time onto the queue, truncating it to
keep it within a reasonable length. To calculate the beat we can use the time different between the start and end of
the queue (timespan), together with the total length (beats), to calculate beats/minute.

PYTHON

from machine import Pin, Signal, I2C, ADC, Timer


import ssd1306
import time

adc = ADC(0)

i2c = I2C(-1, Pin(5), Pin(4))


display = ssd1306.SSD1306_I2C(128, 32, i2c)

MAX_HISTORY = 250
TOTAL_BEATS = 30

def calculate_bpm(beats):
# Truncate beats queue to max, then calculate bpm.
# Calculate difference in time from the head to the
# tail of the list. Divide the number of beats by
# this duration (in seconds)
beats = beats[-TOTAL_BEATS:]
beat_time = beats[-1] - beats[0]
if beat_time:
bpm = (len(beats) / (beat_time)) * 60
display.text("%d bpm" % bpm, 12, 0)

def detect():
# Maintain a log of previous values to
# determine min, max and threshold.
history = []
beats = []
beat = False

while True:
v = adc.read()

history.append(v)

# Get the tail, up to MAX_HISTORY length


history = history[-MAX_HISTORY:]

minima, maxima = min(history), max(history)

threshold_on = (minima + maxima * 3) // 4 # 3/4


threshold_off = (minima + maxima) // 2 # 1/2

if v > threshold_on and beat == False:


beat = True
beats.append(time.time())
beats = beats[-TOTAL_BEATS:]
calculate_bpm(beats)

if v < threshold_off and beat == True:


beat = False

Heart monitor with OLED screen


To create a complete working heart-rate monitor, we can combine what we have so far with a display. The following
code uses an 128x32 OLED i2c display using the ssd1306 display driver.

To use this display, just download that .py le and upload it onto your controller.

The display is wired in using I2C, with the heart rate sensor connected on the same pins as before.

ssd1306 Display Wemos D1

GND GND

VCC 3.3V

SCL D1

SDA D2
The LED ash is replaced with a graphic heart pulse inidicator on the display. The calculated BPM is also shown
alongside on the screen. At the bottom we show a continuously updating trace of the sensor data.

For setting up I2C the Pins passed in don't match the D numbers. You can nd the mapping here for all pins.

PYTHON

from machine import Pin, Signal, I2C, ADC, Timer


import ssd1306
import time

adc = ADC(0)

i2c = I2C(-1, Pin(5), Pin(4))


display = ssd1306.SSD1306_I2C(128, 32, i2c)

MAX_HISTORY = 200
TOTAL_BEATS = 30

The following block de nes the heart image for display on the OLED screen. Since we're using a 1-color screen, we
can set each pixel to either on 1 or off 0 .

PYTHON

HEART = [
[ 0, 0, 0, 0, 0, 0, 0, 0, 0],
[ 0, 1, 1, 0, 0, 0, 1, 1, 0],
[ 1, 1, 1, 1, 0, 1, 1, 1, 1],
[ 1, 1, 1, 1, 1, 1, 1, 1, 1],
[ 1, 1, 1, 1, 1, 1, 1, 1, 1],
[ 0, 1, 1, 1, 1, 1, 1, 1, 0],
[ 0, 0, 1, 1, 1, 1, 1, 0, 0],
[ 0, 0, 0, 1, 1, 1, 0, 0, 0],
[ 0, 0, 0, 0, 1, 0, 0, 0, 0],
]

In the refresh block we rst scroll the display left, for the rolling trace meter. We scroll the whole display because
there is no support for region scroll in framebuf . The other areas of the display are wiped clear anyway, so it has no
effect on appearance. If we have data we plot the trace line, scaled automatically to the min and max values for the
current window. Finally we write the BPM to the display, along with the heart icon if we're currently in a beat state.

PYTHON

last_y = 0

def refresh(bpm, beat, v, minima, maxima):


global last_y
display.vline(0, 0, 32, 0)
display.scroll(-1,0) # Scroll left 1 pixel

if maxima-minima > 0:
# Draw beat line.
y = 32 - int(16 * (v-minima) / (maxima-minima))
display.line(125, last_y, 126, y, 1)
last_y = y

# Clear top text area.


display.fill_rect(0,0,128,16,0) # Clear the top text area

if bpm:
display.text("%d bpm" % bpm, 12, 0)

# Draw heart if beating.


if beat:
for y, row in enumerate(HEART):
for x, c in enumerate(row):
display.pixel(x, y, c)

display.show()

The BPM calculation uses the beats queue, which contains the timestamp (in seconds) of each detected beat. By
comparing the time at the beginning and the end of the queue we get a total time duration. The number of values
in the list equals the number of beats detected. By dividing the number by the duration we get beats/second ( *60
for beats per minute).

PYTHON

def calculate_bpm(beats):
if beats:
beat_time = beats[-1] - beats[0]
if beat_time:
return (len(beats) / (beat_time)) * 60

In the main detection loop we read the sensor, calculate the on and off thresholds and then test our value agains
these. We recalculate BPM on each beat, and refresh the screen on each loop.

Depending on the speed of your display you may want to update less regularly.

PYTHON

def detect():
# Maintain a log of previous values to
# determine min, max and threshold.
history = []
beats = []
beat = False
bpm = None

# Clear screen to start.


display.fill(0)

while True:
v = adc.read()
history.append(v)

# Get the tail, up to MAX_HISTORY length


history = history[-MAX_HISTORY:]

minima, maxima = min(history), max(history)

threshold_on = (minima + maxima * 3) // 4 # 3/4


threshold_off = (minima + maxima) // 2 # 1/2
if v > threshold_on and beat == False:
beat = True
beats.append(time.time())
# Truncate beats queue to max
beats = beats[-TOTAL_BEATS:]
bpm = calculate_bpm(beats)

if v < threshold_off and beat == True:


beat = False

refresh(bpm, beat, v, minima, maxima)

Below is a short animation of the heart monitor with OLED display in action, showing the rolling heart trace, beats
per minute and heart-beat indicator.

goto
Wiring up the sensor

Detecting a beat

Calculating HR
Heart monitor with OLED screen

Share
 Twitter

 Facebook

 Reddit

Building a MicroPython heart rate monitor was published in invent on March 28, 2018 and tagged PYTHON  BIOLOGY  SCIENCE SENSOR

HR HEART-RATE ESP8266 WEMOS-D1 MICROPYTHON  ELECTRONICS

Continue reading 
Heart rate (HR) sensors  RASPBERRY-PI

Pulse sensors are a common feature of tness monitors, used to track your activity and cardiac tness over time. These
external monitors use the re ection and absorption of bright green/infra-red light to detect the pulse wave travelling down
the artery — a technique called photoplethysmography (PPG). This same techique is … More

About me
Books
Python tutorials
PyQt5 Tutorial

Installation guides
First steps with PyQt5
Example PyQt5 Apps
Python Packages & Reusable Code

Write with me
Join the Forum
Latest news
Reference & docs

Contact me
Af liate program
Licensing, Privacy & Legal

   

Martin Fitzpatrick Copyright ©2019-2020   Tutorials CC-BY-NC-SA       Sitemap   Public code BSD & MIT
Registered in the Netherlands with the KvK 66435102

You might also like