Cycling cadence & Speed sensor

Last Updated on 10 augustus 2023 by Syds

Cycling cadence & Speed sensor

Onlangs m’n Polar M600 sporthorloge vervangen door een Garmin Forerunner 955. Daar moeten natuurlijk van allerlei sensoren aan gekoppeld worden. Je kunt die sensoren uiteraard kant-en-klaar kopen van de bekende merken zoals Polar, Garmin, Wahoo etc. Maar veel leuker is om ze zelf te maken. Als inspiratiebron heb ik de Wahoo Blue SC Speed en Candanssensor genomen:

Mijn opzet is als volgt:

  • Centraal staat een Seeed Xiao Studio ESP32C3 microcontroller, deze zorgt voor de rekenkracht en (BLE, Bluetooth) connectiviteit, is ultraklein en gebruikt weinig stroom
  • Deze wordt gevoed door een 3.7v oplaadbare Lipo batterij
  • De magneten op de spaak en crank worden uitgelezen met een reed switch
  • Om stroom te besparen wordt de ESP32C3 microcontroller automatisch in ‘Deepsleep’ modus gezet na 60 seconden idle te zijn geweest
  • Een draai aan het wiel of de crank zorgt ervoor dat de ESP32C3 microcontroller wordt opgestart
  • De wiel- en crank rotaties worden op basis van de GATT-specificaties via bluetooth gecommuniceerd naar afnemende devices (Garmin horloge, app op telefoon etc.)

Te bestellen artikelen:

ArtikelAantalPrijs p/stukTotaalWaar verkrijgbaar
Seeed Xiao Studio ESP32C31€ 6,50€ 6,50kiwi-electronics.com
3.7v Lipo accu 2000mAh1€ 4,42€ 4,42Aliexpress.com
Reed switch2€ 0,08€ 0,16Aliexpress.com
RGB led cathode 5mm1€ 0,07€ 0,15Aliexpress.com
Weerstand 220Ohm3Op voorraad
Weerstand 10K Ohm2Op voorraad
Weerstand 220K Ohm2Op voorraad
JST connector 2-voudig PCB mount1Op voorraad
Male header 2-voudig, haaks3Op voorraad
Male headers 7-voudig2Op voorraad
PLA Filament 1.75mm? meter€ 0,068Op voorraad
Rrskit 360 Graden Roterende Stuur Houder Voor Garmin1€ 4,15€ 4,15Aliexpress.com

Benodigdheden:

  • USB-C kabel + laptop/desktop

Gereedschap:

  • Soldeerbout + tin
  • Tangetje
  • 3D printer

Geinstalleerd en up-and-running:

  • python 3.10.7 of hoger
  • esptool (pip install esptool)
  • ampy (pip install adafruit-ampy)

Stap 1. Proefopstelling gemaakt

De Seeed Xiao Studio ESP32C3 heb ik voorzien van Micropython (zie ook mijn tutorial Micropython flashen op Seeed studio Xiao ESP32C3). Om de in de stap 2. ontwikkelde python code te testen heb ik twee 7-pins headers op het Xiao bordje gesoldeerd (alleen die pins gesoldeerd die gebruikt worden zodat ik ze gemakkelijk weer los kan solderen). Vervolgens het bordje op een breadboard gedrukt. Om de reed switch sensoren te emuleren heb ik twee tactile switches (vlakke puls schakelaars) m.b.v. een 10K weerstand op de breadboard gedrukt en een en ander met jumperkabels verbonden conform onderstaand schema:

Om het meten van het batterijspanning te simuleren heb ik een voltage devider van 10 en 47K toegevoegd die is aangesloten op de 5v pin van de ESP32C3. Dit brengt het voltage terug naar ongeveer 4.1v, vergelijkbaar met de piekspanning van een 3.7v Lipo batterij die volledig opgeladen is. Uiteraard kan de ESP32C3 geen 4.1v verdragen op zijn pins, daarom een twee voltage divider toepast in de vorm van een 10K en 4.7K weerstand in serie met een B100K potentiometer. Met behulp van de potentiometer kan het verloop van de batterijspanning gesimuleerd worden.

