Code the Heart Rate Zone Trainer in CircuitPython

Libraries

Once your Feather nRF52840 is set up with CircuitPython, you'll also need to add some library files. Follow this page for information on how to download and add libraries to your Feather.

From the library bundle you downloaded in that guide page, transfer the following libraries onto the Feather's /lib directory:

  • adafruit_ble
  • adafruit_bus_device
  • adafruit_ht16k33
  • adafruit_register
  • adafruit_ble_heart_rate.mpy
  • neopixel.mpy

Text Editor

Adafruit recommends using the Mu editor for using your CircuitPython code with the Feather boards. You can get more info in this guide.

Alternatively, you can use any text editor that saves files.

Code.py

Copy the code shown below, paste it into Mu. Plug your Feather 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

"""
Heart Rate Trainer
Read heart rate data from a heart rate peripheral using the standard BLE
Heart Rate service.
Displays BPM value to Seven Segment FeatherWing
Displays percentage of max heart rate on another 7Seg FeatherWing
"""

import time
import board

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 adafruit_ht16k33.segments import Seg7x4

from digitalio import DigitalInOut, Direction

# Feather on-board status LEDs setup
red_led = DigitalInOut(board.RED_LED)
red_led.direction = Direction.OUTPUT
red_led.value = True

blue_led = DigitalInOut(board.BLUE_LED)
blue_led.direction = Direction.OUTPUT
blue_led.value = False

# target heart rate for interval training
# Change this number depending on your max heart rate, usually figured
# as (220 - your age).
max_rate = 180

# Seven Segment FeatherWing setup
i2c = board.I2C()
display_A = Seg7x4(i2c, address=0x70)  # this will be the BPM display
display_A.brightness = 15
display_A.fill(0)  # Clear the display
# Second display has A0 address jumpered
display_B = Seg7x4(i2c, address=0x71)  # this will be the % target display
display_B.brightness = 15
display_B.fill(0)  # Clear the display

# display_A "b.P.M."
display_A.set_digit_raw(0, 0b11111100)
display_A.set_digit_raw(1, 0b11110011)
display_A.set_digit_raw(2, 0b00110011)
display_A.set_digit_raw(3, 0b10100111)
# display_B "Prct"
display_B.set_digit_raw(0, 0b01110011)
display_B.set_digit_raw(1, 0b01010000)
display_B.set_digit_raw(2, 0b01011000)
display_B.set_digit_raw(3, 0b01000110)
time.sleep(3)

display_A.fill(0)
for h in range(4):
    display_A.set_digit_raw(h, 0b10000000)
# display_B show maximum heart rate value
display_B.fill(0)
display_B.print(max_rate)
time.sleep(2)

# PyLint can't find BLERadio for some reason so special case it here.
ble = adafruit_ble.BLERadio()    # pylint: disable=no-member

hr_connection = None

def display_SCAN():
    display_A.fill(0)
    display_A.set_digit_raw(0, 0b01101101)
    display_A.set_digit_raw(1, 0b00111001)
    display_A.set_digit_raw(2, 0b01110111)
    display_A.set_digit_raw(3, 0b00110111)


def display_bLE():
    display_B.fill(0)
    display_B.set_digit_raw(0, 0b00000000)
    display_B.set_digit_raw(1, 0b01111100)
    display_B.set_digit_raw(2, 0b00111000)
    display_B.set_digit_raw(3, 0b01111001)

def display_dots():  # "...."
    for j in range(4):
        display_A.set_digit_raw(j, 0b10000000)
        display_B.set_digit_raw(j, 0b10000000)

def display_dashes():  # "----"
    for k in range(4):
        display_A.set_digit_raw(k, 0b01000000)
        display_B.set_digit_raw(k, 0b01000000)

# Start with a fresh connection.
if ble.connected:
    display_SCAN()
    display_bLE()
    time.sleep(1)

    for connection in ble.connections:
        if HeartRateService in connection:
            connection.disconnect()
        break

while True:
    print("Scanning...")
    red_led.value = True
    blue_led.value = False
    display_SCAN()
    display_bLE()
    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)
            display_dots()
            time.sleep(2)
            print("Connected")
            blue_led.value = True
            red_led.value = False
            break

    # Stop scanning whether or not we are connected.
    ble.stop_scan()
    print("Stopped scan")
    red_led.value = False
    blue_led.value = True
    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 bpm is not 0:
                    pct_target = (round(100*(bpm/max_rate)))
                display_A.fill(0)  # clear the display
                display_B.fill(0)
                if values.heart_rate is 0:
                    display_dashes()
                else:
                    display_A.fill(0)
                    display_B.print(pct_target)
                    time.sleep(0.1)
                    display_A.print(bpm)

            time.sleep(0.9)
            display_A.set_digit_raw(0, 0b00000000)

Code Explainer

The code is doing a few fundamental things.

First, it loads the time and board libraries, as well as the necessary libraries to use BLE in general, and the HeartRateService in specific.

We also load the HT16K33 library to use the seven segment displays, and the digitalio library to use the Feather's on-board red and blue indicator LEDs.

Download: file
import time
import board

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 adafruit_ht16k33.segments import Seg7x4

from digitalio import DigitalInOut, Direction

LED setup

Next, code to prepare the on-board LEDs and turn on the red one, while leaving the blue one turned off, until we start scanning for BLE devices.

Download: file
# Feather on-board status LEDs setup
red_led = DigitalInOut(board.RED_LED)
red_led.direction = Direction.OUTPUT
red_led.value = True

