peterhinch / micropython-micro-gui

A lightweight MicroPython GUI library for display drivers based on framebuf, allows input via pushbuttons. See also micropython-touch.
MIT License
270 stars 40 forks source link

[Question] Setting up asyncio background tasks #42

Closed jdkuki closed 10 months ago

jdkuki commented 10 months ago

Hello,

I am looking to use this library to integrate into a small "client" device I'm building to display data being polled from another device over bluetooth. I would like my client to be able to setup the connection to bluetooth in the background, periodically poll data to store in memory or respond to any incoming bluetooth data appropriately. My client device will have multiple "screens" to navigate between to control various aspects of the device.

I am having trouble figuring out how to setup a long lived background task that will persist throughout screen navigations. It seems I can register a task via reg_task, however all tasks here will be cancelled between navigations? It seems I can't setup anything prior to calling Screen.change as this will call asyncio.run which should create the event loop, which would throw an exception if I tried to set one up prior. Calling create_task prior to this should also throw an exception (although it seems like reg_task is calling create_task before asyncio.run(Screen.monitor()) creates the event loop (should also be throwing an exception)?

Any guidance on how I should go about things would be greatly appreciated

peterhinch commented 10 months ago

Microgui is designed to be usable in an application that uses only synchronous code - indeed by users with no knowledge of asyncio. Its use of asyncio is therefore hidden and as you have realised it is started when the first screen is instantiated: in the first call to Screen.change(BaseScreen). asyncio runs until the base screen terminates, returning to the REPL.

The use of reg_task is entirely optional: it allows a task to be associated with a Screen instance, being cancelled when the screen is closed. If tasks are to run for the lifetime of the application (or for some lesser duration) they may be launched at any point by issuing asyncio.create_task. The earliest a task may be started is in the constructor of the base screen.

jdkuki commented 10 months ago

Thanks Peter, I am somewhat new to micropython and just setup a test between normal asyncio and uasyncio on the esp32.

In normal python, calling create_task without an event loop throws an excpetion. It looks like in uasyncio, create_task can be called at any time and is kept in some sort of global task list. Ex:

try:
        import uasyncio as asyncio
except ImportError:
        import asyncio

async def test():
    while True:
        print("hello world")
        await asyncio.sleep(1)

async def main():
    i = 0
    while i < 10:
        i = i + 1
        await asyncio.sleep(1)

asyncio.create_task(test())

asyncio.run(main())

Will throw an exception in a normal interpreter but not on micropython. I think the Screen code is taking advantage of this but was hard to follow (micropython seems to imply uasyncio is a rough 1:1 port?) (at least for me coming from golang into python coroutines and micropython at the same time)

peterhinch commented 10 months ago

Your observation about create_task is correct. MicroPython is not strictly correct in allowing it to be called prior to creating an event loop. You are also right in that Screen.change() instantiates the base screen before starting asyncio. Thanks for pointing this out - this had escaped me.

I'll consider what to do about this. In practical terms I can't see any impact on application design or behaviour.

peterhinch commented 10 months ago

I have pushed an update which fixes this. Thanks for the report.

jdkuki commented 10 months ago

Thanks!