Tot slot een RGB Led toegevoegd die de status van de Cadencesensor gaat aangeven, namelijk:

  • Rood: Onvoldoende batterijspanning
  • Groen: Sensor operationeel
  • Blauw: Sensor via bluetooth geconnect aan device
Volledige proefopstelling

Stap 2. Python script ontwikkeld

Als basis heb ik de C++ code van Neal Markham gebruikt, te vinden op hackster.io: https://particle.hackster.io/neal_markham/ble-bicycle-speed-sensor-f60b80. Deze code heb ik omgezet naar Micropython en verder vervolmaakt.

from machine import deepsleep, Pin, ADC
import machine
import bluetooth
from ble_advertising import advertising_payload
from micropython import const
import time
import struct
import esp32

UPDATE_INTERVAL_MS                      = 2000
DEEP_SLEEP_INTERVAL_MS                  = 60000
lastUpdate                              = 0
lastwheelUpdate                         = 0
lastcrankUpdate                         = 0
swCount                                 = 0
wheel_rev_period                        = 0
crank_rev_period                        = 0
debouncePeriod                          = const(150)                    # interrupt debounce period, per magnet passby
lastInterruptMillis                     = 0

# Battery level Variables
lastBattery                             = 33
minBattery                              = 30                            # minBattery/10 = minimum voltage to prevent Lipo battery from damage

# Cycling Variables
cum_wheel_rev                           = 0
last_wheel_event                        = 0
cum_cranks                              = 0
last_crank_event                        = 0

# Cycling Speed and Cadence configuration
GATT_CSC_UUID                           = const(0x1816)
GATT_CSC_MEASUREMENT_UUID               = const(0x2A5B)
GATT_CSC_FEATURE_UUID                   = const(0x2A5C)
GATT_SENSOR_LOCATION_UUID               = const(0x2A5D)
GATT_SC_CONTROL_POINT_UUID              = const(0x2A55)

# Device Information configuration
GATT_DEVICE_INFO_UUID                   = const(0x180A)
GATT_MANUFACTURER_NAME_UUID             = const(0x2A29)
GATT_MODEL_NUMBER_UUID                  = const(0x2A24)

# CSC Measurement flags
CSC_MEASUREMENT_WHEEL_REV_PRESENT       = const(0x01)
CSC_MEASUREMENT_CRANK_REV_PRESENT       = const(0x02)

# CSC feature flags
CSC_FEATURE_WHEEL_REV_DATA              = const(0x01)
CSC_FEATURE_CRANK_REV_DATA              = const(0x02)
CSC_FEATURE_MULTIPLE_SENSOR_LOC         = const(0x04)

# Sensor location enum
SENSOR_LOCATION_OTHER                   = const(0)
SENSOR_LOCATION_TOP_OF_SHOE             = const(1)
SENSOR_LOCATION_IN_SHOE                 = const(2)
SENSOR_LOCATION_HIP                     = const(3)
SENSOR_LOCATION_FRONT_WHEEL             = const(4)
SENSOR_LOCATION_LEFT_CRANK              = const(5)
SENSOR_LOCATION_RIGHT_CRANK             = const(6)
SENSOR_LOCATION_LEFT_PEDAL              = const(7)
SENSOR_LOCATION_RIGHT_PEDAL             = const(8)
SENSOR_LOCATION_FROT_HUB                = const(9)
SENSOR_LOCATION_REAR_DROPOUT            = const(10)
SENSOR_LOCATION_CHAINSTAY               = const(11)
SENSOR_LOCATION_REAR_WHEEL              = const(12)
SENSOR_LOCATION_REAR_HUB                = const(13)
SENSOR_LOCATION_CHEST                   = const(14)
SENSOR_LOCATION_SPIDER                  = const(15)
SENSOR_LOCATION_CHAIN_RING              = const(16)

# Bluetooth 
_ADV_APPEARANCE_CADENCE_SENSOR          = const(1155)              # org.bluetooth.characteristic.gap.appearance.xml
sensor_name                             = const("myCadenceSensor")