blue_led = DigitalInOut(board.BLUE_LED)
blue_led.direction = Direction.OUTPUT
blue_led.value = False

Max Heart Rate Variable

The max_rate variable is used to calculate your heart rate training zone percentages. You can change this to suit your maximum heart rate. The simplest way to calculate this is by subtracting your age from 220, but you can get much more specific numbers from a doctor or training specialist.

max_rate = 180

Display Prep

To prep for using the two displays, the Set7x4 objects on the I2C bus are set using unique addresses. Remember, the second display's A0 pads was jumpered to set the address to 0x71, while leaving the first display at the default address 0x70.

Download: file
# Seven Segment FeatherWing setup
i2c = board.I2C()
display_A = Seg7x4(i2c, address=0x70)  # this will be the BPM display
display_A.brightness = 15
display_A.fill(0)  # Clear the display
# Second display has A0 address jumpered
display_B = Seg7x4(i2c, address=0x71)  # this will be the % target display
display_B.brightness = 15
display_B.fill(0)  # Clear the display

Seven Segment Display Use

This guide includes a great intro to using the matrix displays with CircuitPython.

You can set the display in a few different ways:

  • display_A.print(1234) will display 1234
  • display_A.set_digit_raw(3, 0b00000001) will light up the top segment of the fourth digit (far right) only.
  • display[0] = '6' will display a 6 on the first digit (far left)

During the startup sequence we'll display "b.P.M" on display A (the red one) and "Prct" on the display B (the blue one).

Download: file
# display_A "b.P.M."
display_A.set_digit_raw(0, 0b11111100)
display_A.set_digit_raw(1, 0b11110011)
display_A.set_digit_raw(2, 0b00110011)
display_A.set_digit_raw(3, 0b10100111)
# display_B "Prct"
display_B.set_digit_raw(0, 0b01110011)
display_B.set_digit_raw(1, 0b01010000)
display_B.set_digit_raw(2, 0b01011000)
display_B.set_digit_raw(3, 0b01000110)
time.sleep(3)

Here's a good image guide to the raw segment bitmask.

Max Heart Rate Display

Next, we'll set display A to "...." and show the max_rate value for a couple seconds on display B.

Download: file
display_A.fill(0)
for h in range(4):
    display_A.set_digit_raw(h, 0b10000000)
# display_B show maximum heart rate value
display_B.fill(0)
display_B.print(max_rate)
time.sleep(2)

BLE Instance

The BLE radio is defined next with ble = adafruit_ble.BLERadio() 

Display Functions

We'll keep the code neat by creating some functions that can be used repeatedly to show certain messages on the displays.

Download: file
def display_SCAN():
    display_A.fill(0)
    display_A.set_digit_raw(0, 0b01101101)
    display_A.set_digit_raw(1, 0b00111001)
    display_A.set_digit_raw(2, 0b01110111)
    display_A.set_digit_raw(3, 0b00110111)


def display_bLE():
    display_B.fill(0)
    display_B.set_digit_raw(0, 0b00000000)
    display_B.set_digit_raw(1, 0b01111100)
    display_B.set_digit_raw(2, 0b00111000)
    display_B.set_digit_raw(3, 0b01111001)

def display_dots():  # "...."
    for j in range(4):
        display_A.set_digit_raw(j, 0b10000000)
        display_B.set_digit_raw(j, 0b10000000)

def display_dashes():  # "----"
    for k in range(4):
        display_A.set_digit_raw(k, 0b01000000)
        display_B.set_digit_raw(k, 0b01000000)

Fresh Connection

We scan for a BLE device with the Heart Rate Service being advertised, and set the displays and status LEDs to match.

When we connect, the displays switch to four dots .... and we stop scanning.

Download: file
# Start with a fresh connection.
if ble.connected:
    display_SCAN()
    display_bLE()
    time.sleep(1)

    for connection in ble.connections:
        if HeartRateService in connection:
            connection.disconnect()
        break

while True:
    print("Scanning...")
    red_led.value = True
    blue_led.value = False
    display_SCAN()
    display_bLE()
    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)
            display_dots()
            time.sleep(2)
            print("Connected")
            blue_led.value = True
            red_led.value = False
            break

    # Stop scanning whether or not we are connected.
    ble.stop_scan()
    print("Stopped scan")
    red_led.value = False
    blue_led.value = True
    time.sleep(0.5)

Device Info

With the heart rate monitor connected, we'll request info that is displayed in the Mu REPL, if your Feather is connected to your computer over USB. This is purely informational for curiosity and debug purposes, and not displayed on the seven segment displays.

Download: file
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 Zone Percent

We've reached 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 measurment 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 display four dashes.

We'll create a pct_target variable that calculates the percentage of the max_rate based on current bpm.

The displays are cleared and then the percent value is shown on display B and the bpm is shown on display A. This display blinks each time the loop is run, and the whole process repeats every second.

Download: file
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 bpm is not 0:
                    pct_target = (round(100*(bpm/max_rate)))
                display_A.fill(0)  # clear the display
                display_B.fill(0)
                if values.heart_rate is 0:
                    display_dashes()
                else:
                    display_A.fill(0)
                    display_B.print(pct_target)
                    time.sleep(0.1)
                    display_A.print(bpm)

            time.sleep(0.9)
            display_A.set_digit_raw(0, 0b00000000)

On the next page we'll see it in action.

This guide was first published on Feb 05, 2020. It was last updated on 2020-02-19 14:44:18 -0500. This page (Code the Heart Rate Zone Trainer in CircuitPython) was last updated on Feb 20, 2020.