Coder Social home page Coder Social logo

brianpugh / belay Goto Github PK

View Code? Open in Web Editor NEW
222.0 8.0 10.0 881 KB

Belay is a python library that enables the rapid development of projects that interact with hardware via a micropython-compatible board.

License: Apache License 2.0

Python 99.42% Makefile 0.23% Dockerfile 0.35%
arduino esp32 hardware micropython raspberry-pi-pico robotics serial firmata package-manager iot

belay's Introduction

image

Python compat PyPi GHA Status Coverage Documentation Status

Belay is:

  • A python library that enables the rapid development of projects that interact with hardware via a MicroPython or CircuitPython compatible board.
  • A command-line tool for developing standalone MicroPython projects.
  • A MicroPython package manager.

Belay supports wired serial connections (USB) and wireless connections via WebREPL over WiFi.

Quick Video of Belay in 22 seconds.

See the documentation for usage and other details.

Who is Belay For?

Belay is for people creating a software project that needs to interact with hardware. Examples include:

  • Control a motor so a webcam is always pointing at a person.
  • Turn on an LED when you receive a notification.
  • Read a potentiometer to control system volume.

The Belay Package Manager is for people that want to use public libraries, and get them on-device in an easy, repeatable, dependable manner.

What Problems Does Belay Solve?

Typically, having a python script interact with hardware involves 3 major challenges:

  1. On-device firmware (usually C or MicroPython) for directly handling hardware interactions. Typically this is developed, compiled, and uploaded as a (nearly) independent project.
  2. A program on your computer that performs the tasks specified and interacts with the device.
  3. Computer-to-device communication protocol. How are commands and results transferred? How does the device execute those commands?

This is lot of work if you just want your computer to do something simple like turn on an LED. Belay simplifies all of this by merging steps 1 and 2 into the same codebase, and manages step 3 for you. Code is automatically synced at the beginning of script execution.

The Belay Package Manager makes it easy to cache, update, and deploy third party libraries with your project.

Installation

Belay requires Python >=3.8 and can be installed via:

pip install belay

The MicroPython-compatible board only needs MicroPython installed; no additional preparation is required. If using CircuitPython, and additional modification needs to be made to boot.py. See documentation for details.

Examples

Turning on an LED with Belay takes only 6 lines of code. Functions decorated with the task decorator are sent to the device and interpreted by the MicroPython interpreter. Calling the decorated function on-host sends a command to the device to execute the actual function.

import belay

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


@device.task
def set_led(state):
    print(f"Printing from device; turning LED to {state}.")
    Pin(25, Pin.OUT).value(state)


set_led(True)

Outputs from print calls from on-device user-code are forwarded to host stdout.

For more examples, see the examples folder.

belay's People

Contributors

brianpugh avatar dependabot[bot] avatar freemansoft avatar timaidley avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

belay's Issues

Can't connect ESP32

I'm testing out an ESP32 (yesterday I worked on a RPi Pico), and I am getting errors connecting it with Belay. It seemingly fails in Pyboard, however if I connect thorugh Pyboard directly, it works.

pyb = pyboard.Pyboard('/dev/cu.SLAB_USBtoUART', 115200)
# pyb = belay.Device('/dev/cu.SLAB_USBtoUART', 115200)

pyb.close()
# Works without errors
# pyb = pyboard.Pyboard('/dev/cu.SLAB_USBtoUART', 115200)
pyb = belay.Device('/dev/cu.SLAB_USBtoUART', 115200)

pyb.close()
# Produces an error >>>

---------------------------------------------------------------------------
PyboardError                              Traceback (most recent call last)
Cell In [24], line 2
      1 # pyb = pyboard.Pyboard('/dev/cu.SLAB_USBtoUART', 115200)
----> 2 pyb = belay.Device('/dev/cu.SLAB_USBtoUART', 115200)
      4 pyb.close()
      6 # poll = select.poll()
      7 # poll.register(pyb.serial, select.POLLIN)

File /usr/local/anaconda3/envs/buxr/lib/python3.10/site-packages/belay/device.py:464, in Device.__init__(self, startup, attempts, *args, **kwargs)
    461 self.attempts = attempts
    462 self._cmd_history = []
--> 464 self._connect_to_board(**self._board_kwargs)
    466 self.task = _TaskExecuter(self)
    467 self.thread = _ThreadExecuter(self)

File /usr/local/anaconda3/envs/buxr/lib/python3.10/site-packages/belay/device.py:523, in Device._connect_to_board(self, **kwargs)
    521 else:
    522     soft_reset = True