# SC Control Point op codes
SC_CP_OP_SET_CUMULATIVE_VALUE           = const(1)
SC_CP_OP_START_SENSOR_CALIBRATION       = const(2)
SC_CP_OP_UPDATE_SENSOR_LOCATION         = const(3)
SC_CP_OP_REQ_SUPPORTED_SENSOR_LOCATIONS = const(4)
SC_CP_OP_RESPONSE                       = const(16)

# SC Control Point response values
SC_CP_RESPONSE_SUCCESS                  = const(1)
SC_CP_RESPONSE_OP_NOT_SUPPORTED         = const(2)
SC_CP_RESPONSE_INVALID_PARAM            = const(3)
SC_CP_RESPONSE_OP_FAI                   = const(4)

# Battery level
BLE_SIG_UUID_BATTERY_SVC                = const(0x180F)
BLE_SIG_BATTERY_LEVEL                   = const(0x2A19)

# IRQ
_IRQ_CENTRAL_CONNECT                    = const(1)
_IRQ_CENTRAL_DISCONNECT                 = const(2)
_IRQ_GATTS_INDICATE_DONE                = const(20)

# CSC simulation configuration
CSC_FEATURES                            = (CSC_FEATURE_WHEEL_REV_DATA | CSC_FEATURE_CRANK_REV_DATA | CSC_FEATURE_MULTIPLE_SENSOR_LOC)

CSC_UUID                                = bluetooth.UUID(GATT_CSC_UUID)                                                                 # "Cycling Speed and Cadence"	org.bluetooth.service.cycling_speed_and_cadence	0x1816	GSS
CSC_Measurement                         = (bluetooth.UUID(GATT_CSC_MEASUREMENT_UUID), bluetooth.FLAG_NOTIFY,)                           # "CSC Measurement"	org.bluetooth.characteristic.csc_measurement	0x2A5B
CSC_Feature                             = (bluetooth.UUID(GATT_CSC_FEATURE_UUID), bluetooth.FLAG_READ,)                                 # "CSC Feature"	org.bluetooth.characteristic.csc_feature	0x2A5C
Sensor_Location                         = (bluetooth.UUID(GATT_SENSOR_LOCATION_UUID), bluetooth.FLAG_READ,)                             # "Sensor location"	org.bluetooth.characteristic.sensor_location	0x2A5D
SC_ControlPoint                         = (bluetooth.UUID(GATT_SC_CONTROL_POINT_UUID), bluetooth.FLAG_INDICATE | bluetooth.FLAG_WRITE,) # "SC ControlPoint"	org.bluetooth.characteristic.sc_control_point	0x2A55

CyclingSpeedAndCadenceService           = (CSC_UUID, (CSC_Measurement, CSC_Feature, Sensor_Location, SC_ControlPoint,),)

# The battery level service allows the battery level to be monitored
BatteryLevel_UUID                       = bluetooth.UUID(BLE_SIG_UUID_BATTERY_SVC)
BatteryLevel                            = (bluetooth.UUID(BLE_SIG_BATTERY_LEVEL), bluetooth.FLAG_NOTIFY,)
BatteryLevel_service                    = (BatteryLevel_UUID, (BatteryLevel,),)

Services                                = (CyclingSpeedAndCadenceService, BatteryLevel_service,)

def millis():
    return round(time.time()*1000)

def switchIsr(pin):
    global swCount, wheel_rev_period, lastwheelUpdate, lastcrankUpdate, cum_wheel_rev, last_wheel_event, debouncePeriod
    global cum_cranks, crank_rev_period, last_crank_event, lastInterruptMillis
 
    if (millis() - lastInterruptMillis > debouncePeriod):
    
        swCount            += 1
        lastInterruptMillis = millis()
    
        if (pin == Pin(4)):
            wheel_rev_period    = millis() - lastwheelUpdate
            lastwheelUpdate     = millis()
            cum_wheel_rev      += 1
            last_wheel_event   += wheel_rev_period
        
        if (pin == Pin(2)):
            crank_rev_period    = millis() - lastcrankUpdate
            lastcrankUpdate     = millis()
            cum_cranks         += 1
            last_crank_event   += crank_rev_period
            
        measurements_update()
        print("Interrupt handling")
        
