Simple uasyncio question

General discussions and questions abound development of code with MicroPython that is not hardware specific.
Target audience: MicroPython Users.
Post Reply
User avatar
superthaix
Posts: 5
Joined: Sun Jan 05, 2020 7:37 pm
Location: USA

Simple uasyncio question

Post by superthaix » Thu Oct 29, 2020 1:09 am

Hi.

I'm attempting to learn asyncio. But I'm stumped on the behaviour uasyncio.sleep(0) in this case.

The code below is my first exercise to get a grasp of asyncio programming. The code simply flashes the two ESP8266 nodemcu onboard LEDs continiously at different rates. I have posted two version of the code. The first example works, but the second doesn't. Please see the differences in the main() coro. I thought that await uasyncio.sleep(0) would allow the coro loops to run continuously on the second example. If not, please let me know the best and/or correct way to implement this.
Thanks

This works :D

Code: Select all

#ESP8266 asyncio dual led continuous flasher

from machine import Pin
import uasyncio

led1 = Pin(2, Pin.OUT, value=1) # built in nodemcu LEDs
led2 = Pin(16, Pin.OUT, value=1)

async def blink():
    while True:
        led1.off()
        await uasyncio.sleep(1)
        led1.on()
        await uasyncio.sleep(1)
        
async def blink_2():
    while True:
        led2.off()
        await uasyncio.sleep(.25)
        led2.on()
        await uasyncio.sleep(.25)
        
async def main():
    uasyncio.create_task(blink())
    await uasyncio.create_task(blink_2()) # this works
    
uasyncio.run(main())
This doesn't :?:

Code: Select all

#ESP8266 asyncio dual led continuous flasher

from machine import Pin
import uasyncio

led1 = Pin(2, Pin.OUT, value=1) # built in nodemcu LEDs
led2 = Pin(16, Pin.OUT, value=1)

async def blink():
    while True:
        led1.off()
        await uasyncio.sleep(1)
        led1.on()
        await uasyncio.sleep(1)
        
async def blink_2():
    while True:
        led2.off()
        await uasyncio.sleep(.25)
        led2.on()
        await uasyncio.sleep(.25)
        
async def main():
    uasyncio.create_task(blink())
    uasyncio.create_task(blink_2())
    await uasyncio.sleep(0) # this doesn't work
    
uasyncio.run(main())

User avatar
jimmo
Posts: 2754
Joined: Tue Aug 08, 2017 1:57 am
Location: Sydney, Australia
Contact:

Re: Simple uasyncio question

Post by jimmo » Thu Oct 29, 2020 4:27 am

The key issue here is that asyncio.run() (and loop.run_until_complete) will run until the specified task finishes. main() completes almost immediately in your second example. The fact that it spawned some new tasks doesn't matter.
superthaix wrote:
Thu Oct 29, 2020 1:09 am
The first example works, but the second doesn't.
This works because main() will be blocked awaiting the second (blink_2) completing.
superthaix wrote:
Thu Oct 29, 2020 1:09 am
I thought that await uasyncio.sleep(0) would allow the coro loops to run continuously on the second example.
This doesn't work because the sleep completes (almost) immediately, main resumes, and then completes immediately.
superthaix wrote:
Thu Oct 29, 2020 1:09 am
If not, please let me know the best and/or correct way to implement this.
There are a few options, but really the choice of which option to use depends on what your program is eventually going to do.

As an illustrative example of something that "just works", if you change main() to instead sleep in a loop, then that will do what you want:

Code: Select all

async def main():
    asyncio.create_task(blink())
    asyncio.create_task(blink_2())
    while True:
        await asyncio.sleep_ms(100)
You could make that sleep into any amount of time you like... longer would be better to avoid main having to do as much work. But even zero work work here. Note that I'm using sleep_ms instead of sleep which avoids the floating point number (which avoids heap allocation).

Conceptually though, your first solution is actually kind of correct -- you want to make main() block until blink and blink_2 completes (and as it happens, blink_2 runs forever, so that does exactly what you need). Possibly the more generalised way to express this would be:

Code: Select all

async def main():
    task1 = asyncio.create_task(blink())
    task2 = asyncio.create_task(blink_2())
    await asyncio.gather(task1, task2)
and this works nicely if the "while True" loops in blink and blink_2 are replaced with for loops that eventually terminate -- main() will now block until both tasks have completed.

The even more generalised answer to this problem is that you can use asyncio.Event to notify across tasks.

User avatar
pythoncoder
Posts: 5956
Joined: Fri Jul 18, 2014 8:01 am
Location: UK
Contact:

Re: Simple uasyncio question

Post by pythoncoder » Thu Oct 29, 2020 2:10 pm

@superthaix see also this explanation.
Peter Hinch
Index to my micropython libraries.

User avatar
superthaix
Posts: 5
Joined: Sun Jan 05, 2020 7:37 pm
Location: USA

Re: Simple uasyncio question

Post by superthaix » Fri Oct 30, 2020 2:38 pm

Thank you both jimmo and pythoncoder.

All information provided has been extremely helpful.

I've actually been reading the V3 TUTORIAL.md, as you can see it take me a little longer to comprehend the text, :lol:

Now that leads me to 2 more questions:

#1. Below is the another version of my exercise, but I get the feeling that using the run_forever() method is frowned upon?

Code: Select all

from machine import Pin
import uasyncio

event_loop = uasyncio.get_event_loop()

led1 = Pin(2, Pin.OUT, value=1) # inverted operation
led2 = Pin(16, Pin.OUT, value=1)

async def blink():
    while True:
        led1.on()
        await uasyncio.sleep(1)
        led1.off()
        await uasyncio.sleep(1)
        
async def blink_2():
    while True:
        led2.on()
        await uasyncio.sleep(.25)
        led2.off()
        await uasyncio.sleep(.25)
        
event_loop.create_task(blink())
event_loop.create_task(blink_2())

event_loop.run_forever()
#2. Pythoncoder, in the tutorial you mention, "In this instance main() terminates after 10s. This is atypical of embedded uasyncio systems. Normally the application is started at power up by a one line main.py and runs forever."

What is your preferred way(s) to run forever (maybe I missed it in the tutorial)?

Thanks in advance.

User avatar
pythoncoder
Posts: 5956
Joined: Fri Jul 18, 2014 8:01 am
Location: UK
Contact:

Re: Simple uasyncio question

Post by pythoncoder » Sat Oct 31, 2020 8:57 am

A typical firmware application starts at power up and runs forever. You can usually identify at least one task which never terminates: let's call that task main(). An application broadly looks like this:

Code: Select all

import uasyncio as asyncio
async def read_device(device):
    while not device.ready():
        await asyncio.sleep_ms(100)
    return device.data()

async def main():
    device = Device()
    while True:
        data = await read_device(device)
        # do something with data
        await asyncio.sleep(10)

try:
    asyncio.run(main())  # Only terminates on a bug or ctrl-c
finally:
    asyncio.new_event_loop()  # Clear retained state: useful when testing
Note the use of asyncio.run() rather than the event loop methods which are the V2 way of doing things.

The main applications which terminate are test scripts. There are rare special cases like applications which issue a hard reset or use the standby state.
Peter Hinch
Index to my micropython libraries.

User avatar
superthaix
Posts: 5
Joined: Sun Jan 05, 2020 7:37 pm
Location: USA

Re: Simple uasyncio question

Post by superthaix » Sun Nov 01, 2020 1:04 pm

Thanks for the explanation and example. I'm looking forward to tinkering with asyncio on upcoming projects.
This has been a tremendous help.

Post Reply