ESP32 In MicroPython: Asyncio
Written by Harry Fairhead & Mike James   
Monday, 09 September 2024
Article Index
ESP32 In MicroPython: Asyncio
Awaiting Sleep
Sequential and Concurrent
Shared Variables and Locks
Using uasyncio

Using uasyncio

When you first meet uasyncio, or its full Python equivalent asyncio, it is all too easy to see it as a total solution. The idea that you can structure a program as a collection of tasks which get to run when they are needed seems to be a simplification. However, the MicroPython implementation of uasyncio provides only two coroutines that free the thread – uasyncio.sleep() and uasyncio.sleep_ms(). There are also has some network classes and methods which make uasyncio much more useful, but it is still worth looking at its more basic use.

 

The only sort of task you can write that actually gives up the thread, and hence take advantage of asynchronous implementation, are of the form:

async task1():
	while True:
		do something
		await uasyncio.sleep(t)
		do something

The call to sleep releases the thread and allows other tasks to run for at least t seconds. What this means is that all of the tasks you create have to be able to be suspended for a given amount of time to allow other tasks to run. Notice that there is no indication of how often any of the tasks will run. For example, if one of the tasks is designed to read a sensor every few seconds then there is no way that you can use an asynchronous approach to guarantee that this is the case unless you handcraft all of the other tasks to ensure that the sensor task gets its turn at the right time. This is just as difficult, if not more so, than writing a simple polling loop that calls the tasks in a fixed order.

Consider the following program modeled on the example in the documentation:

import uasyncio
from machine import Pin
async def blink(led, period_ms):
    while True:
        led.on()
        await uasyncio.sleep_ms(5)
        led.off()
        await uasyncio.sleep_ms(period_ms)
async def main(led1, led2):
    uasyncio.create_task(blink(led1, 700))
    uasyncio.create_task(blink(led2, 400))
    await uasyncio.sleep_ms(10_000)
uasyncio.run(main(Pin(22,Pin.OUT), Pin(23,Pin.OUT)))

This flashes two LEDs connected to GPIO22 and GPIO23. The blink coroutine turns the LED on and then sleeps for 5ms, giving other tasks a chance to run. It then switches the LED off and sleeps for a specified period. If you try this out you will find that you do get 5ms pulses spaced at 700ms and 400ms. However, none of the periods are guaranteed. All it takes is another task, or a set of tasks, that take longer to process than 5ms to disrupt the intended timing. For example, we can introduce a task that simply wastes some time:

import uasyncio
from machine import Pin
from time import sleep_ms
async def blink(led, period_ms):
    while True:
        led.on()
        await uasyncio.sleep_ms(5)
        led.off()
        await uasyncio.sleep_ms(period_ms)
async def timewaste():
    while True:
        sleep_ms(10)
        await uasyncio.sleep_ms(0)
async def main(led1, led2):
    uasyncio.create_task(blink(led1, 700))
    uasyncio.create_task(blink(led2, 400))
    uasyncio.create_task(timewaste())
    await uasyncio.sleep_ms(10_000)
uasyncio.run(main(Pin(22,Pin.OUT), Pin(23,Pin.OUT)))

Now if you run the program you will discover that the pulses are now 10ms in size. The timewaste coroutine now hogs the only thread of execution, only giving it up every 10ms, which means that the blink coroutine only gets the thread back after at least 10ms whenever it gives it up.

Even if you find this difficult to understand, an additional negative point for the approach is that the timing of blink depends on the timing of the other tasks it finds itself running with.

Asynchronous approaches generally only work well when each task keeps the thread for a time that is much shorter than the time that each task needs to run – and this implies that the thread has to be idle for most of the time.

You can convert any polling interaction into an uasyncio task by using the sleep_ms method. All you have to do is write a small infinite pooling loop:

async def checkReady():
	while True:
		if read hardware state:
			process hardware
		await uasyncio.sleep_ms(10)

This can be run along with similar tasks and as long as none of them take longer to process the hardware than the sleep time they should all work together.

In chapter but not in this extract

  • Async Networking
  • Downloading A Web Page
  • Server
  • A Web Server
  • Best Practice

 

Summary

  • The uasyncio module provides single-threaded multi-tasking.

  • A coroutine is a function that can be suspended and resumed by the use of the await instruction.

  • A coroutine has to be run in conjunction with an event loop. uasyncio.run creates an event loop and runs a coroutine as a task using it.

  • A Task is a coroutine with some additional methods and it is what is added to the event loop’s queue using uasyncio.create_task. The Task is run when the thread becomes free.

  • When you await a coroutine it starts running to completion.

  • When you await a Task, i.e. a coroutine already on the task loop, it only starts running if it isn’t already completed.

  • The await always returns the result of the Task, including any exceptions that might have occurred.

  • You can use wait_for as a version of await with a timeout.

  • Task coroutines can be executed in sequential order by awaiting each one in turn. They can be run concurrently by adding them to the queue or by using the gather coroutine.

  • A task can be canceled but is up to you to handle the exception.

  • A Task returns any exceptions to the awaiting coroutine – these can be raised or processed.

  • Locks are less useful for coroutines because unless the thread is released they are atomic. If a race condition can occur there are asynchronous equivalents of some of the standard synchronization objects.

  • The uasyncio module makes network connections easy and asynchronous.

  • Implementing a web client is easy, but there is no high-level function which downloads an HTML page. You have to work with the HTTP protocol.

  • Creating a web server is only slightly more difficult in that you have to support multiple potential clients.

Programming the ESP32in MicroPython

By Harry Fairhead & Mike James

esppython360

Buy from Amazon.

Contents

       Preface

  1. The ESP32 – Before We Begin
  2. Getting Started
  3. Getting Started With The GPIO 
  4. Simple Output
  5. Some Electronics
  6. Simple Input
  7. Advanced Input – Interrupts
  8. Pulse Width Modulation
       Extract:
    PWM And The Duty Cycle
  9. Controlling Motors And Servos
  10. Getting Started With The SPI Bus
  11. Using Analog Sensors
       Extract:
    Analog Input
  12. Using The I2C Bus
       Extract
    : I2C, HTU21D And Slow Reading 
  13. One-Wire Protocols
  14. The Serial Port
  15. Using WiFi
     Extract:
    WiFi 
  16. Sockets
     Extract:
    Client Sockets
     Extract:
    SSL Client Sockets
  17. Asyncio And Servers
     Extract:
    Asyncio ***NEW!
  18. Direct To The Hardware
     Extract:
    Using Hardware Registers 

<ASIN:187196282X>

espbook

 

Comments




or email your comment to: comments@i-programmer.info

To be informed about new articles on I Programmer, sign up for our weekly newsletter, subscribe to the RSS feed and follow us on Twitter, Facebook or Linkedin.



Last Updated ( Tuesday, 10 September 2024 )