def blecsc_simulate_speed_and_cadence():
    global wheel_rev_period, crank_rev_period
    global cum_wheel_rev, last_wheel_event, cum_cranks, last_crank_event

    lastInterruptMillis = millis() # fake Interrupt on D0 & D2
    
    # Calculate simulated measurement values
    wheel_rev_period  = (36*64*WHEEL_CIRCUMFERENCE_MM) / (625*csc_sim_speed_kph)
    cum_wheel_rev    += 1
    last_wheel_event += wheel_rev_period

    crank_rev_period  = (60*64) / csc_sim_crank_rpm
    cum_cranks       += 1
    last_crank_event += crank_rev_period

def measurements_update():
    global swCount
   
    print("Measurement Update Loop")
    
    if (swCount):

        # unsigned long currentTime = millis()
        
        swCount = 0
       
        print("Wheel_Rev_Period : %d", wheel_rev_period)
        print("LastwheelUpdate : %d", lastwheelUpdate)
        print("Cum_Wheel_Rev : %d", cum_wheel_rev)
        print("Crank_Rev_Period : %d", crank_rev_period)
        print("LastcrankUpdate : %d", lastcrankUpdate)
        print("Cum. Cranks : %d", cum_cranks)

    print("Finished Measurement Update Loop")
    
def convert(x, in_min, in_max, out_min, out_max):
    return (x - in_min) * (out_max - out_min) // (in_max - in_min) + out_min

class BLEcsc:
    def __init__(self, ble, name=sensor_name):
        self._ble = ble
        self._ble.active(True)
        self._ble.irq(self._irq)
        ((self._CSC_Measurement_handle, self._CSC_Feature_handle, self._Sensor_Location_handle, self._SC_ControlPoint_handle,), (self._BatteryLevel_handle,), ) = self._ble.gatts_register_services((Services))
        self._connections = set()
        self._payload = advertising_payload(
            name=name, services=[CSC_UUID], appearance=_ADV_APPEARANCE_CADENCE_SENSOR
        )

        self._advertise()

    def _irq(self, event, data):
        # Track connections so we can send notifications.
        if event == _IRQ_CENTRAL_CONNECT:
            conn_handle, _, _ = data
            self._connections.add(conn_handle)
        elif event == _IRQ_CENTRAL_DISCONNECT:
            conn_handle, _, _ = data
            self._connections.remove(conn_handle)
            # Start advertising again to allow a new connection.
            self._advertise()
        elif event == _IRQ_GATTS_INDICATE_DONE:
            conn_handle, value_handle, status = data
            
    def set_csc(self, csc, notify=False, indicate=False):
        # Write CSC Feature
        self._ble.gatts_write(self._CSC_Feature_handle, struct.pack("2s", CSC_FEATURES.to_bytes(2, 'little'))) # Change to your setup
        if notify or indicate:
            for conn_handle in self._connections:
                if notify:
                    # Notify connected centrals.
                    self._ble.gatts_notify(conn_handle, self._CSC_Feature_handle)
                if indicate:
                    # Indicate connected centrals.
                    self._ble.gatts_indicate(conn_handle, self._CSC_Feature_handle)         
        
        # Write sensor location
        self._ble.gatts_write(self._Sensor_Location_handle, struct.pack("1s", SENSOR_LOCATION_RIGHT_CRANK.to_bytes(1, 'little'))) # Change to your own sensor location
        if notify or indicate:
            for conn_handle in self._connections:
                if notify:
                    # Notify connected centrals.
                    self._ble.gatts_notify(conn_handle, self._Sensor_Location_handle)
                if indicate:
                    # Indicate connected centrals.
                    self._ble.gatts_indicate(conn_handle, self._Sensor_Location_handle)         

        # Write the local value, ready for a central to read.
        self._ble.gatts_write(self._CSC_Measurement_handle, struct.pack("%ds" % 11, csc))
        if notify or indicate:
            for conn_handle in self._connections:
                if notify:
                    # Notify connected centrals.
                    self._ble.gatts_notify(conn_handle, self._CSC_Measurement_handle)
                if indicate:
                    # Indicate connected centrals.
                    self._ble.gatts_indicate(conn_handle, self._CSC_Measurement_handle)
                    
        # Write batterylevel
        self._ble.gatts_write(self._BatteryLevel_handle, struct.pack("<B", convert(lastBattery, 29, 33, 0, 100)))
        if notify or indicate:
            for conn_handle in self._connections:
                if notify:
                    # Notify connected centrals.
                    self._ble.gatts_notify(conn_handle, self._BatteryLevel_handle)
                if indicate:
                    # Indicate connected centrals.
                    self._ble.gatts_indicate(conn_handle, self._BatteryLevel_handle)


    def _advertise(self, interval_us=500000):
        self._ble.gap_advertise(interval_us, adv_data=self._payload)

