Giter VIP home page Giter VIP logo

adafruit_circuitpython_asyncio's Introduction

Introduction

Documentation Status

Discord

Build Status

Code Style: Black

Cooperative multitasking and asynchronous I/O

The code in this library is largely based on the MicroPython uasyncio implementation.

Dependencies

This driver depends on:

Please ensure all dependencies are available on the CircuitPython filesystem. This is easily achieved by downloading the Adafruit library and driver bundle or individual libraries can be installed using circup.

Installing from PyPI

This library is meant to be a subset of the asynciomodule in CPython, and will not be made available on PyPI. Use the CPython version instead.

Installing to a Connected CircuitPython Device with Circup

Make sure that you have circup installed in your Python environment. Install it with the following command if necessary:

pip3 install circup

With circup installed and your CircuitPython device connected use the following command to install:

circup install asyncio

Or the following command to update an existing version:

circup update

Documentation

API documentation for this library can be found on Read the Docs.

For information on building library documentation, please check out this guide.

Contributing

Contributions are welcome! Please read our Code of Conduct before contributing to help this project stay welcoming.

adafruit_circuitpython_asyncio's People

Contributors

dedukun avatar dhalbert avatar evaherrada avatar foamyguy avatar furbrain avatar imnotjames avatar jepler avatar kattni avatar kbsriram avatar ladyada avatar neradoc avatar retiredwizard avatar tannewt avatar tekktrik avatar

Stargazers

 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  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

adafruit_circuitpython_asyncio's Issues

RTD Documentation: Build status not successful

The RTD documentation was determined not to have built successfully. This could be the result of one of two reasons:

  1. The documentation truly had an error occur and failed to build
  2. The badge icon and/or link is not correctly formatted or pointing to the correct RTD link

Missing simple test

The Examples for asyncio on Read the Docs have a examples/asyncio_simpletest.py with some partial comments and no other content.

Indeed, the entire contents in that file in this repo since its creation are:

# SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries
# SPDX-FileCopyrightText: Copyright (c) 2021 Dan Halbert for Adafruit Industries
#
# SPDX-License-Identifier: Unlicense

Would love an example to follow that doesn't require a whole PyPortal! :}

Trinket M0: ImportError: no module named 'select'

On Trinket M0, you get the error:

>>> import asyncio
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "asyncio/__init__.py", line 14, in <module>
  File "asyncio/core.py", line 15, in <module>
ImportError: no module named 'select'

Apparently small CircuitPython builds do not have select?

add TaskGroup here or in another library

asyncio could really use a TaskGroup implementation, as provided by Trio (aka nurseries), Curio, aiotools, and EdgeDB.

TaskGroup was originally slated to be added Python as long ago as 3.8, but was held up because exception group handling was not available. However, exception groups are now being added: PEP 654, PR). Once that PEP is implemented, TaskGroup will probably follow. But the timeline for PEP 654 to go into the MicroPython core is unclear. So any TaskGroup we add will not be quite compatible, and we probably want to add it in an ancillary library (adafruit_asyncio or asynciox or xasyncio or ??).

Bugs in streams.py

This code should show a constantly increasing number, interspersed with groups of four characters read from
the usb_cdc.data serial terminal:

"""
example that reads from the cdc data serial port in groups of four and prints
to the console. The USB CDC data serial port will need enabling. This can be done
by copying examples/usb_cdc_boot.py to boot.py in the CIRCUITPY directory

Meanwhile a simple counter counts up every second and also prints
to the console.
"""


import asyncio
import usb_cdc

async def client():
    s = asyncio.StreamReader(usb_cdc.data)
    while True:
        text = await s.read(4)
        print(text)
        await asyncio.sleep(0)

async def counter():
    i = 0
    while True:
        print(i)
        i += 1
        await asyncio.sleep(1)

async def main():
    client_task = asyncio.create_task(client())
    count_task = asyncio.create_task(counter())
    await asyncio.gather(client_task, count_task)

asyncio.run(main())

What actually happens is 0 is printed, then nothing until four characters have been received on usb_cdc.data, and then this error message is produced:

Traceback (most recent call last):
  File "code.py", line 39, in <module>
  File "/lib/asyncio/core.py", line 292, in run
  File "/lib/asyncio/core.py", line 256, in run_until_complete
  File "/lib/asyncio/core.py", line 241, in run_until_complete
  File "code.py", line 36, in main
  File "/lib/asyncio/funcs.py", line 113, in gather
  File "/lib/asyncio/funcs.py", line 108, in gather
  File "/lib/asyncio/core.py", line 241, in run_until_complete
  File "code.py", line 22, in client
  File "/lib/asyncio/stream.py", line 63, in read
  File "/lib/asyncio/core.py", line 152, in queue_read
  File "/lib/asyncio/core.py", line 140, in _enqueue
AssertionError: 

This is due to this code in asyncio.stream.Stream:

    async def read(self, n):
        """Read up to *n* bytes and return them.

        This is a coroutine.
        """

        core._io_queue.queue_read(self.s)
        await core.sleep(0)
        return self.s.read(n)

It seems that the call to core._io_queue.queue_read(self.s) sets up the asyncio main loop to poll for
updates to self.s (usb_cdc.data) in this instance, and the schedules the current task to be re-awoken when it happens.
It then calls await core.sleep(0) which almost immediately returns.
self.s.read is then called which blocks everything until 4 characters are read.
Next time around, again core._io_queue.queue_read(self.s) is read, but this time there is already an entry waiting for usb_cdc.data to be updated and the code gets confused and raises an error.

This can be fixed by adding a yield statement instead of core.sleep(0) - I'll add a PR to that effect. However, I can see that the code previously did use that and I'm not sure why the change was made.

There is also an error in that if we add the yield, then the coroutine will pause until one character has been read (thus triggering the poll in core._io_queue), but it will then (again) block until the other 3 characters have been read. However, this time it will not crash. This probably needs to be fixed in python, probably using a scheme similar to Stream.read_exactly

core.py calls nonexistent function sys.print_exception

Various errors using asyncio result in a traceback leading to line core.py:283, like this from the included code.py, which contains a task that tries to call a string:

    code.py output:
    Task exception wasn't retrieved
    future: <Task object at 0x200275b0> coro= <coroutine object 'main' at 0x20004000>
    Traceback (most recent call last):
      File "code.py", line 11, in <module>
      File "asyncio/core.py", line 235, in run
      File "asyncio/core.py", line 230, in run_until_complete
      File "asyncio/core.py", line 287, in call_exception_handler
      File "asyncio/core.py", line 283, in default_exception_handler
    AttributeError: 'module' object has no attribute 'print_exception'

If I change the sys.print_exception call to:

traceback.print_exception(None, context["exception"], None)

then the console output is:

code.py output:
Task exception wasn't retrieved
future: <Task object at 0x200275a0> coro= <coroutine object 'main' at 0x20004000>
TypeError: 'str' object is not callable

The example code.py:

import os, asyncio

async def bad():
    os.sep()  # os.sep NOT CALLABLE
    print('after')

async def main():
    bad_task = asyncio.create_task(bad())
    await asyncio.gather(bad_task)
    
asyncio.run(main())

Running CP 7.1.0-beta.1 on a PyBadge, asyncio source code 0.50

Support Task methods `result()`, `exception()`, and `cancelled()`

CPython's tasks are subclasses of PyFuture. This means they have result, exception methods. They also have a cancelled method.

As a user of the asyncio library I want the circuitpython library to support these so I can inspect tasaks more easily from outside asyncio - eg, without having to muck around with CircuitPython asyncio task internals like state or data.

This would require some changes to the core loop for setting these values, but shouldn't be a huge difficulty otherwise? It should be negligible for memory usage / library footprint / etc, unless I'm misunderstanding how the library operates today.

Add docstring documentation

Code needs documentation as generated by docstrings like the other libraries. When this gets done, the link in the README should also be updated!

Exceptions can use up large amounts of memory

If an exception occurs in a task which is not the "main task", the exception is stored in a global variable: _exc_context in core.py. This is then passed to call_exception_handler. The exception contains the full traceback at the point of error - which includes references to multiple objects at the time of the error. These objects cannot then be garbage collected until the next exception occurs and _exc_context is overwritten. In complex programs this can cause large amounts of memory to be used

