Wednesday 24 February 2021

Building an Cadence Display for a BLE Cycling Sensor with ESP32 / MicroPython

With lockdowns and winter I converted my bike into a "turbo trainer" to cycle indoors. A cheap eBay cadence sensor can then report on my RPM, however when doing guided exercises on my Phone, I cant use the app to see my cadence. This makes it difficult when the coaches say "now hit upper 90s on your RPM".

I decided to try and build a cheap display to show the cadence using an ESP32 and a SSD1306 OLED screen: total cost ~£10

 It turned out to be one of the harder projects I've completed. You know those projects where someone solves something with bold code, smart electronics and a nifty 3D print? This isn't that, this is someone struggling through a bunch of areas and learning as they went. The code is probably wrong.

Some of the challenges covered in this blog:

  • Micropython on ESP32
  • Bluetooth Low Energy (BLE)
  • BLE in micropython
  • Understanding the sensor
  • Getting it all together

Adventures in Micropython

I knew this project would be a lot of experimentation and as I'm more comfortable with Python, figured it was best to use that. Also, you can have a live "shell" on the ESP32 with Python which would allow experimentation without uploading new code.

There are a number of blogs of coding python on the ESP32. I decided on the Adafruit method, as it seemed to require the least installed on my windows computer, and I can use VS Code with normal linting to write the code.

A good blog was: https://www.digikey.co.uk/en/maker/projects/micropython-basics-load-files-run-code/fb1fcedaf11e4547943abfdd8ad825ce

Then to develop I would just use VS Code, and when deploying: run the ampy module in the directory where I was developing to move main.py onto the esp32.

The guides will recommend that you add in switches etc to switch into REPL (python shell) mode, but instead I just used Putty as my Serial interface, which lets you send Ctrl-C to kill the python script and drop you into the shell.

A quick warning was this GitHub issue, you may need to use an older micropython firmware if you want BT LE to work.

Another note here: If you installed python using the Windows App Store then anything you pull with Pip is in a horrible chain of subdirectories under your user's AppData\Local\Packages\PythonSoftware.. directory.

Bluetooth Low Energy

There are a number of blogs on Bluetooth Low Energy in micropython but none of them did the full job helping me understand so I'll try here. Some of the theory in this Blog is a good start https://learn.adafruit.com/bluetooth-bicycle-speed-cadence-sensor-display-with-clue/understanding-ble

A real struggle with this project is that I couldn't find many examples or docs on how to do this in Python, so will attempt to detail here.

But here are the key bits:

Devices will advertise themselves

In the advertisement:

  • all devices will have a MAC address of the form 11:22:33:44:55:66
  • they may also have extended information like a common name

Once you connect to a device, you can discover it's services. These will have GUIDs and potentially that GUID maps to a name such as "Generic Access".

A Service can be probed for it's "Characteristics" which will be a "handle" (small integer), details on whether the handle can be read/written etc and maybe a name.

As an example:

A device advertises its address: 11:22:33:44:55:66 and name: "FitBob".

You connect to it and its services are:

  • 19b3530b-c999-4184-92b5-2c3b60a7e926
  • 44bd726c-3d6d-4073-993f-c487437917ab

You query the characteristics for the first GUID and see that it has two characteristics

  • Handle 4 - read only - name: "Steves FitBob"
  • Handle 6 - read only - Version: 4

There are many recommendations in blogs of apps you can use to explore BLE, however they all suffer from a similar issue: its hard to get technical details out like the MAC address, and specific handles. I found that the "Bluetooth LE Explorer" program in the Windows Store on Win10 from Microsoft was by far the best for exploring devices and understanding them. Highly Recommended.


Connecting to BLE in Micropython

So, you understand your device a bit, lets try connecting to it

In Micropython starting a connection means 

bt = ubluetooth.BLE() #create an object
bt.active(True) #activate BT, this wierdly takes upto 30 seconds each time

bt.gap_scan(3000) # scan for advertising devices for 3000ms, you can give more specific numbers if needed like how long the scan window is, see documentation at https://docs.micropython.org/en/latest/library/ubluetooth.html
Now this was where I initially struggled quite a lot. The ubluetooth module doesn't return data like a function in python, instead it raises an interrupt that you have to handle. So you fire off something (like a scan) and each result is raised as an interrupt.
You therefore need a function to handle it like:
def bt_irq(event, data):

    print(str(event))