# Init lastInterruptMillis, lastwheelUpdate and lastcrankUpdate
lastInterruptMillis = millis()
lastwheelUpdate     = millis()
lastcrankUpdate     = millis()

# Set up pin modes for input, interrupt for input
D0 = Pin(2, Pin.IN, Pin.PULL_DOWN)                  # Wheel reed-sensor GPIO2
D0.irq(trigger=Pin.IRQ_FALLING, handler=switchIsr)

D2 = Pin(4, Pin.IN, Pin.PULL_DOWN)                  # Crank reed-sensor GPIO4
D2.irq(trigger=Pin.IRQ_FALLING, handler=switchIsr)

D1 = ADC(Pin(3))                                    # Batterylevel GPIO3
D1.atten(ADC.ATTN_11DB)                             # Full range: 0 - 3.3v

BLE = bluetooth.BLE()
csc = BLEcsc(BLE)

# Setup RGB Led
Red   = Pin(5, Pin.OUT, Pin.PULL_DOWN)              # Red led
Green = Pin(6, Pin.OUT, Pin.PULL_DOWN)              # Green led
Green.value(1)
Blue  = Pin(7, Pin.OUT, Pin.PULL_DOWN)              # Blue led

# Set up deep_sleep mode
esp32.wake_on_ext1(pins = (D0, D2,), level = esp32.WAKEUP_ANY_HIGH)


# Loop
while True:
    if (millis() - lastUpdate >= UPDATE_INTERVAL_MS):
        lastUpdate = millis()
        
        if len(csc._connections) != 0:
            Red.value(0)
            Green.value(0)
            Blue.value(1)
            
            # blecsc_simulate_speed_and_cadence() # Simulate wheel and crank movement
            swCount    += 1
           
            data_buf     = bytearray(11)
 
            # The Cycle Measurement Characteristic data is defined here:
            # https://www.bluetooth.com/wp-content/uploads/Sitecore-Media-Library/Gatt/Xml/Characteristics/org.bluetooth.characteristic.csc_measurement.xml
            # First byte is flags. Wheel Revolution Data Present (0 = false, 1 = true) = 1, Crank Revolution Data Present (0 = false, 1 = true), so the flags are 0x03 (binary 11 converted to HEX).
            # buf[0]=0x03;
            
            data_buf[0] |= CSC_MEASUREMENT_WHEEL_REV_PRESENT | CSC_MEASUREMENT_CRANK_REV_PRESENT
             
            # Setting values for cycling measures (from the Characteristic File)
            # Cumulative Wheel Revolutions (unitless)
            # Last Wheel Event Time (Unit has a resolution of 1/1024s)
            # Cumulative Crank Revolutions (unitless)
            # Last Crank Event Time (Unit has a resolution of 1/1024s)
            
            data_buf[1:4]  = cum_wheel_rev.to_bytes(4, 'little')
            data_buf[5:6]  = int(last_wheel_event).to_bytes(2, 'little')
            data_buf[7:8]  = cum_cranks.to_bytes(2, 'little')
            data_buf[9:10] = int(last_crank_event).to_bytes(2, 'little')

            csc.set_csc(data_buf, notify=True, indicate=False)

            # Read batterylevel
            lastBattery    = convert(D1.read(), 0, 4095, 0, 33)           # Read batterylevel
            print("Battery voltage:", lastBattery)
            
            time.sleep_ms(1000)          
        
        else:
            Red.value(0)
            Green.value(1)
            Blue.value(0)

            print("Not yet connected")
            cum_wheel_rev    = 0                  # keep the cumulative wheel revolutions at 0 whilst not connected
            cum_cranks       = 0                  # keep the cumulative crank revolutions at 0 whilst not connected
            last_wheel_event = 0                  # keep the last wheel event at 0 whilst not connected
            last_crank_event = 0                  # keep the last wheel event at 0 whilst not connected
            
    if ((millis() - lastInterruptMillis >= DEEP_SLEEP_INTERVAL_MS) or (lastBattery < minBattery)):
        if (lastBattery < minBattery):
            Green.value(0)
            Blue.value(0)
            Red.value(1)
        time.sleep(10)
        print("Going to sleep now")
        deepsleep()