--> 523 self._board.enter_raw_repl(soft_reset=soft_reset)

File /usr/local/anaconda3/envs/buxr/lib/python3.10/site-packages/belay/pyboard.py:382, in Pyboard.enter_raw_repl(self, soft_reset)
    380 if not data.endswith(b"raw REPL; CTRL-B to exit\r\n>"):
    381     print(data)
--> 382     raise PyboardError("could not enter raw repl")
...
    386 # Waiting for "soft reboot" independently to "raw REPL" (done below)
    387 # allows boot.py to print, which will show up after "soft reboot"
    388 # and before "raw REPL".

PyboardError: could not enter raw repl

Before the error it seems to print:

b'MicroPython v1.19.1 on 2022-06-18; ESP32 module with ESP32\r\nType "help()" for more information.\r\n>>> '

API for interrupts / callbacks

have you thought about ways to implement something to allow 2-way async communication?
i.e. setting up a callback for a hardware interrupt generated from a "board gpio"/button
could also be useful scenarios when you read a sensor that emits its value when its ADC conversion is done, so instead of having a poll loop on the host, it would be nice if a value or fifo can be dispatched on the next (upstream) usb packet transfer => event to belay

global scope vars can't be accessed if reassigned in task

I can print a global scope var in a function. But I can't print it or access it if the variable is reassigned inside the function. It looks like it is changing from the global scoped variable to a local scoped one.

The only variable that is important for this issue is current_tics. Including everything else for completeness

    @Device.setup(
        autoinit=True
    )  # ``autoinit=True`` means method will be called during object creation.
    def setup():
        import board
        import touchio
        import usb_hid
        from adafruit_hid.keyboard import Keyboard
        from adafruit_hid.keyboard_layout_us import KeyboardLayoutUS
        from adafruit_hid.keycode import Keycode
        import neopixel

        keyboard = Keyboard(usb_hid.devices)
        keyboard_layout = KeyboardLayoutUS(keyboard)

        touch1 = touchio.TouchIn(board.TOUCH1)
        touch2 = touchio.TouchIn(board.TOUCH2)

        pixels = neopixel.NeoPixel(board.NEOPIXEL, 4)

        default_color = (1, 1, 1)  # color when button not pressed
        current_color = default_color  # button color or default_color
        cycle_length_tics = 600  # loop cycle time --> blink cycle length
        cycle_blank_length_tics = cycle_length_tics // 3  # blanking time
        current_tics = 0

print(current_tics) fails here

    @Device.task
    def broken_vars():
        print("====try_touch()============")
        print_vars()
        print(keyboard)
        print(current_tics)
        current_tics = 0

print(current_tics) succeeds here

    @Device.task
    def broken_vars():
        print("====try_touch()============")
        print_vars()
        print(keyboard)
        print(current_tics)

driver program

from trinkeyfunctions import MyDevice

parser = argparse.ArgumentParser()
parser.add_argument("--port", "-p", default="/dev/ttyUSB0")
args = parser.parse_args()

print("starting setup")
device = MyDevice(args.port, startup="")  # perform no convenience imports

device.print_vars()
device.broken_vars()

failure message is

NameError: local variable referenced before assignment

It looks like the global scoped variable is replaced with a local one if I reassign it. This means I can't have any loop counters or state between tasks.

OSError: could not get source code

Trying to run the LED example on an RP Pico. It blows up with OSError Could not get source code in a task print statement.
Got to be something obvious


Pico RP2040
MicroPython 1.9.1
Python 3.9.13

PS C:\Users\joe\Documents\GitHub\belay>
>>> parser = argparse.ArgumentParser()
>>> parser.add_argument("--port", "-p", default="COM21")
_StoreAction(option_strings=['--port', '-p'], dest='port', nargs=None, const=None, default='COM21', type=None, choices=None, required=False, help=None, metavar=None)>>> args = parser.parse_args()>>>>>> # Setup the connection with the micropython board.
>>> # This also executes a few common imports on-device.
>>> device = belay.Device(args.port)
>>>>>>... def setup():  # The function name doesn't matter, but is "setup" by convention....     from machine import Pin
...     print('hello world')...Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
  File "C:\Users\joe\Documents\GitHub\belay\belay\executers.py", line 57, in __call__
    src_code, src_lineno, src_file = getsource(f, strip_signature=True)
  File "C:\Users\joe\Documents\GitHub\belay\belay\inspect.py", line 91, in getsource
    lines, src_lineno = inspect.getsourcelines(f)
  File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.9_3.9.3568.0_x64__qbz5n2kfra8p0\lib\inspect.py", line 1006, in getsourcelines      
    lines, lnum = findsource(object)
  File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.9_3.9.3568.0_x64__qbz5n2kfra8p0\lib\inspect.py", line 835, in findsource
    raise OSError('could not get source code')