asyncio syntax error

While following the asyncio tutorial I am getting a syntax error. I am using the"QT Py Haxpress" uf2.

async def read_color(curent_color): <------ SyntaxError: invalid syntax
while True:
r, g, b, c = apds.color_data
print("Red: {0}, Green: {1}, Blue: {2}, Clear: {3}".format(r, g, b, c))
curent_color.value = (r, g, b)
await asyncio.sleep(0.2)

in the REPL:

'>>>' import asyncio
Traceback (most recent call last):
File "", line 1, in
File "asyncio/init.py", line 14, in
File "asyncio/core.py", line 15, in
ImportError: no module named 'select'
code_py.zip

asyncio.start_server relies on usocket, which is not available for circuitpython

asyncio.start_server does not start because there is no usocket.

If anyone has a workaround for running a TCP server without polling or blocking please lmk - in the meantime it's possible to poll a non-blocking TCP socket with socketpool, but obviously without the benefits of asyncio for performance.

Minimal reproduction code - be sure to update with your specific WiFi info below:

import asyncio
import wifi


wificreds = ["your_ssidname", "your_password"]

while wifi.radio.ipv4_address is None:
    found_networks = wifi.radio.start_scanning_networks()
    for network in found_networks:
        if network.ssid == wificreds[0]:
            wifi.radio.stop_scanning_networks()
            wifi.radio.connect(*wificreds)
    if wifi.radio.ipv4_address is not None:
        break
print("connected to wifi")


async def run_server():
    await asyncio.start_server(connection_callback, wifi.radio.ipv4_address, 8747)


def connection_callback(reader: asyncio.StreamReader, writer: asyncio.StreamWriter):
    print("incoming connection")


asyncio.run(run_server())

The output of running that code on the latest CircuitPython 8:

code.py output:
connected to wifi
Traceback (most recent call last):
File "code.py", line 26, in
File "asyncio/core.py", line 312, in run
File "asyncio/core.py", line 271, in run_until_complete
File "asyncio/core.py", line 256, in run_until_complete
File "code.py", line 19, in run_server
File "asyncio/stream.py", line 231, in start_server
ImportError: no module named 'usocket'

Code done running.

Lock is not preventing concurrent acquiring

asyncio.Lock() is not preventing concurrent acquiring of the lock.

Circuitpython version: Adafruit CircuitPython 7.3.3 on 2022-08-29; Adafruit Feather Bluefruit Sense with nRF52840

main.py:

import asyncio

async def lock_test(name, lock, sleep_time=0):
    print(name, 'start')
    async with lock:
        print(name, 'acquired lock')
        if sleep_time:
            await asyncio.sleep(sleep_time)
    print(name, 'released lock')
    return name

async def main(yield_mode):
    lock = asyncio.Lock()

    lock.yield_mode = yield_mode
    print('Lock test yield mode:', yield_mode)

    results = await asyncio.gather(
        asyncio.create_task(lock_test('A', lock, sleep_time=0.1)),
        asyncio.create_task(lock_test('B', lock, sleep_time=0.1)),
    )
    print('Task results:', results)


yield_mode = True
asyncio.run(main(yield_mode))

yield_mode = False
asyncio.run(main(yield_mode))

Output:

main.py output:
Lock test yield mode: True
A start
A acquired lock
B start
A released lock
B acquired lock
B released lock
Task results: ['A', 'B']
Lock test yield mode: False
A start
A acquired lock
B start
B acquired lock
A released lock
Traceback (most recent call last):
  File "asyncio/core.py", line 214, in run_until_complete
  File "main.py", line 8, in lock_test
  File "/lib/asyncio/lock.py", line 91, in __aexit__
  File "/lib/asyncio/lock.py", line 51, in release
RuntimeError: Lock not acquired

You are in safe mode because:
CircuitPython core code crashed hard. Whoops!
Crash into the HardFault_Handler.

When await core.sleep(0) is used in acquire(), task B is able to acquire the lock even though task A has not yet released the lock. This then leads to the exception when B tries to release the lock that was already released by A.

