Quick Start

Belay is a library that makes it quick and easy to interact with hardware via a MicroPython-compatible microcontroller. Belay has a single imporant class, Device:

import belay

device = belay.Device("/dev/ttyUSB0")

Creating a Device object connects to the board at the provided port. On connection, the device is reset into REPL mode, and a few common imports are performed on-device, namely:

import os, time, machine
from time import sleep
from micropython import const
from machine import ADC, I2C, Pin, PWM, SPI, Timer

The device object has 4 important methods for projects: __call__, task, thread, and sync. These are described in the subsequent subsections.

call

Directly calling the device instance, like a function, invokes a python statement or expression on-device.

Invoking a python statement like:

ret = device("foo = 1 + 2")

would execute the code foo = 1 + 2 on-device. Because this is a statement, the return value, ret is None.

Invoking a python expression like:

res = device("foo")

results in res == 3.

Direct invocations like this are common to import modules and declare global variables. Alternative methods are described in the task section.

task

The task decorator sends the decorated function to the device, and replaces the host function with a remote-executor.

Consider the following:

@device.task
def foo(a):
    return a * 2

Invoking bar = foo(5) on host sends a command to the device to execute the function foo with argument 5. The result, 10, is sent back to the host and results in bar == 10. This is the preferable way to interact with hardware.

If a task is registered to multiple Belay devices, it will execute sequentially on the devices in the order that they were decorated (bottom upwards). The return value would be a list of results in order.

To explicitly call a task on just one device, it can be invoked device.task.foo().

thread

thread is similar to task, but executes the decorated function in the background on-device.

@device.thread
def led_loop(period):
    led = Pin(25, Pin.OUT)
    while True:
        led.toggle()
        sleep(period)


led_loop(1.0)  # Returns immediately

Not all MicroPython boards support threading, and those that do typically have a maximum of 1 thread. The decorated function has no return value.

If a thread is registered to multiple Belay devices, it will execute sequentially on the devices in the order that they were decorated (bottom upwards).

To explicitly call a thread on just one device, it can be invoked device.thread.led_loop().

sync

For more complicated hardware interactions, additional python modules/files need to be available on the device's filesystem. sync takes in a path to a local folder. The contents of the folder will be synced to the device's root directory.

For example, if the local filesystem looks like:

project
├── main.py
└── board
    ├── foo.py
    └── bar
        └── baz.py

Then, after device.sync("board") is ran from main.py, the remote filesystem will look like

foo.py
bar
└── baz.py

Subclassing Device

Device can be subclassed and have task/thread methods. Benefits of this approach is better organization, and being able to define tasks/threads before the actual object is instantiated.

Consider the following:

from belay import Device

device = Device("/dev/ttyUSB0")


@device.task
def foo(a):
    return a * 2

is roughly equivalent to:

from belay import Device


class MyDevice(Device):
    @Device.task
    def foo(a):
        return a * 2


device = MyDevice("/dev/ttyUSB0")

Marking methods as tasks/threads in a class requires using the capital @Device.task decorator. Methods marked with @Device.task are similar to @staticmethod in that they do not contain self in the method signature. To the device, each marked method is equivalent to an independent function.