Toelichting:

  • Voor het advertisen van de bluetooth services maak ik gebruik van het ble_advertising example van de micropython github pagina: https://github.com/micropython/micropython/blob/master/examples/bluetooth/ble_advertising.py
  • Na de initialisatie van de verschillende micropython bibliotheken worden de verschillende variabelen en constanten gedefinieerd
  • Vervolgens worden de bluetooth objecten en characteristic voor CSC measurement, feature, … en batterylevel gedefinieerd, dit conform de GATT specificatie supplement voor bluetooth: https://www.bluetooth.com/specifications/specs/gatt-specification-supplement-5/
  • Daarna worden er een aantal functies gedefinieerd, namelijk:
    • millis(), python equivalent voor de C++ functie millis()
    • switchIsr(), deze functie wordt aangeroepen bij iedere interrupt (lees iedere keer dat de reed-switch contact maakt) van de GPIO’s GPIO0 (D0) en GPIO4 (D2), deze functie zorgt voor ophoging van het aantal wiel- en crank omwentelingen
    • blecsc_simulate_speed_and_cadence(), simulatie functie, kun je aanroepen als je nog geen proefopstelling of reed-switches hebt, deze simuleert wiel- en crank omwentelingen
    • convert(), equivalent voor de C++ functie map()
  • Vervolgens wordt de class BLEcsc gedefinieerd, met daarbinnen de volgende functies:
    • __init__(): initialiseert bluetooth en start advertising van de services
    • _irq(): interrupt afhandeling van een nieuwe connectie
    • set_csc(): schrijft de gemeten wiel- en crankomwentelingen en batterijlevel naar de geconnecteerde GATT-clients
    • _advertise(): hulp functie voor het adverteren van de services
  • In de Setup sectie worden een aantal variabelen van een waarde voorzien, de GPIO’s geinitialiseerd en de wake_on_ext1 functie van GPIO’s GPIO0 en GPIO4 geactiveerd, ook worden de GPIO’s voor de RGB led geinitialiseerd
  • Daarna volgt de loop die er iedere 2 seconden voor zorgt dat als er een GATT-client geconnecteerd is de gemeten wiel- en crank omwentelingen en het level van de batterij doorgegeven wordt aan betreffende GATT-client

De benodigde files kun je uiteraard downloaden:

Filedownload locatieToelichting
ble_advertising.pyGithubBibliotheek voor het advertisen van bluetooth services
myCadenceSensor.pyhttps://drive.google.com/file/d/1Mlt9k7HuetQoRqaNb_Dhp0hOzAs3T0m1/view?usp=share_link
install.bathttps://drive.google.com/file/d/1-gwOdbaXByOzhqYYPvZa_Vde9yyI_Aqi/view?usp=share_linkInstallatie script, pas wel eerst de COM poort en eventueel bestandslocaties aan
main.pyhttps://drive.google.com/file/d/19i0vmKWKnd4CZCgp9zUTcgJQYYvQg8vf/view?usp=share_linkZorgt ervoor dat myCadenceSensor.py automatish gestart wordt na (re)boot van de ESP32C3

Om de myCadenceSensor software én Micropython software te installeren op de ESP32C3 kun je het batch-commando bestand install.bat gebruiken. Pas wel eerst de COM-poort en eventueel bestandslocatie van de files aan. Sluit de ESP32C3 middels een USB-C kabel aan op je laptop/desktop.

