Tuesday, 10 November 2020

Micro:bit Bluetooth communications with RaspberryPi

I have acquired a number of micro:bits for a primary school Code Club - currently in abeyance because of COVID-19 constraints - and I've been thinking about useful experiments and instrumentation.

One of the basic models is collect data over a period of time and then look for patterns, make associations and draw conclusions. A microbit doesn't really have a means to store data over an arbitrary length of time, let alone process it in any meaningful way. For that you need a "proper" computer. So how do you get machine-readable data off a microbit?

The first way is to write the data down a serial link over a USB connection to a PC. Then since that requires a terminal program to "catch" the serial data, the terminal program has to write it to a file. Then what if you want to use multiple microbits? That means multiple USB connections, wires etc., multiple terminal program instances and so on. Of course, you could write a Python program to do all that...

It seemed more interesting to use Bluetooth if possible. No wires!! So I've been investigating python-bluezero, which provides a handy Python interface to the Linux BlueZ Bluetooth software. It also includes a microbit interface, which is extremely handy. In fact, the software includes 3 different levels of interaction with BlueZ, all based on the Linux d-Bus IPC mechanism, requiring greater or lesser (in my case!) knowledge of the underlying mechanism. 

Well, dear reader, it works. Certainly the simple, polling-style interfaces seem to work just fine. You can scan the microbit for temperature, button state, LED state etc., and also set LED state and send max. 20 character text strings to display. Pretty handy! I've got it working with a single microbit, and will have a go at interfacing to 2+ microbits simultaneously. Python allows for multiple instances of the Microbit class, so I can presumably handle each one separately in a single Python process. That would allow for, for example, differential temperature measurements across a room, or inside/outside, taken simultaneously. Nice.

One strange behaviour I've noticed is that the callback for Button presses doesn't seem to work. This is in microbit.py:

def _decode_btn_a(self, *button_values):
    """Decode button A state and pass on to user callback."""
    if 'Value' in button_values[1]:
        self.user_btn_a_callback(int(button_values[1]['Value'][0]))

def subscribe_button_a(self, user_callback):
    """
    Execute user_callback on Button A being pressed on micro:bit
    :param user_callback: User callback method receiving the button state
    :return:
    """
    self.user_btn_a_callback = user_callback
    self._btn_a_state.add_characteristic_cb(self._decode_btn_a)
    self._btn_a_state.start_notify()

Basically, you call subscribe_button_a with your routine to be called when Button A is detected to have changed state e.g. 

def buttonA_pressed (action):
    print('Button A: ', action)

This is effected by

  • Saving the user callback in the Microbit class
  • Installing _decode_btn_a as the callback from the dBus messaging service
  • When that is called, decoding the notification and Button action value from the dBus message
  • Invoking the saved user callback with a single parameter, the action value
So in principle, this should all work. It doesn't seem to. In fact, it's like the code actually installed the user callback as the dBus-level callback, because the parameter count and types don't match. I've ended up replicating the code in the _decode_btn_a function in order to make it work. Poo. Weird.

Update

I contacted the author on GitHub, and the short version is that I hadn't correctly installed the latest version into python3. I had downloaded the old version (event_service), as indicated by a blog I found, which actually did install the user callback into the dBus slot! So it worked exactly as coded.

Learning Points
Always 
  • Go to GitHub and get the code that corresponds to what you think you are supposed to be running
  • Do sudo python3 -m pip install <installable_thing> - that way it gets installed in the right environment