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
# 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 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.
""" 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.
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.
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.
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.
Text editor powered by tinymce.