Stap 3. Testen python script + connectie met GATT-client

Na de proefopstelling bij elkaar gestoken te hebben op een breadboard, en de ESP32C3 geprepareerd te hebben m.b.v. het install.bat script is het tijd om één en ander te testen.

Zodra de ESP32C3 aangesloten wordt op een stroombron (laptop of adapter) begint hij met het adverteren van de bluetooth services onder de naam “myCadenceSensor” (pas eventueel deze naam aan op regel 70 van myCadenceSensory.py). Let op, probeer niet vanaf je telefoon of laptop het device te pairen, dit is namelijk niet geimplementeerd binnen de bluetooth stack van de Micropython distributie van de generic esp32c3 en resulteert in een crash van de software.

Voor een eerste test kun je het beste de app “nRF Connect” van Nordic Semiconductor installeren op je telefoon, deze is te vinden in de Google play store: https://play.google.com/store/apps/details?id=no.nordicsemi.android.mcp&hl=nl&gl=US

Nadat je de app gestart hebt, kies binnen “Devices” voor het tabblad “Scanner” en klik op “SCAN”:

Als het goed is verschijnt nu de device “myCadenceSensor” in het overzicht, klik op “CONNECT”, er wordt nu een nieuw tabblad geopend:

Klik vervolgens op “Cycling Speed and Cadence”, en je krijgt de characteristics van de bluetooth service te zien, inclusief hun waarden:

Druk nu een paar keer op de pushbuttons van de proefopstelling om wiel- en crank omwentelingen te simuleren, je ziet vervolges de waarden van Wheel rev en Crank rev oplopen:

Let op, als er 60 seconden (DEEP_SLEEP_INTERVAL_MS op regel 11) geen wiel- of crank omwentelingen zijn gesignaleerd, gaat het device automatisch op Deepsleep en wordt de connectie verbroken !

Nu dit werkt is de volgende stap om een verbinding te maken met je sporthorloge, in mijn geval een Garmin Forerunner 955 Solar. De volgende instructies zijn speficiek voor dit horloge, raadpleeg de handleiding van jou horloge/device voor de instellingen die nodig zijn om verbinding te maken met een Cycling speed- and cadence sensor.

Hou de knop (3) “UP” ingedrukt, het volgende scherm verschijnt:

Druk de knop (4) “DOWN” meerdere keren in totdat de instelling “Sensors en accessoires” verschijnt:

Druk op de knop (5) “Start-Stop” om deze keuze te bevestigen, en het volgende scherm verschijnt:

Druk nogmaals op de knop (5) “Start-Stop” om de keuze te “Voeg nieuw toe” te bevestigen, en het volgende scherm verschijnt:

Druk de knop (4) “DOWN” meerdere keren in totdat de optie “Snelheid/cadanssensor” verschijnt. Bevestig deze keuze met de knop (5) “Start-stop”, en het horloge gaat op zoek naar de “myCadenceSensor” en vind deze

Druk op de knop (5) “Start-stop” om de “myCadenceSensor” toe te voegen. De sensor wordt automatisch verbonden. Druk daarna meerdere keren op de knop (6) “Back” om terug te keren naar het hoofdscherm.

Om de werking van de Cycling speed en cadence sensor te testen starten we nu een fietstraining. Druk langere tijd op de knop (5) “Start-stop”, het volgende scherm verschijnt:

Druk meerdere keren op de knop (4) “DOWN” om naar de activiteit fietsen te navigeren:

Druk tweemaal op de knop (5) “Start-stop” om de training te starten, de “myCadenceSensor” wordt automatisch verbonden:

Druk nu wederom een aantal keren op de pushbuttons van de proefopstelling, en je ziet de afstand en snelheid oplopen:

Nadat je de training hebt opgeslagen (Druk knop (5) “Start-stop” langere tijd in, en kies binnen het menu daarna voor “Sla op”) kun je de training raadplegen in Garmin Connect:

De software werkt, nu de hardware.

Stap 3. PCB ontwerp

M.b.v. Autodesk Eagle heb ik het volgende ontwerp voor een printplaat gemaakt.

