A Python driver module for Brilliant Labs Monocle

I bundled up the things I learned about driving a Brilliant Labs Monocle into a new Python package. While doing the fine-tuning, I learned a bit more about driving the MicroPython UART interface.

Worth noting: I only discovered it after I completed this project, but another good example of using Python to drive Monocle actually comes from the Monocle’s creators.

Fine-tuning the connection

The UART service has a maximum size of data it can consume in one “chunk;” overflowing this limit can result in dropping bytes. As of right now, the count is 128 bytes (- 3 bytes for overhead). For some reason I haven’t sussed out yet, the BLE characteristic reports the size is only 20 bytes, so hard-coding it to 128 sped it up. The Bleak module will do chunking for you if you set the mtu_size, but I implemented my own (mostly because I didn’t know Bleak would handle it for me, but now that it’s implemented I like the logging so I’m keeping it.

De-automating the REPL’s terminal

By default, the MicroPython REPL in the Monocle firmware has some user-friendliness features that are extremely convenient for hand-coding MicroPython, but complicate passing MicroPython through as literal strings. To that end, the module wraps the commands it’s sending in

  • Control-C, to stop any running programs on the Monocle
  • Control-A, to set the REPL to “raw” mode where it accepts input but does not interpret it.
  • After the program is sent, Control-D to accept and interpret the sent code.

With these changes in place, the library far more reliably sends programs. A short demo below shows how to send a program that updates the battery status once a second for five seconds.

import asyncio
from brilliant_monocle_driver import Monocle

def callback(channel, text_in):
  """
  Callback to handle incoming text from the Monocle.
  """
  print(text_in)

# Simple MicroPython command that prints battery level for five seconds
# and then blanks the screen
COMMAND = """
import display
import device
import time

def show_battery(count):
  batLvl = str(device.battery_level())
  display.fill(0x000066)
  display.text("bat: {} {}".format(batLvl, count), 5, 5, 0xffffff)
  display.show()

count = 0
while (count < 5):
  show_battery(count)
  time.sleep(1)
  count += 1

display.fill(0x000000)
display.show()

print("Done")

"""

async def execute():
    mono = Monocle(callback)
    async with mono:
        await mono.send(COMMAND)

asyncio.run(execute())

What’s next?

Now that the module is working, I can focus on doing things on the Monocle itself. Next, I’m going to try and plumb data from the Monocle back to react to the control inputs.