Which you pass in to the bt instance (before you do anything like a scan) using:
bt.irq(bt_irq)
Everything has its own irq number: a scan result is 5, a scan complete is 6 for example. A good list is and how to handle each event code can be found near the top of https://docs.micropython.org/en/latest/library/ubluetooth.html although I also found useful this GitHub Issue

Once you have irqs handled (I slowly built them up as I needed them), you can start connecting
adr = binascii.unhexlify(b'112233445566') #puts the address in a format that the next function can handle
bt.gap_connect(1,adr,10000) # connects
bt.gattc_discover_services(0) # the 0 is for the connection handle, starts at 0 if you have only connected to 1 device, it will return the services and their handles
bt.gattc_discover_characteristics(0, 14, 24) # give the handle ranges from the last call to discovered characteristics.

Understanding your target device

Exploring the device in "BT Low Energy Explorer" I found its MAC address and the cycle profile. This is covered a bit by https://learn.adafruit.com/bluetooth-bicycle-speed-cadence-sensor-display-with-clue/cycling-speed-and-cadence-service and the full documentation is available at https://www.bluetooth.com/specifications/specs/cycling-speed-and-cadence-profile-1-0/

Looking at the Cycling Cadence service, you can see it has a "Measurement" characteristic. But how to read it? 


I assumed I could poll it, but this service is a notification service. I.e. you write to it telling it you want it to notify you, it starts to notify you with each notification raising an irq event.
Clicking on the Measurement service in BT LE Explorer gives you the handle to write to in order to trigger notifications, 27 in this case.


So the python to activate notifications in python is:

bt.gattc_write(0,27, struct.pack('<h', 1), 1) # from https://github.com/micropython/micropython/issues/6185#issuecomment-650848053

From then, sweet sweet measurements start coming in.

I used excerpts from some of the code from a related AdaFruit project to translate the data 
However, the data only gives you:

  • Count of how many revolutions of the pedal
  • Time in 1/1024 of a second from last crank to this 

Which you then need to convert to RPM. I am particularly bad at Python and maths so this wasn't easy, but the below gives me reasonable output. I'm using globals because remember it gets called by the irq event and so you can't pass in variables.

global last_crank_count
global last_crank_time
global rpm
if crank_time >= last_crank_time:
    if crank_count-last_crank_count == 0:
        rpm = 0
    rpm = (60/((crank_time - last_crank_time)/1024))*(crank_rev_count-last_crank_count)
last_crank_count = crank_count
last_crank_time = crank_time

Putting it all together

I built a quick breadboard with an ESP32 on and my I2C SSD1306 OLED screen. I discovered way too late into this project that there is not good micropython SSD1306 support so the text is tiny as you cant customise text size. 
I uploaded the code and turned it on. It entirely failed to find the BLE cycle cadence sensor.

A few days of debugging followed:
  • taking battery out of sensor and replacing worked about 10% of the time
  • I could see it on my phone
  • I could often see it on Windows
  • I could rarely see it on the ESP32
Eventually a kind soul told me that once a BLE device had been connected to, it often stops advertising. And so my phone was the culprit, seizing it whenever it saw it appear, before the ESP32 could. Once I unpaired it from phone, it worked perfectly. It was time to build it out on a project board and use scrap wood to give it a home as need practice at woodworking:

Future Plans


Now I know what I'm doing (a bit) I will rewrite in C/Arduino. This is for a couple of reasons

  • Better OLED support so dont have to squint
  • The 30s to boot Bluetooth is a pain
  • Practice C
Hopefully some of this was useful for you in avoiding some of the traps. It was a good project and pushed me, it's rare I have a project where there isn't a basically finished project on GitHub to be inspired by/copy paste from. When most of the tips are coming from GitHub issues or reading source, rather than StackOverflow is a sign you're not on easy mode anymore too. Crucially, this worked and is already thoroughly useful: 

Total cost about £10 and a few evenings of entertainment / frustration.


No comments:

Post a Comment