Code Quickstart

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)

Code Explainer

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. 

 

wearables_2027_Phases_of_the_Cardiac_Cycle.jpg
https://en.wikipedia.org/wiki/Cardiac_cycle#/media/File:2027_Phases_of_the_Cardiac_Cycle.jpg

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]

*Yes, the cardiac cycle is more complicated than just a ventricular diastole and systole. For the purposes of this guide, these elements are what we're looking for to make the heartbeat look realistic and accurate enough with the NeoPixels.

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)

This guide was first published on Jan 25, 2022. It was last updated on 2022-01-25 23:41:52 -0500.

This page (Code the Heart Rate Display in CircuitPython) was last updated on Mar 30, 2022.

Text editor powered by tinymce.