Micro:bit as a game controller

The Goal

My aim was originally to set up a Micro:bit as a Bluetooth HID1, but it turns out that’s beyond my ken.

My initial attempts were using C++ or PXT as per these projects:

PXT Bluetooth Keyboard
BLE HID Keyboard Stream Demo

The first indicates that it will sync with MacOS and work with Android. I could only get a brief sync with MacOS sometimes and while it synced with Android, I couldn’t get any keystrokes to show up.

The second briefly showed up as a possible pairing device, but beyond that I got no joy. It doesn’t help that I couldn’t understand just about any of the code.

Revising Expectations

Okay, this is beyond my abilities at the moment. But there’s a cheaty way – based on the concepts used in this project.

Sam El-Husseini’s project uses three easy to implement components:

  1. Pushing data to the serial bus (USB) from the Micro:bit when a button is pressed
  2. A listener program on the host device that turns the data from serial into actions on the host
  3. A second/more Micro:bits that send Bluetooth messages to the first device – in this way, it would act as a proxy (basically a proprietary dongle).

Component 3 is compelling, because implementing Bluetooth communication between Micro:bits is almost comically easy in the interpreted languages (such as Python, Javascript etc). I imagine its probably significantly easier to implement in C++ than proper BT pairing too, but I’ll cross that bridge when I’m good and ready.

Part 1 – Proof-of-Concept Tethered Device

I decided to implement the first two components and leave the third for another time 2.

The tethered MB uses very similar code to Sam’s project above:

Javascript is spawned from the third circle of hell, but I’ll be damned if it isn’t easy to use in this case
basic.forever(function () {
    if (input.buttonIsPressed(Button.A)) {
        serial.writeNumber(1)
    }
    if (input.buttonIsPressed(Button.B)) {
        serial.writeNumber(2)
    }
    if (!(input.buttonIsPressed(Button.A)) && !(input.buttonIsPressed(Button.B))) {
        serial.writeNumber(0)
    }
})

The only modification here is to send a stream of numbers rather than linefeed delimited strings3.

The third if statement is to work with the key events that the host program needs to implement – if we’re just typing, sending the keystrokes in isolation is fine, but we need to indicate when a key is pressed and when it should be released. The 1s and 2s indicate when A and B are pressed respectively. The 0s indicate that nothing is being pressed. Is it bad and naughty that I’m sending a constant stream of zeros when nothing is happening? I’ll fix it in the first beta/third production patch.

I’ve never used Node before, so I had a go at using Node as per Sam’s project. It works great for his purposes because there’s a specific Node module for integration with Spotify. I needed something more general-purpose. The only two modules I could find were a keyboard simulator that is a wrapper for a JAR file and requires full-blown Java VM and robot JS which is also pretty big for my needs, but hey, let’s give the robot a go.

The issue with robot is that it implements a function called “keyTap”, which, well, taps a key. If you’ve ever watched someone use a controller, they ain’t sittin’ there tapping the buttons, they’re pressing and holding most of the time.

No dice. Back to Python.

Part 2 – Host Program

import serial
from pynput.keyboard import Key, Controller
ser = serial.Serial('/dev/tty.usbmodem14202', 115200)

keyboard = Controller()

last = 0

while 1:
  serial_line = int(ser.read())
  if serial_line != 0:
    last = 1
    if serial_line == 1:
      keyboard.press('a')
    elif serial_line == 2:
      keyboard.press('d')
  elif last != 0:
    keyboard.release('a')
    keyboard.release('d')
    last = 0

Python provides two modules for our purposes: serial and pynput.

Pynput gives us much finer control over keyboard simulation – pressing and holding and releasing keys, among other functions.

There’s really not a lot to the host program – it’s surprisingly simple. The one point to note is the use of the “last” variable.

During testing, I noticed that the host program was blocking normal keyboard input – this is because the constant stream of 0s was causing it to continuously trigger key releases for the related keys, rendering the keyboard useless for those keys. The use of a test for whether or not the release command has been sent removes that unintended side effect. I investigated putting similar code into the Micro:bit program, but the nature of the byte stream meant that the 0s didn’t always register with the host program.

Any necessary improvements are indicated in the todo list below.

Proof of concept using a silly pygame Spider-man thing I made

Wait – what’s the point?

“But Jonathan,” you say, “What’s the point of a controller with only two buttons? Even Tetris requires at least four!”

Ah, you’re forgetting about… Tron:

14 years and still going strong…

Armagetron Advanced, to be exact. Played with just two buttons (unless you’re a coward who uses the brakes).

In all seriousness, the proof of concept is for a more ambitious project: Micro:bits can be easily connected to a breakout board which allows for a wide array of inputs, buttons etc to be connected to its IO pins.

In theory (and what I’d like to do with my video game design class) one could design the composition and layout of their own “ideal” controller and create a Micro:bit program to pipe commands to serial.

Laser cutting or 3D printing the necessary structure of the controller should be straightforward enough (we’re not shooting for aesthetic design awards). The end result is a custom controller powered by any Micro:bit.

Now that this proof of concept is complete, there’s a little more work to be done.

Todo List

  1. Test with compiled, rather than translated code on both the host and Micro:bit. It could have been my imagination, but it felt that the controller was a touch delayed, which would make sense given the pipeline from physical button to simulated keypress.
  2. Test with two Micro:bits – using the tethered MB as a dongle for the “wireless” one.
  3. Connect external physical buttons and joysticks to the MB IO pins. This process is well documented and I do not anticipate it to be particularly difficult4
  4. Investigate removing or mitigating the zero stream when the device is idle.
  5. Investigate using Bluetooth to connect as a direct serial device without pairing – no idea if possible or easy, but would allow a similar serial streaming process with the need for a dongle MB.
  6. Modify the host program to autodetect the appropriate port.
  7. Verify cross-platform compatibility (read: Windows).
  8. Modify host program to allow for a button to be held down while tapping another.
  9. Design and implement a physical casing for a customised controller.

Definitely a job to finish in 2019, but happy to have found this process so easy to do (in comparison to native Bluetooth pairing).