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.

Installing Project Code

To use with CircuitPython, you need to first install a few libraries, into the lib folder on your CIRCUITPY drive. Then you need to update code.py with the example script.

Thankfully, we can do this in one go. In the example below, click the Download Project Bundle button below to download the necessary libraries and the code.py file in a zip file. Extract the contents of the zip file, open the directory CLUE_Cycling_Simple/ and then click on the directory that matches the version of CircuitPython you're using and copy the contents of that directory to your CIRCUITPY drive.

Your CIRCUITPY drive should now look similar to the following image:

CIRCUITPY
# SPDX-FileCopyrightText: 2020 John Park for Adafruit Industries
#
# SPDX-License-Identifier: MIT

"""
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.

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.

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.

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 Mar 24, 2023.

Text editor powered by tinymce.