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.