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
# SPDX-FileCopyrightText: 2020 John Park for Adafruit Industries # # SPDX-License-Identifier: MIT """ 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() # uses board.SCL and board.SDA # i2c = board.STEMMA_I2C() # For using the built-in STEMMA QT connector on a microcontroller display_A = Seg7x4(i2c, address=0x70) # this will be the BPM display 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.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.
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.
# 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.
# 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).
# 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)
Max Heart Rate Display
Next, we'll set display A to "...." and show the max_rate value for a couple seconds on display B.
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)
Display Functions
We'll keep the code neat by creating some functions that can be used repeatedly to show certain messages on the displays.
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.
# 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.
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.
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.
Text editor powered by tinymce.