Here's a bonus version of the BLE Heart Rate Zone Trainer you can make with the CLUE board alone, no need for external displays!

Follow these instructions to set up your CLUE with CircuitPython, then check out this page for info on adding libraries.

Libraries

Once your CLUE is set up with CircuitPython and library files in general, we'll add some project specific libraries.

From the library bundle you downloaded in that guide page, transfer any additional libraries shown here onto the CLUE's /lib directory on the CIRCUITPY drive:

  • adafruit_apds9960
  • adafruit_ble
  • adafruit_ble_heart_rate.mpy
  • adafruit_bmp280.mpy
  • adafruit_bus_device
  • adafruit_clue.py
  • adafruit_display_notification
  • adafruit_display_shapes
  • adafruit_display_text
  • adafruit_lis3mdl.mpy
  • adafruit_lsm6ds.mpy
  • adafruit_register
  • adafruit_sht31d.mpy
  • neopixel.mpy
  • simpleio.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 CLUE 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 CLUE'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 and percentage of max heart rate on CLUE
"""

import time
from adafruit_clue import clue
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

clue_data = clue.simple_text_display(title="Heart Rate", title_color = clue.PINK,
                                     title_scale=1, text_scale=3)

alarm_enable = True

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

# 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:
    print("SCAN")
    print("BLE")
    time.sleep(1)

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

while True:
    print("Scanning...")
    print("SCAN")
    print("BLE")
    time.sleep(1)
    clue_data[0].text = "BPM: ---"
    clue_data[0].color = ((30, 0, 0))
    clue_data[1].text = "Scanning..."
    clue_data[3].text = ""
    clue_data[1].color = ((130, 130, 0))
    clue_data.show()

    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()
            print("....")
            time.sleep(2)
            print("Connected")
            break

    # Stop scanning whether or not we are connected.
    ble.stop_scan()
    print("Stopped scan")
    time.sleep(0.1)

    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)))
                if values.heart_rate is 0:
                    print("----")
                    clue_data[0].text = "BPM: ---"
                    clue_data[0].color = ((80, 0, 0))
                    clue_data[1].text = "Target: --"
                    clue_data[1].color = ((0, 0, 80))
                else:
                    clue_data[0].text = "BPM: {0:d}".format(bpm)
                    clue_data[0].color = clue.RED

                    clue_data[1].text = "Target: {0:d}%".format(pct_target)
                    if pct_target < 90:
                        alarm = False
                        clue_data[1].color = clue.CYAN
                    else:
                        alarm = True
                        clue_data[1].color = clue.RED

                    clue_data[3].text = "Max HR: : {0:d}".format(max_rate)
                    clue_data[3].color = clue.BLUE
                    clue_data.show()

                    if alarm and alarm_enable:
                        clue.start_tone(2000)
                    else:
                        clue.stop_tone()

                    # Inputs
                    if clue.button_a:
                        if clue.touch_2:  # hold cap touch 2 for bigger change rate
                            max_rate = max_rate -10
                        else:
                            max_rate = max_rate - 1
                    if clue.button_b:
                        if clue.touch_2:
                            max_rate = max_rate + 10
                        else:
                            max_rate = max_rate + 1

                    if clue.touch_0:
                        alarm_enable = False
                    if clue.touch_1:
                        alarm_enable = True

            time.sleep(0.2)

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 adafruit_ble_heart_rate library in specific.

We also load the adafruit_clue library so we can take advantage of convenient commands that simplify using the CLUE's display.

The clue_data variable is created to instantiate the CLUE display object for simple text and titles.

We also set up the BLERadio so it can be used to communicate with the sensor.

Download: file
"""
Heart Rate Trainer
Read heart rate data from a heart rate peripheral using the standard BLE
Heart Rate service.
Displays BPM value and percentage of max heart rate on CLUE
"""
 
import time
from adafruit_clue import clue
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
 
clue_data = clue.simple_text_display(title="Heart Rate", title_color = clue.PINK,
                                     title_scale=1, text_scale=3)
 
alarm_enable = True
 
# target heart rate for interval training
# Change this number depending on your max heart rate, usually figured
# as (220 - your age).
max_rate = 180
 
# PyLint can't find BLERadio for some reason so special case it here.
ble = adafruit_ble.BLERadio()    # pylint: disable=no-member

Connection

Next, we scan for a BLE peripheral device advertising that it has the Heart Rate service.

We display the BPM, and "Scanning..." text, specifying their lines and colors on the CLUE TFT display.

When it is found, the CLUE will connect to it and then display the device name and other info. These are regular print() statements that show up in the REPL or other serial display, including the CLUE's display.

Download: file
print("Scanning...")
    print("SCAN")
    print("BLE")
    time.sleep(1)
    clue_data[0].text = "BPM: ---"
    clue_data[0].color = ((30, 0, 0))
    clue_data[1].text = "Scanning..."
    clue_data[3].text = ""
    clue_data[1].color = ((130, 130, 0))
    clue_data.show()
 
    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()
            print("....")
            time.sleep(2)
            print("Connected")
            break
 
    # Stop scanning whether or not we are connected.
    ble.stop_scan()
    print("Stopped scan")
    time.sleep(0.1)
 
    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)

Measurements

Next we begin displaying the measurements. At first we show dashes while the data streams to the CLUE from the HRM, then switch to real data.

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)))
                if values.heart_rate is 0:
                    print("----")
                    clue_data[0].text = "BPM: ---"
                    clue_data[0].color = ((80, 0, 0))
                    clue_data[1].text = "Target: --"
                    clue_data[1].color = ((0, 0, 80))
                else:
                    clue_data[0].text = "BPM: {0:d}".format(bpm)
                    clue_data[0].color = clue.RED
 
                    clue_data[1].text = "Target: {0:d}%".format(pct_target)

Alarm

We'll do a calculation of the current BPM vs the Max HR and set off an alarm when it goes above 90%.

Well use the buttons and cap touch to adjust max heart rate and alarm on/off.

Download: file
if pct_target < 90:
                        alarm = False
                        clue_data[1].color = clue.CYAN
                    else:
                        alarm = True
                        clue_data[1].color = clue.RED
 
                    clue_data[3].text = "Max HR: : {0:d}".format(max_rate)
                    clue_data[3].color = clue.BLUE
                    clue_data.show()
 
                    if alarm and alarm_enable:
                        clue.start_tone(2000)
                    else:
                        clue.stop_tone()
            
                    # Inputs
                    if clue.button_a:
                        if clue.touch_2:  # hold cap touch 2 for bigger change rate
                            max_rate = max_rate -10
                        else:
                            max_rate = max_rate - 1
                    if clue.button_b:
                        if clue.touch_2:
                            max_rate = max_rate + 10
                        else:
                            max_rate = max_rate + 1
 
                    if clue.touch_0:
                        alarm_enable = False
                    if clue.touch_1:
                        alarm_enable = True
 
            time.sleep(0.2)

In Use

You can use the Heart Rate Zone Trainer any time you want to do some exercise, and be aware of your heart rate and the zone percentage you're in. Set it on a surface where you can see it easily, and pay attention to how long you are in different heart rate training zones.

Setup

Plug in the battery (or a USB power cable).

The display will show that it is scanning for a BLE HRM to connect to.

Turn on the heart rate monitor and strap it to your inner arm.

Next, the CLUE will connect and then show dashed lines for the BPM and Percent of target while the monitor begins streaming the data.

Next, your BPM, Target percentage, and max heart rate are displayed.

Adjustment

Since the CLUE has buttons and cap sense inputs, let's use them!

Press the B button to increase the max HR value one unit at a time.

The A button will decrease it.

To make larger changes, hold the 2 cap sense pad while using the buttons. This will increment the tens place.

Alarm!

If your heart rate goes above 90% of your max HR, the target percentage text will turn red and the alarm buzzer on the CLUE will sound!

Press cap touch 0 to turn off the alarm or touch 1 to turn it back on. Also, take a rest!

This guide was first published on Feb 05, 2020. It was last updated on Feb 05, 2020.

This page (CLUE Heart Rate Trainer) was last updated on Nov 06, 2020.