As a test I modified asyncio/lock.py to show that going back to using yield works while using await core.sleep(0) does not.

# SPDX-FileCopyrightText: 2019-2020 Damien P. George
#
# SPDX-License-Identifier: MIT
#
# MicroPython uasyncio module
# MIT license; Copyright (c) 2019-2020 Damien P. George
#
# This code comes from MicroPython, and has not been run through black or pylint there.
# Altering these files significantly would make merging difficult, so we will not use
# pylint or black.
# pylint: skip-file
# fmt: off
"""
Locks
=====
"""

from . import core

# Lock class for primitive mutex capability
class Lock:
    """Create a new lock which can be used to coordinate tasks. Locks start in
    the unlocked state.

    In addition to the methods below, locks can be used in an ``async with``
    statement.
    """

    def __init__(self):
        # The state can take the following values:
        # - 0: unlocked
        # - 1: locked
        # - <Task>: unlocked but this task has been scheduled to acquire the lock next
        self.state = 0
        # Queue of Tasks waiting to acquire this Lock
        self.waiting = core.TaskQueue()
        self.yield_mode = True

    def locked(self):
        """Returns ``True`` if the lock is locked, otherwise ``False``."""

        return self.state == 1

    def release(self):
        """Release the lock. If any tasks are waiting on the lock then the next
        one in the queue is scheduled to run and the lock remains locked. Otherwise,
        no tasks are waiting and the lock becomes unlocked.
        """

        if self.state != 1:
            raise RuntimeError("Lock not acquired")
        if self.waiting.peek():
            # Task(s) waiting on lock, schedule next Task
            self.state = self.waiting.pop_head()
            core._task_queue.push_head(self.state)
        else:
            # No Task waiting so unlock
            self.state = 0

    async def acquire(self):
        """Wait for the lock to be in the unlocked state and then lock it in an
        atomic way. Only one task can acquire the lock at any one time.

        This is a coroutine.
        """

        if self.state != 0:
            # Lock unavailable, put the calling Task on the waiting queue
            self.waiting.push_head(core.cur_task)
            # Set calling task's data to the lock's queue so it can be removed if needed
            core.cur_task.data = self.waiting
            try:
                if self.yield_mode:
                    yield
                else:
                    await core.sleep(0)
            except core.CancelledError as er:
                if self.state == core.cur_task:
                    # Cancelled while pending on resume, schedule next waiting Task
                    self.state = 1
                    self.release()
                raise er
        # Lock available, set it as locked
        self.state = 1
        return True

    async def __aenter__(self):
        return await self.acquire()

    async def __aexit__(self, exc_type, exc, tb):
        return self.release()

Remove or move non-CPython asyncio functionality

These things are in the MicroPython implementation of asyncio but not in CPython asyncio:

core.py:

  • SingletonGenerator: maybe rename to _SingletonGenerator
  • sleep_ms():omit, rename to _sleep_ms or move.
  • IOQueue
  • Loop -> _Loop

event.py:

  • ThreadSafeFlag

funcs.py:

  • wait_for_ms()

stream.py:

  • Stream: serves for StreamReader and StreamWriter, maybe _Stream
  • stream_awrite, Stream.aclose (?), Stream.awrite, Stream.awritestr: legacy uasyncio

task.py:

  • most things could be underscored

Make traceback optional

It seems that some boards omit traceback, but would we still want to support asyncio for those?

I think changing it to try/except ImportError for the import of traceback & then simplifying the output for printing the traceback could be fine?

Rename to adafruit_asyncio?

Thinking about this from a different angle. Is there a specific reason that the module is named asyncio instead of adafruit_asyncio? My guess is perhaps this was done to equalize the import and as much of the API as possible between CircuitPython and CPython.

But in other libraries that are similar modeled after and contain a subset of APIs of CPython modules they do have the adafruit_ in their name, and examples use as in the import to change the name to match CPython. The one that comes to mind for me is: https://github.com/adafruit/Adafruit_CircuitPython_Requests which has the module named adafruit_requests and imports in the examples are done like this:

import adafruit_requests as requests