OSError: could not get source code
>>>

Code

This code blows up on print(Hellow World)

import argparse
import time

import belay

parser = argparse.ArgumentParser()
parser.add_argument("--port", "-p", default="COM21")
args = parser.parse_args()

# Setup the connection with the micropython board.
# This also executes a few common imports on-device.
device = belay.Device(args.port)


@device.setup
def setup():  # The function name doesn't matter, but is "setup" by convention.
    from machine import Pin
    print('hello world')

Timed generator helper function

Hey Brian. I'm finding myself using the generators as my go-to currently, and started working on a single function to ease with logging data. So something like (roughly copy-pasted out of context):

try:
        for val in device.my_generator(iti):
            val_pd = pd.DataFrame([val])
            data = pd.concat([data, val_pd], ignore_index=True)
            data.loc[data.index[-1],'time'] = time.time() - t_init
            print(val_pd)
            t0 = time.time()
            t1 = time.time()
            iti_adjusted = iti - ((t0 - t_init) % iti)
            while t1 - t0 < iti_adjusted:
                t1 = time.time()
    except:
        pass

Could instead be something like:

values = timed_generator(task, repeats, interval_type, duration, **args, **kwargs)

So I was wondering if that would be something it could make sense to include Belay or if I should make it somewhere separate? I am currently using pandas which is a major dependency to add onto the project. On the other hand it might be nice to have a few functions to aid various routine tasks, maybe in a separate sub-library. What do you think? I can create a pull request later this week if you'd like to see a draft.

Bug: `setup` with arguments fails

On belay 0.15, I could do:

from belay import Device, list_devices

class Pico(Device):
    @Device.setup
    def setup1(argument=False):
        from machine import Pin
        led = Pin(25, Pin.OUT)

    @Device.task
    def led_toggle():
        led.toggle()



if __name__ == "__main__":
    port = list_devices()[-1]
    with Pico(port) as pico:
        pico.setup1(argument=True)
        pico.led_toggle()

However, I just updated to 0.19, and it no longer works - for good reason of course, as I can't provide an argument to stuff that autoinits. However, even adding autoinit=False gives the error:

from belay import Device, list_devices

class Pico(Device):
    @Device.setup(
        autoinit=False
    )
    def setup1(argument=False):
        from machine import Pin
        led = Pin(25, Pin.OUT)

    @Device.task
    def led_toggle():
        led.toggle()

if __name__ == "__main__":
    port = list_devices()[-1]
    with Pico(port) as pico:
        pico.setup1(argument=True)
        pico.led_toggle()

# ValueError: Method <function Pico.setup1 at 0x10699a320> decorated with "@Device.setup(autoinit=True)" must have no arguments.

EDIT: I use this when I have multiple separate setup routines, and parameters depending e.g. on the number of identical sensors I want to initialize.

Unable to synch with CircuitPython 8.0 on Trinkey

Unable to run examples/02_blink_neopixel against an Adafruit Trinkey Neo board running CircuitPython 8.0 release. Haven't tried on 7.x

Belay: 0.16.2
CircuitPython version: 8.0 https://circuitpython.org/board/neopixel_trinkey_m0/
boot.py has been configured per https://belay.readthedocs.io/en/latest/CircuitPython.html