Partlist:

Toelichting:

  • Voorlopig twee 7-pins female headers ingetekend, dit om de ESP32C3 eenvoudig te kunnen vervangen indien nodig. Als de beschikbare ruimte in de behuizing dit niet toelaat, dan vervang ik die door male headers en soldeer ik de ESP32C3 hierop vast.

Note: 10-8 Bij het assembleren van de PCB kwam ik tot de conclusie dat het monteren van de ESP32C3 op de PCB m.b.v. male- en female headers teveel hoogte/ruimte zou innemen, daarom besloten om de ESP32C3 rechtstreeks m.b.v. male headers op de PCB te solderen. Is wat minder onderhoudsvriendelijk, maar het is niet anders.

  • De RGB led komt haaks op de printplaat gesoldeerd en steekt aan de bovenkant door de behuizing, aandacht voor waterdicht monteren hiervan
  • In Pad 1 en Pad 2 worden twee draadjes gesoldeerd, en aan de achterkant van de ESP32C3 op de batterypads gesoldeerd, misschien vervang ik dit nog door een JST aansluiting, afhankelijk van uitkomst 1e bullit
  • Op de aansluitingen voor de Crank reel, Charger en Switch worden 90 graden 2 pins male headers gesoldeerd, zodat de behuizing van de electronica en batterij eenvoudig van het frame afgeklikt kan worden om de batterij op te kunnen laden.

Uiteraard zijn de CAD en gerber files weer te downloaden:

FileDownload linkToelichting
cadence_sensor.brdhttps://drive.google.com/file/d/1ex3KKgROhpnCS2rucznjsG9N9lS3dZpj/view?usp=drive_linkAutodesk Eagle: Board
cadense_sensor.schhttps://drive.google.com/file/d/1zHE4fW12rUPSz7ChkjH9MyqzBjXZU-FX/view?usp=drive_linkAutodesk Eagle: Schema
cadense_sensor_2023_07_08.ziphttps://drive.google.com/file/d/1N4K_KAIcQnwwZ3KpXSGLUUkTolDhAHhP/view?usp=drive_linkZIP-file met de gerber files

Stap 4. PCB besteld

Bij PCB gogo de in stap 3. ontwikkelde PCB besteld, hiervoor de volgende instellingen gebruikt:

De kosten bedragen maar liefst $ 5,- voor 5 printplaten. Heb deze bestelling gecombineerd met 2 andere PCB’s en daardoor de verzend- en inklaringskosten laag kunnen houden. Totaal komen de kosten op zo’n €12,50 inclusief inklaringskosten.

Stap 5. PCB assembleren

De PCB’s zijn binnengekomen, zien er goed uit. Jammer is dat de partnummers en -waarden ontbreken op de PCB, maar zal wel een verkeerde keuze hebben gemaakt in het toekennen een label aan een layer of het exporteren naar Gerber.

We gaan de componenten op de PBC solderen, benodigd zijn:

  • 1x Reed switch
  • 1x RGB led cathode 5mm
  • 3x Weerstand 220 Ohm
  • 2x Weerstand 10K Ohm
  • 2x Weerstand 220K Ohm
  • 4x JST connector 2-voudig PCB mount, haaks
  • 2x male headers 7-voudig
  • 1x PCB
  • 1x ESP32C3
  • Soldeerbout + tin
  • Tangetje

De headers had ik al eerder op de ESP32C3 gesoldeerd, dus deze stap sla ik nu over. Heb er voor gekozen om alleen die pinnen vast te solderen die gebruikt worden, dus D0, D1, D2, GND en 3V3. Mocht de ESP32C3 het begeven, dan hoef ik alleen die pinnen los te solderen om een vervangende ESP32C3 te plaatsen. We beginnen met het solderen van de weerstanden op de PCB, benodigd zijn:

  • 3x Weerstand 220 Ohm
  • 2x Weerstand 10K Ohm
  • 2x Weerstand 220K Ohm

Steek de weerstanden op de juiste plek door de PCB, en soldeer ze vast.

Geef een reactie

Je e-mailadres wordt niet gepubliceerd. Vereiste velden zijn gemarkeerd met *