Maybe it would make sense to add the adafruit_ prefix to asyncio instead of making an exception for it in the bundle / build tools command?

Originally posted by @FoamyGuy in adafruit/Adafruit_CircuitPython_Bundle#356 (comment)

Exception within a task can cause other tasks to run before the exceptioned-task completes

Placeholder for now, until I can come up with a minimum reproducible example.

Not sure if this belongs here, or in circuitpython.

Occurs in 7.3.3 and 8.0.0-beta.5.

Basic scenario:

async def foo(interval1):
     print("A")
     # some_statement_that_produces_a_caught_exception()
     print("B")
await asyncio.sleep(interval1)

async def bar(interval2):
     print("C")
     print("D")
     await asyncio.sleep(interval2)

Output:

A
C
D
# traceback.print_exception(None, exc, exc.__traceback__) from library
B (??)

It seems to occur when the exception is caught by the library-internal exception handler, rather than when the exception is caught by user code.

edited to reflect additional testing

So maybe this is intended behavior? It was confusing that the exceptioned-task was interrupted, but the exception wasn't printed until after other tasks had run. I thought I had a case where print("B") would show up later after the library traceback, but now I'm not sure.

Addendum: When the library catches routine development bugs like this (syntax errors, type errors), but everything continues to operate, it can mask problems with the user code.

"no module named 'usocket'" on Raspberry Pico W

Hi,

I'm trying to use this package in order to run an HTTP Server (Microdot) concurrently with other tasks on Raspberry Pico W.
It's failing for me with:

Traceback (most recent call last):
  File "<stdin>", line 42, in <module>
  File "microdot_asyncio.py", line 320, in run
  File "asyncio/core.py", line 297, in run
  File "asyncio/core.py", line 261, in run_until_complete
  File "asyncio/core.py", line 246, in run_until_complete
  File "microdot_asyncio.py", line 278, in start_server
  File "asyncio/stream.py", line 226, in start_server
ImportError: no module named 'usocket'

Am I doing something totally unsupported yet, like maybe only some subset of CircuitPython devices have usocket?

I know that sockets generally work if I use socketpool for screating sockets, so maybe I'm really looking after #4 ?

I'd appreciate any pointers!

CancelledError not showing up as a backtrace

Canceling a task or raising CancelledError does not produce a backtrace. This is true when using the Python Task implementation. When tested on MicroPython, which uses the native _uasyncio.Task implementation, this works properly.

(I also tried getting the native _uasyncio up and running in CircuitPython, but it's also having issues, because we require that it provide an Task.__await__(), which it does not provide.)

Could `TaskQueue` be implemented without `Task` being in C?

I have a hypothesis that the big reason why Task and TaskQueue are implemented in C are because the task queue's pairing heap implementation in C is far more performant than implementing it in native python.

Because the entire TaskQueue is in C, with the way it's written it needs to reference the tasks themselves - so then the Task class has to be written in C as well. This leads to troubles when - for example - we want to reference other errors, make changes to the Task class, etc. (Okay, so maybe it's just me that has been feeling this pain!)

Something I was poking at is rewriting the TaskQueue to use the heapq implementation. The big question I have and why I opened this issue is to ask "Is this a path forward?"

Another alternative would be to abstract the Task piece so it no longer stores its own ph_key / etc and include the task within the pairing heap wrapped. There's two places where Task's ph_key is checked outside of the task queue - once by the task itself during cancellation (this could be done by somehow getting when a task is scheduled from the currently running loop?) - and once by the running loop which could just be asking the task queue when the next Task is going to be at during the peek.

Better tutorials in using Asyncio?

I'm developing some puzzles for Escape Room and I'm porting some of my solutions to Circuitpython. But I'm having some difficulty with the idea of multi-tasking. I'm using Asyncio and even managed to put a countdown running at the same time as one of my games. But I couldn't figure out yet how I could make these two tasks communicate. I noticed that Asyncio has an Events feature, and many others, but there is no specific tutorial and no examples. I'm working on the basis of trial and error. If I have any miraculous understanding, I will write something and notice to you. But in the meantime, I signal this lack here.

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.