PS C:\Users\joe\Documents\GitHub\belay\examples\02_blink_neopixel> python3 .\circuitpython.py -p COM12
Traceback (most recent call last):
  File "C:\Users\joe\Documents\GitHub\belay\examples\02_blink_neopixel\circuitpython.py", line 12, in <module>
    device = belay.Device(args.port)
    self._connect_to_board(**self._board_kwargs)
  File "C:\Users\joe\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.9_qbz5n2kfra8p0\LocalCache\local-packages\Python39\site-packages\belay\device.py", line 337, in _connect_to_board    self._board.enter_raw_repl(soft_reset=soft_reset)
  File "C:\Users\joe\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.9_qbz5n2kfra8p0\LocalCache\local-packages\Python39\site-packages\belay\pyboard.py", line 421, in enter_raw_repl    self.read_until(1, b"soft reboot\r\n")
  File "C:\Users\joe\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.9_qbz5n2kfra8p0\LocalCache\local-packages\Python39\site-packages\belay\pyboard.py", line 398, in read_until  
    raise PyboardError(
belay.pyboard.PyboardError: Timed out reading until b'soft reboot\r\n'
    Received: b'OK\r\nraw REPL; CTRL-B to exit\r\n>'

ImportError no module named analogio

i'm cool if belay is really targeted at the Pico RP 2040. But just in case

Firmware: Circuitpython 7.3
Board: Trinkey Neo
Error: no module named 'analogio'

This module does not exist for this distribution of circuitpython on this board.

Code

import argparse
import time

import belay

parser = argparse.ArgumentParser()
parser.add_argument("--port", "-p", default="/dev/ttyUSB0")
args = parser.parse_args()

# Setup the connection with the micropython board.
# This also executes a few common imports on-device.
device = belay.Device(args.port)

Outout

Traceback (most recent call last):
  File "C:\Users\joe\Documents\GitHub\Adafruit-Trinkey-CircuitPython\Neo\capacitive-touch-belay\standalone.py", line 12, in <module>
    device = belay.Device(args.port)
  File "C:\Users\joe\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.9_qbz5n2kfra8p0\LocalCache\local-packages\Python39\site-packages\belay\device.py", line 275, in __init__
    self._exec_snippet("convenience_imports_circuitpython")
  File "C:\Users\joe\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.9_qbz5n2kfra8p0\LocalCache\local-packages\Python39\site-packages\belay\device.py", line 348, in _exec_snippet
    return self("\n".join(snippets))
  File "C:\Users\joe\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.9_qbz5n2kfra8p0\LocalCache\local-packages\Python39\site-packages\belay\device.py", line 410, in __call__
    self._board.exec(cmd, data_consumer=data_consumer)
  File "C:\Users\joe\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.9_qbz5n2kfra8p0\LocalCache\local-packages\Python39\site-packages\belay\pyboard.py", line 524, in exec
    raise PyboardException(ret_err.decode())
belay.pyboard.PyboardException:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ImportError: no module named 'analogio'

The import fails in the REPL

>>> import board
>>> import analogio
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ImportError: no module named 'analogio'

The module is not in the modules list

>>> help("modules")
__future__        adafruit_hid/mouse                  microcontroller   supervisor
__main__          adafruit_pixelbuf micropython       sys
adafruit_hid/__init__               array             neopixel          time
adafruit_hid/consumer_control       board             neopixel_write    touchio
adafruit_hid/consumer_control_code  builtins          os                usb_cdc
adafruit_hid/keyboard               collections       rainbowio         usb_hid
adafruit_hid/keyboard_layout_base   digitalio         random            usb_midi
adafruit_hid/keyboard_layout_us     gc                storage
adafruit_hid/keycode                math              struct
Plus any modules on the filesystem
>>>

I think it is because because convenience_imports_circuitpython.py contains modules that are not on all boards.

import os, time, analogio, board, digitalio
from time import sleep
from busio import I2C, SPI

I feel like a bad penny

LED Example does not run

I am using desktop with an AMD Athlon(tm) II X4 635 Processor the OS is Linux Mint 20.2.
Thonny is installed and my Raspberry PI Pico (with MicroPytho) is connected. In Thonny I can turn the onboard LED on and off, and can get readings from the onboard temperature sensor but cannot get the belay example to run.
The following is my version for the LED example (pico_led.py) annotated to show my configuration and the output when it is run from the terminal:

import belay

# list_ports_linux.py
# /dev/ttyACM0 - Board in FS mode - Board CDC: usb

# udevadm info -q all -a -n /dev/ttyACM0
#  looking at parent device '/devices/pci0000:00/0000:00:13.1/usb6/6-2/6-2:1.0'
#  ATTRS{interface}=="Board CDC"
#  looking at parent device '/devices/pci0000:00/0000:00:13.1/usb6/6-2':
#  ATTRS{manufacturer}=="MicroPython"
#  ATTRS{product}=="Board in FS mode"

# lsusb
# Bus 006 Device 002: ID 2e8a:0005 MicroPython Board in FS mode
device = belay.Device("/dev/ttyACM0")
from machine import Pin


@device.task
def set_led(state):
    print(f"Printing from device; turning LED to {state}.")
    Pin(25, Pin.OUT).value(state)


set_led(True)

the following is copied from the terminal:

python3 pico_led.py
Traceback (most recent call last):
  File "pico_led.py", line 14, in <module>
    device = belay.Device("/dev/ttyACM0")
  File "/home/user1/.local/lib/python3.8/site-packages/belay/device.py", line 120, in __init__
    self._exec_snippet("startup")
  File "/home/user1/.local/lib/python3.8/site-packages/belay/device.py", line 239, in _exec_snippet
    snippets = [read_snippet(name) for name in names]
  File "/home/user1/.local/lib/python3.8/site-packages/belay/device.py", line 239, in <listcomp>
    snippets = [read_snippet(name) for name in names]
  File "/home/user1/.local/lib/python3.8/site-packages/belay/helpers.py", line 23, in read_snippet
    return importlib.resources.files(snippets).joinpath(resource).read_text()
AttributeError: module 'importlib.resources' has no attribute 'files'

Are there fixes for the scripts in belay
Thanks
Michael

Advised way of importing libraries

I was wondering how you can import libraries other than the ones imported by default by Belay. I've made a @device.task def setup(): function which works well, but is that the intended way? As a side note I'm awaiting Micropython v1.20 which includes mip (so you should be able to install using mip install <package> if I've understood correctly). Maybe it would be a nice addition to have a "installing and importing libraries" section in the documentation?

`Yield` returns `None`

I think there may have been a breaking change somewhere. I'm trying a simple generator function, but I only get None yields. Tried on both my Mac and a student's Windows with Belay 0.15.

from belay import Device
import time

class PicoW(Device):
    @Device.task
    def my_generator():
        i = 0
        while i < 10:
            yield i
            i += 1

if __name__ == "__main__":
    picoW = PicoW(list_devices()[-1])
    values = []
    for val in picoW.my_generator():
        values.append(val)
    picoW.close()
    print(values)
# [None, None, None, None, None, None, None, None, None, None]

If I print from within the generator I get the correct values, so I believe they're lost during transit somewhere.
I also tried in the old-fashioned way without a class, getting the same results.

No way to clean up the connection status / cleanly end a connection / cleanly restart a connection

Iยดd like to build on belay lib, but it lacks some mechanisms:

  • how to call the status of a connection (connected, lost..)
  • how to cleanly re-establish a connection

While i can use the exceptions (pyboard or serial exceptions) that a thrown to detect a problem, I cannot cleanly re-establish a connection with both resetting the board and restarting my script.

The idea is to automatically restart my belay task when after a devices is resetted or un-plugged > replugged (failproof connection)
(so far i tested only belay with serial connections)

Can one return bytes or bytearray from task decorated function?

Just a question here. Can I return a bytearray or bytes from a @device.task decorated function? I tried but I'm getting some errors:

bytearray(b'Hello, world!')
<class 'bytearray'>
Traceback (most recent call last):
  File "/home/pdietl/unorg/mpy-belay/./demo.py", line 93, in <module>
    chip_read(0, len(msg))
  File "/home/pdietl/unorg/mpy-belay/.venv/lib/python3.10/site-packages/belay/executers.py", line 141, in func_executer
    return self._belay_device._traceback_execute(src_file, src_lineno, name, cmd, record=record)
  File "/home/pdietl/unorg/mpy-belay/.venv/lib/python3.10/site-packages/belay/device.py", line 790, in _traceback_execute
    res = self(cmd, record=record)
  File "/home/pdietl/unorg/mpy-belay/.venv/lib/python3.10/site-packages/belay/device.py", line 286, in __call__
    self._board.exec(cmd, data_consumer=data_consumer)
  File "/home/pdietl/unorg/mpy-belay/.venv/lib/python3.10/site-packages/belay/pyboard.py", line 592, in exec
    ret, ret_err = self.exec_raw(command, data_consumer=data_consumer)
  File "/home/pdietl/unorg/mpy-belay/.venv/lib/python3.10/site-packages/belay/pyboard.py", line 584, in exec_raw
    return self.follow(timeout, data_consumer)
  File "/home/pdietl/unorg/mpy-belay/.venv/lib/python3.10/site-packages/belay/pyboard.py", line 506, in follow
    data = self.read_until(b"\x04", timeout=timeout, data_consumer=data_consumer)
  File "/home/pdietl/unorg/mpy-belay/.venv/lib/python3.10/site-packages/belay/pyboard.py", line 440, in read_until
    data_consumer(data_for_consumer)
  File "/home/pdietl/unorg/mpy-belay/.venv/lib/python3.10/site-packages/belay/device.py", line 280, in data_consumer
    out = parse_belay_response(line)
  File "/home/pdietl/unorg/mpy-belay/.venv/lib/python3.10/site-packages/belay/device.py", line 61, in parse_belay_response
    return ast.literal_eval(line)
  File "/usr/lib/python3.10/ast.py", line 110, in literal_eval
    return _convert(node_or_string)
  File "/usr/lib/python3.10/ast.py", line 109, in _convert
    return _convert_signed_num(node)
  File "/usr/lib/python3.10/ast.py", line 83, in _convert_signed_num
    return _convert_num(node)
  File "/usr/lib/python3.10/ast.py", line 74, in _convert_num
    _raise_malformed_node(node)
  File "/usr/lib/python3.10/ast.py", line 71, in _raise_malformed_node
    raise ValueError(msg + f': {node!r}')
ValueError: malformed node or string on line 1: <ast.Call object at 0x7ffb4422ca60>

The first print is a print of the bytearray I want to return.

How to control memory allocation

So I am calling a function that writes its argument of bytes to a SPI FRAM chip. I call it with a bytes argument of say, 10k bytes. When the function call is over, even when I call gc.collect(), the memory in microPython keeps going down. Any suggestions?

My function looks like this:

@device.task
def chip_write_remote(bytes_to_write):
    print(gc.mem_free())
    gc.collect()
    # Write enable
    cs(0)
    spi.write(b'\x06')
    cs(1)
    # Perform the write
    cs(0)
    spi.write(bytes_to_write)
    cs(1)
    del bytes_to_write
    print(gc.mem_free())
    gc.collect()
    print(gc.mem_free())

On my Linux machine I try to take an arbitrary number of bytes and chunk it over to micropython:

def batched(iterable, n):
    "Batch data into tuples of length n. The last batch may be shorter."
    # batched('ABCDEFG', 3) --> ABC DEF G
    if n < 1:
        raise ValueError('n must be at least one')
    it = iter(iterable)
    while batch := bytes(islice(it, n)):
        yield batch

def chip_write(address, data_bytes):
    assert address >> 18 == 0, "Address must be 18 bits or less!"

    for bites in batched(data_bytes, 50 * 1024):
        write_buffer = bytearray()
        write_buffer.append(0x02)
        write_buffer.extend(address.to_bytes(3, byteorder='big'))
        write_buffer.extend(data_bytes)
        chip_write_remote(write_buffer)
        device("import gc; gc.collect()")

Using belay in a class

I'm now trying to create a class that takes a port as input, creates the Belay.device in __init__ and has belay-decorated methods. However, I can't do that currently as the decorator depends on device. I also tried initiating the device outside the class and using it as an argument, but that doesn't help either. Any ideas on how to go about this?

import belay

class BelayDevice():
    def __init__(self, port, **kwargs):
        device = belay.Device(port, 115200) #'/dev/cu.usbmodem141201'

    @device.task
    def led_loop(self, period):
        from neopixel import NeoPixel
        np = NeoPixel(Pin(17), 8)
        x = 0
        state = False
        light = (0,0,0)
        while x < 6:
            # np.value(state)
            if state is False:
                light = (255, 255, 255)
            else:
                light = (0,0,0)
            np.fill(light)
            state = not state
            sleep(period)
            x += 1

bel = BelayDevice('/dev/tty.SLAB_USBtoUART')
bel.led_loop(1)

# Traceback (most recent call last):
#  File "/Users/roaldarbol/Filen/git-projects/belay/mikkel-test.py", line 3, in <module>
#   class BelayDevice():
#  File "/Users/roaldarbol/Filen/git-projects/belay/mikkel-test.py", line 7, in BelayDevice
#    @device.task
# NameError: name 'device' is not defined

For background, I'd like to be able to connect to multiple controllers and run code separately. I'll use multiprocessing for that, but I've already got that side of things down - I just need a way that I can easily instantiate a class multiple times.

WebREPL support?

Is it possible to use this without a physically connected board (e.g. via the WebREPL)?

Send event/request from board/device

Hi,
I got find belay in search of way to communicate RPi Pico <=> PC via serial (both ways).
I saw examples (i.e. 07_lcd), and it looks like communication is initiate by PC only.
Beside printing something at LCD or return value from some function executed at Pico, I need as well way to get notified about pushed button at Pico (or event form LCD touch screen).
Shortly when message/request was initiate by Pico itself.

Can belay support this, or maybe you know already existed solution.

AttributeError: type object 'Executer' has no attribute 'setup'

Just a bug report. I'm trying to use the new @Device.setup decorator (I am on v0.14.1), but I get the following error:

from belay import Device
import time

class PicoW(Device):
    @Device.setup
    def setup():
        led = Pin('LED', Pin.OUT)

    @Device.task
    def led_toggle():
        led.toggle()

if __name__ == "__main__":
    picoW = PicoW('/dev/cu.usbmodem14240')
    picoW.setup()
    picoW.led_toggle()
    time.sleep(2)
    picoW.led_toggle()
    picoW.close()

# Traceback (most recent call last):
#   File "/Users/roaldarbol/MEGA/Documents/sussex/research/experiments/experiments/PicoW.py", line 14, in <module>
#     picoW = PicoW('/dev/cu.usbmodem14240')
#   File "/usr/local/lib/python3.10/site-packages/belay/device.py", line 251, in __init__
#     executer = getattr(Executer, metadata.executer.__registry__.name)
# AttributeError: type object 'Executer' has no attribute 'setup'

Package manager dependency indexes

I've not used the package manager yet, but just based on the documentation it seems that you have to specify URLs to repos. Poetry indexes PyPi, so maybe it could be an idea to do something similar to automatically index both PyPi and micropython-lib? E.g. having

[tools.belay.dependencies.pip]
toml = "^0.10.2"

[tools.belay.dependencies.mip]
logging = "^0.5.2"

or a more Poetry compatible version:

[tools.belay.group.pip.dependencies]
toml = "^0.10.2"

[tools.belay.group.mip.dependencies]
logging = "^0.5.2"

And since Micropython packages can also be in places like pycopy maybe thinking about how to implement specify indexes more generally (like secondary indexes in Poetry) would be worthwhile. ๐Ÿ˜Š

Injecting methods

Again, just running an idea by you to hear your thoughts. Would it be possible to inject calls to the close method and the class instantiation somehow?

Is there a way one can check if any functions with a particular decorator is present? Then, upon instantiating the class, we could check to see whether there are any @Device.setup methods and automatically run them, and similarly, with a @Device.close, inject them into the default close method?

For close, the way I work right now, which is still super easy is just to make a new task of some name, say housekeeping which turns off LEDs, stops motors, etc. I just thought that it would be more intuitive if you could inject them into close, so you know that all the housekeeping will always be performed when closing the device.

E.g. in this minimal example, the led would be automatically set up, and turned off:

class Pico(Device):
    @Device.setup
    def setup():
        led = Pin(25, Pin.OUT)
      
    @Device.task
    def led_on():
        led.on()

    @Device.close
    def close():
        led.off()

if __name__ == "__main__":
    pico = Pico(list_devices()[-1])
    pico.led_on()
    time.sleep(3)
    pico.close()

Find available devices

I often use list_ports from serial.tools to find available devices. However, you always need a few more steps. As it's such a common need I was thinking it would be useful to add into Belay as a helper function. Could be:

from serial.tools import list_ports

def list_devices():
    ports = []
    for port in list(list_ports.comports()):
        ports.append(port.device)
    return(ports)

I'll do a PR, so you can comment on that too. ๐Ÿ˜…
EDIT: This is my first proper pull request on a collaborative project, so hope I've done it right.

Package manager documentation

The package manager is already dawning on me. I really really like it! However, I'm a bit confused by:

Belay assumes your project contains a python-package with the same name as tool.belay.name located in the root of your project.

Does that mean you cannot have just a pyproject.toml file, the python script and nothing else in a repo? (except for the autogenerated .belay-lib).

sync uses wrong path `\` characters when copying files to /pyboard while Python 3 is running on windows machines.

Was testing examples/06_external_modules_and_files with REPL across a Bluetooth Serial connection. The sync operator appears to be doing using the backslash \ character in the path when copying from the windows filesystem to the device file system.

Supposition: \ is the separator for the file system for Python on the PC picked up from the file/dir scan.

Test machine: Windows PC
Target device: Pico RP2040
Windows shell running "python 3" command: Powershell.

Symptoms:

  1. Test program copies files that end up prefixed with the backslash character, the windows root separator.
  2. Program fails to find led.py

Board directory after test fails.

C:\ESP8266-MicroPython> ls /pyboard/
\somemodule/       \led.py            \somemodule\led.py  \hello_world.txt

Expected results

C:\ESP8266-MicroPython> ls /pyboard/
somemodule/       led.py            somemodule/led.py   hello_world.txt

Board directory after manually restoring boot.py into the same device using rshell builtin cp cp boot-origi.py /pyboard command

C:\ESP8266-MicroPython> ls /pyboard/
\somemodule/       \led.py            \somemodule\led.py boot-orig.py       \hello_world.txt

Using Pimoroni libraries

...this is a tentative issue title, until we figure out what the underlying problem should be. Let's see. ;-)

I'd like to be able to use Pimoroni's sensors, and they have lots of libraries for them (found here). However, I can't find the appropriate .py files, only .cpp or .hpp. Any ideas about how to use those? If there's an easy way of using those, then the issue is just this.

They have made their own micropython firmware which includes all their libraries. I tried to just upload their firmware on my Pico, but when loading the libraries I can't (NameError: name 'pimoroni_i2c' isn't defined). So I guess we don't have access to the on-board libraries - or am I just doing something wrong?

So, perhaps the issue could also be about how to work with alternative firmware. I thought there might be a case for having a section in pyproject.toml where one could specify a URL to a custom firmware, and if it isn't already loaded, then load that firmware onto the board? I don't know if it'll be confusing, but it would help with "working out of the box".

However, I think it can become problematic with multiple boards, and we would have to put some thought into it.

Once mip flourishes, it should possibly become redundant, but for now I think there's a need.

CircuitPython remount in boot.py makes drive readonly to PC.

I have this boot.py per the instructions and now the device is mounted read only to my pc. I think it changes the drive from writable via circuitpython and read only to the computer. You may wish to add notes on how to remove that boot.py from the repl so that people can code with files on the drive again.

import storage

storage.remount("/")

Continuous logging and injecting parameters

Hi Brian. Thanks for the amazing project! I'm trying to put it to good use for some scientific experiments (neuroscience in case you'd find it interesting ๐Ÿ˜Š). First of all sorry - this is a dual issue, but I think they make sense together. Do feel free to split them into separate issues if you wish.

I'd like to be able to do two specific things:

  • Continuously (or semi-continuously) log the output from the board without interrupting it. With device.task() I can only log the function output at the end of the function. And with list_of_values = device.thread() I haven't been able to return values (I get NoneType). I could run a loop locally, but it doesn't offer the same time precision I'm after (I can provide an example if needed). I've written about it previously on the MicroPython Github which is where I was recommended to have a look at Belay.
  • I need to be able to send updated variables as it is running. I've been able to do that previously with poll on the board and serial.write() on the local side. See more on the MicroPython forum.

Do you think I can achieve this with Belay, or what would be needed? I'd be happy to beta test features if you'd like me to. I can get away with some sloppier code for now, but I'd love to make the most of the tidiness offered by Belay! ๐Ÿ˜ƒ

Yield result

First I just wanted to thank you for this library, it's been super helpful!

I've just run in to a problem, I would like to yield a result instead of returning a result.

This is works fine on my pi pico:

from time import sleep

def count():
    i = 0
    while True:
        i += 1
        yield i

for index in count():
    sleep(1)
    print(index)

However when I try and use belay with:

import belay
from time import sleep

device = belay.Device("COM5")

@device.task
def count():
    i = 0
    while True:
        i += 1
        yield i


for index in count():
    print(index)

I receive the error:

  Type "() -> Generator[Literal[1], None, None]" cannot be assigned to type "((...) -> PythonLiteral) | None"
    Type "() -> Generator[Literal[1], None, None]" cannot be assigned to type "(...) -> PythonLiteral"
      Function return type "Generator[Literal[1], None, None]" is incompatible with type "PythonLiteral"
        Type "Generator[Literal[1], None, None]" cannot be assigned to type "PythonLiteral"
          Type cannot be assigned to type "None"
          "Generator[Literal[1], None, None]" is incompatible with "bool"
          "Generator[Literal[1], None, None]" is incompatible with "bytes"
          "Generator[Literal[1], None, None]" is incompatible with "int"

Thanks!

v0.20.0 deps and belay select issue

repro:

  • did a clean venv + sourced it
  • pip install belay
  • belay [enter]
venv/lib/python3.11/site-packages/belay/cli/new.py", line 6, in <module>
    from packaging.utils import canonicalize_name
ModuleNotFoundError: No module named 'packaging'

then:

  • pip install packaging
  • belay [enter] all good
  • belay select
  • note: this only happens when no board is found/connected
  File "venv/lib/python3.11/site-packages/belay/cli/select.py", line 119, in select
    device_index = select_table(
                   ^^^^^^^^^^^^^
  File "venv/lib/python3.11/site-packages/belay/cli/questionary_ext.py", line 104, in select_table
    raise ValueError("A list of choices needs to be provided.")
ValueError: A list of choices needs to be provided.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.