If you'd just like to get the code working fast, click "Download Project Bundle" from the code below, copy the files over to your CIRCUITPY drive and verify the file system looks the same as here. Turn on your heart rate monitor and the program should be working!
Text Editor
If you'd like to edit the code, Adafruit recommends using the Mu editor. You can get more info in this guide.
Alternatively, you can use any text editor that saves files.
Code.py
If you'd like to see the messages printed in the REPL, your CPB isn't working as intended, or you'd just like to dig into the code consider the following:
Copy the code shown below, paste it into Mu. Plug your CPB into your computer via a known good USB cable. In your operating system's file explorer/finder, you should see a new flash drive named CIRCUITPY. Save the code from Mu to the Feather's CIRCUITPY drive as code.py.
# SPDX-FileCopyrightText: 2022 Isaac Wellish for Adafruit Industries
#
# SPDX-License-Identifier: MIT
"""
SPDX-FileCopyrightText: 2022 Isaac Wellish for Adafruit Industries
SPDX-License-Identifier: MIT
Circuit Playground Bluefruit BLE Heart Rate Display
Read heart rate data from a heart rate peripheral using
the standard BLE heart rate service.
The heart rate monitor connects to the CPB via BLE.
LEDs on CPB blink to the heart rate of the user.
"""
import time
import board
import neopixel
import adafruit_ble
from adafruit_ble.advertising.standard import ProvideServicesAdvertisement
from adafruit_ble.services.standard.device_info import DeviceInfoService
from adafruit_ble_heart_rate import HeartRateService
from digitalio import DigitalInOut, Direction
#on-board status LED setup
red_led = DigitalInOut(board.D13)
red_led.direction = Direction.OUTPUT
red_led.value = True
#NeoPixel code
pixels = neopixel.NeoPixel(board.NEOPIXEL, 10, brightness=0.2, auto_write=False)
RED = (255, 0, 0)
LIGHTRED = (20, 0, 0)
def color_chase(color, wait):
for i in range(10):
pixels[i] = color
time.sleep(wait)
pixels.show()
time.sleep(0.5)
# animation to show initialization of program
color_chase(RED, 0.1) # Increase the number to slow down the color chase
#starting bpm value
bpm = 60
# PyLint can't find BLERadio for some reason so special case it here.
ble = adafruit_ble.BLERadio() # pylint: disable=no-member
hr_connection = None
# Start with a fresh connection.
if ble.connected:
time.sleep(1)
for connection in ble.connections:
if HeartRateService in connection:
connection.disconnect()
break
while True:
print("Scanning...")
red_led.value = True
time.sleep(1)
for adv in ble.start_scan(ProvideServicesAdvertisement, timeout=5):
if HeartRateService in adv.services:
print("found a HeartRateService advertisement")
hr_connection = ble.connect(adv)
time.sleep(2)
print("Connected")
red_led.value = False
break
# Stop scanning whether or not we are connected.
ble.stop_scan()
print("Stopped scan")
red_led.value = False
time.sleep(0.5)
if hr_connection and hr_connection.connected:
print("Fetch connection")
if DeviceInfoService in hr_connection:
dis = hr_connection[DeviceInfoService]
try:
manufacturer = dis.manufacturer
except AttributeError:
manufacturer = "(Manufacturer Not specified)"
try:
model_number = dis.model_number
except AttributeError:
model_number = "(Model number not specified)"
print("Device:", manufacturer, model_number)
else:
print("No device information")
hr_service = hr_connection[HeartRateService]
print("Location:", hr_service.location)
while hr_connection.connected:
values = hr_service.measurement_values
print(values) # returns the full heart_rate data set
if values:
bpm = (values.heart_rate)
if values.heart_rate == 0:
print("-")
else:
time.sleep(0.1)
print(bpm)
if bpm != 0: # prevent from divide by zero
#find interval time between beats
bps = bpm / 60
period = 1 / bps
time_on = 0.375 * period
time_off = period - time_on
# Blink leds at the given BPM
pixels.fill(RED)
pixels.show()
time.sleep(time_on)
pixels.fill(LIGHTRED)
pixels.show()
time.sleep(time_off)
The code is doing several important things here.
First, the necessary libraries are loaded such as board, time and neopixel. Additionally, a slew of various BLE support libraries are loaded.
import time import board import neopixel import adafruit_ble from adafruit_ble.advertising.standard import ProvideServicesAdvertisement from adafruit_ble.services.standard.device_info import DeviceInfoService from adafruit_ble_heart_rate import HeartRateService from digitalio import DigitalInOut, Direction
LED and NeoPixels Set Up
Next, the on board status LED is set up.
Then the NeoPixels are initialized and a special color_chase function is defined then called. This has a nice effect of red leads "circling" the board when the CPB boots up.
#on-board status LED setup
red_led = DigitalInOut(board.D13)
red_led.direction = Direction.OUTPUT
red_led.value = True
#NeoPixel code
pixels = neopixel.NeoPixel(board.NEOPIXEL, 10, brightness=0.2, auto_write=False)
RED = (255, 0, 0)
LIGHTRED = (20, 0, 0)
def color_chase(color, wait):
for i in range(10):
pixels[i] = color
time.sleep(wait)
pixels.show()
time.sleep(0.5)
# animation to show initialization of program
color_chase(RED, 0.1) # Increase the number to slow down the color chase
Defining BPM and Connecting to a BLE Device
Next, an example bpm (Beats Per Minute) level is defined which will be changed immediately when a new bpm is detected.
We scan for a BLE device with the Heart Rate Service being advertised and set the status LED to match.
When we connect, the LED turns off and we stop scanning.
#starting bpm value
bpm = 60
# PyLint can't find BLERadio for some reason so special case it here.
ble = adafruit_ble.BLERadio() # pylint: disable=no-member
hr_connection = None
# Start with a fresh connection.
if ble.connected:
time.sleep(1)
for connection in ble.connections:
if HeartRateService in connection:
connection.disconnect()
break
while True:
print("Scanning...")
red_led.value = True
time.sleep(1)
for adv in ble.start_scan(ProvideServicesAdvertisement, timeout=5):
if HeartRateService in adv.services:
print("found a HeartRateService advertisement")
hr_connection = ble.connect(adv)
time.sleep(2)
print("Connected")
red_led.value = False
break
# Stop scanning whether or not we are connected.
ble.stop_scan()
print("Stopped scan")
red_led.value = False
time.sleep(0.5)
Device Info
With the heart rate monitor connected, the code requests info that is displayed in the Mu REPL, if your CPB is connected to your computer over USB. This is purely informational for curiosity and debug purposes.
if hr_connection and hr_connection.connected:
print("Fetch connection")
if DeviceInfoService in hr_connection:
dis = hr_connection[DeviceInfoService]
try:
manufacturer = dis.manufacturer
except AttributeError:
manufacturer = "(Manufacturer Not specified)"
try:
model_number = dis.model_number
except AttributeError:
model_number = "(Model number not specified)"
print("Device:", manufacturer, model_number)
else:
print("No device information")
hr_service = hr_connection[HeartRateService]
print("Location:", hr_service.location)
Heart Rate and NeoPixels
Now this is "the heart" of the program! This is the code that loops over and over while the devices are connected.
First, we cast the heart rate service's measurement characteristic attributes that are sent as values, and then we cast the heart rate value itself as bpm
We'll check at first for non-zero bpm readings, as the heart rate monitor sends a few zeros at first, and we'll ignore them so nobody gets too worried, and just print dashes to the REPL.
Calculating and Displaying the Heart Rate
Next, in order to convert the BPM into blinky heartbeats, we have to do a couple of calculations. I used the code from this Hackaday project by Dillon Nichols to do the calculations.
The general idea is that each heartbeat consists of two parts, a ventricular diastole, and a ventricular systole*.
From the below diagram, the ventricular diastole is the start of the cycle and is when the heart fills up with blood. The ventricular systole is the 2nd phase where the blood vigorously pumps and ejects the blood into the rest of the body.
How long does each part last?
In terms of time, in general, the diastole is roughly 5/8s of the total cycle and the systole is about 3/8. So if one heartbeat took 1 second (60 BPM), the diastole would last about 0.625s (5/8) and the systole would like about 0.375s (3/8)
A more nuanced explanation via Wikipedia:
At the start of the cycle, during ventricular diastole–early, the heart relaxes and expands while receiving blood into both ventricles through both atria; then, near the end of ventricular diastole–late, the two atria begin to contract (atrial systole), and each atrium pumps blood into the ventricle below it.[3] During ventricular systole the ventricles are contracting and vigorously pulsing (or ejecting) two separated blood supplies from the heart—one to the lungs and one to all other body organs and systems—while the two atria are relaxed (atrial diastole). This precise coordination ensures that blood is efficiently collected and circulated throughout the body.[4]
So with this in mind, we now need to find the time for the diastole and systole for a given BPM. These times will then be given to the NeoPixels to turn them on and off. The diastole will be the time the NeoPixels are dimmed (longer) and the systole will be the time they are brightened (shorter).
The Calculations
First, we convert BPM to beats per second. Then we find the period, which is the length of time for one heartbeat. Next, we multiply that by 3/8 or 0.375 to calculate the systole time. Lastly, the diastole is just the period - the systole.
Now we can take these values and tell the NeoPixels to brighten for the length of the systole and dim for the length of the diastole. How cool!
while hr_connection.connected:
values = hr_service.measurement_values
print(values) # returns the full heart_rate data set
if values:
bpm = (values.heart_rate)
if values.heart_rate == 0:
print("-")
else:
time.sleep(0.1)
print(bpm)
if bpm != 0: # prevent from divide by zero
#find interval time between beats
bps = bpm / 60
period = 1 / bps
time_on = 0.375 * period
time_off = period - time_on
# Blink leds at the given BPM
pixels.fill(RED)
pixels.show()
time.sleep(time_on)
pixels.fill(LIGHTRED)
pixels.show()
time.sleep(time_off)
Page last edited January 21, 2025
Text editor powered by tinymce.