Code the CLUE in CircuitPython

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_cycling_speed_and_cadence.py
  • 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

"""
Read cycling speed and cadence data from a peripheral using the standard BLE
Cycling Speed and Cadence (CSC) Service.
Works with single sensor (e.g., Wahoo Blue SC) or sensor pair, such as Wahoo RPM
"""

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_cycling_speed_and_cadence import CyclingSpeedAndCadenceService

clue_data = clue.simple_text_display(title="Cycle Revs", title_scale=1, text_scale=3)

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


while True:
    print("Scanning...")
    # Save advertisements, indexed by address
    advs = {}
    for adv in ble.start_scan(ProvideServicesAdvertisement, timeout=5):
        if CyclingSpeedAndCadenceService in adv.services:
            print("found a CyclingSpeedAndCadenceService advertisement")
            # Save advertisement. Overwrite duplicates from same address (device).
            advs[adv.address] = adv

    ble.stop_scan()
    print("Stopped scanning")
    if not advs:
        # Nothing found. Go back and keep looking.
        continue

    # Connect to all available CSC sensors.
    cyc_connections = []
    for adv in advs.values():
        cyc_connections.append(ble.connect(adv))
        print("Connected", len(cyc_connections))

    # Print out info about each sensors.
    for conn in cyc_connections:
        if conn.connected:
            if DeviceInfoService in conn:
                dis = conn[DeviceInfoService]
                try:
                    manufacturer = dis.manufacturer
                except AttributeError:
                    manufacturer = "(Manufacturer Not specified)"
                print("Device:", manufacturer)
            else:
                print("No device information")

    print("Waiting for data... (could be 10-20 seconds or more)")
    # Get CSC Service from each sensor.
    cyc_services = []
    for conn in cyc_connections:
        cyc_services.append(conn[CyclingSpeedAndCadenceService])
    # Read data from each sensor once a second.
    # Stop if we lose connection to all sensors.

    while True:
        still_connected = False
        wheel_revs = None
        crank_revs = None
        for conn, svc in zip(cyc_connections, cyc_services):
            if conn.connected:
                still_connected = True
                values = svc.measurement_values
                if values is not None:  # add this
                    if values.cumulative_wheel_revolutions:
                        wheel_revs = values.cumulative_wheel_revolutions
                    if values.cumulative_crank_revolutions:
                        crank_revs = values.cumulative_crank_revolutions

        if not still_connected:
            break
        if wheel_revs:   # might still be None
            print(wheel_revs)
            clue_data[0].text = "Wheel: {0:d}".format(wheel_revs)
            clue_data.show()
        if crank_revs:
            print(crank_revs)
            clue_data[2].text = "Crank: {0:d}".format(crank_revs)
            clue_data.show()
        time.sleep(0.1)

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_cycling_speed_and_cadence 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
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_cycling_speed_and_cadence import CyclingSpeedAndCadenceService

clue_data = clue.simple_text_display(title="Cycle Revs", title_scale=1, text_scale=3, num_lines=3)

# 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 Cycling Speed & Cadence service.

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
while True:
    print("Scanning...")
    # Save advertisements, indexed by address
    advs = {}
    for adv in ble.start_scan(ProvideServicesAdvertisement, timeout=5):
        if CyclingSpeedAndCadenceService in adv.services:
            print("found a CyclingSpeedAndCadenceService advertisement")
            # Save advertisement. Overwrite duplicates from same address (device).
            advs[adv.address] = adv

    ble.stop_scan()
    print("Stopped scanning")
    if not advs:
        # Nothing found. Go back and keep looking.
        continue

    # Connect to all available CSC sensors.
    cyc_connections = []
    for adv in advs.values():
        cyc_connections.append(ble.connect(adv))
        print("Connected", len(cyc_connections))

    # Print out info about each sensors.
    for conn in cyc_connections:
        if conn.connected:
            if DeviceInfoService in conn:
                dis = conn[DeviceInfoService]
                try:
                    manufacturer = dis.manufacturer
                except AttributeError:
                    manufacturer = "(Manufacturer Not specified)"
                print("Device:", manufacturer)
            else:
                print("No device information")

    print("Waiting for data... (could be 10-20 seconds or more)")
    # Get CSC Service from each sensor.
    cyc_services = []
    for conn in cyc_connections:
        cyc_services.append(conn[CyclingSpeedAndCadenceService])
    # Read data from each sensor once a second.
    # Stop if we lose connection to all sensors.

Receiving Revolution Values

Once connected, we create a pair of variables to store the wheel and crank revolution values as wheel_revs and crank_revs

When we receive values for these two attributes they are displayed on the CLUE screen using the clue_data[].text command, and clue_data.show() to update the display.

Download: file
while True:
        still_connected = False
        wheel_revs = None
        crank_revs = None
        for conn, svc in zip(cyc_connections, cyc_services):
            if conn.connected:
                still_connected = True
                values = svc.measurement_values
                if values is not None:  # add this
                    if values.cumulative_wheel_revolutions:
                        wheel_revs = values.cumulative_wheel_revolutions
                    if values.cumulative_crank_revolutions:
                        crank_revs = values.cumulative_crank_revolutions

        if not still_connected:
            break
        if wheel_revs:   # might still be None
            print(wheel_revs)
            clue_data[0].text = "Wheel: {0:d}".format(wheel_revs)
            clue_data.show()
        if crank_revs:
            print(crank_revs)
            clue_data[2].text = "Crank: {0:d}".format(crank_revs)
            clue_data.show()
        time.sleep(0.1)

Let's check it out in action!

This guide was first published on Feb 10, 2020. It was last updated on Feb 10, 2020. This page (Code the CLUE in CircuitPython) was last updated on Feb 20, 2020.