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 5 important methods for projects: __call__, setup, 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.

setup

The setup decorator is a way of invoking code on-device in a global context, and is commonly used for imports and instantiating objects and hardware. For example:

@device.setup
def setup(pin_number):
    from machine import Pin

    led = Pin(pin_number)


setup(25)

is equivalent to:

device("pin_number = 25")
device("from machine import Pin")
device("led = Pin(pin_number)")

Functions decorated with setup should be called only a few times at most. For repeated functions calls, use the task decorator.

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.

Alternatively, the foo function will also be available at 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.

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. Methods can be marked with @Device.setup or @Device.thread for their respective functionality.