Book Image

MicroPython Projects

By : Jacob Beningo
Book Image

MicroPython Projects

By: Jacob Beningo

Overview of this book

With the increasing complexity of embedded systems seen over the past few years, developers are looking for ways to manage them easily by solving problems without spending a lot of time on finding supported peripherals. MicroPython is an efficient and lean implementation of the Python 3 programming language, which is optimized to run on microcontrollers. MicroPython Projects will guide you in building and managing your embedded systems with ease. This book is a comprehensive project-based guide that will help you build a wide range of projects and give you the confidence to design complex projects spanning new areas of technology such as electronic applications, automation devices, and IoT applications. While building seven engaging projects, you'll learn how to enable devices to communicate with each other, access and control devices over a TCP/IP socket, and store and retrieve data. The complexity will increase progressively as you work on different projects, covering areas such as driver design, sensor interfacing, and MicroPython kernel customization. By the end of this MicroPython book, you'll be able to develop industry-standard embedded systems and keep up with the evolution of the Internet of Things.
Table of Contents (14 chapters)
11
Downloading and Running MicroPython Code

Cooperative multitasking using asyncio

So far, we have examined how we can schedule tasks in a MicroPython-based system using round-robin, timers, and threads. While threads may be the most powerful scheduling option available, they aren't deterministic schedulers and don't fit the bill for most MicroPython applications. There is another scheduling algorithm that developers can leverage to schedule tasks within their systems: cooperative scheduling.

A cooperative scheduler, also known as cooperative multitasking, is basically a round-robin scheduling loop that includes several mechanisms to allow a task to yield the CPU to other tasks that may need to use it. The developer can fine-tune the way that their application behaves, and their tasks execute without adding the complexity that is required for a pre-emptive scheduler, like those included in an RTOS. Developers who decide that a cooperative scheduler fits their application best will need to make sure that each task they create can complete before any other task needs to execute, hence the name cooperative. The tasks cooperate to ensure that all the tasks are able to execute their code within their requirements but are not held to their timing by any mechanism.

Developers can develop their own cooperative schedulers, but MicroPython currently provides the asyncio library, which can be used to create cooperatively scheduled tasks and to handle asynchronous events in an efficient manner. In the rest of this chapter, we will examine asyncio and how we can use it for task scheduling within our embedded applications.

Introducing asyncio

The asyncio module was added to Python starting in version 3.4 and has been steadily evolving ever since. The purpose of asyncio is to handle asynchronous events that occur in Python applications, such as access to input/output devices, a network, or even a database. Rather than allowing a function to block the application, asyncio added the functionality for us to use coroutines that can yield the CPU while they wait for responses from asynchronous devices.

MicroPython has supported asyncio in the kernel since version 1.11 through the uasyncio library. Prior versions still supported asyncio, but the libraries had to be added manually. This could be done through several means, such as the following:

  • Copying the usyncio library to your application folder
  • Using micropip.py to download the usyncio library
  • Using upip if there is a network connection

If you are unsure whether your MicroPython port supports asyncio, all you need to do is type the following into the REPL:

import usyncio

If you receive an import error, then you know that you need to install the library before continuing. Peter Hinch has put together an excellent guide regarding asyncio with instructions for installing the library that you can find at https://github.com/peterhinch/micropython-async/blob/master/TUTORIAL.md#0-introduction.

It's important to note that the support for asyncio in MicroPython is for the features that were introduced in Python 3.4. Very few features from the Python 3.5 or above asyncio library have been ported to MicroPython, so if you happen to do more in-depth research into asyncio, please keep this in mind to avoid hours of debugging.

The main purpose of asyncio is to provide developers with a technique for handling asynchronous operations in an efficient manner that doesn't block the CPU. This is done through the use of coroutines, which are sometimes referred to as coros. A coroutine is a specialized version of a Python generator function that can suspend its execution before reaching a return and indirectly passes control to another coroutine. Coroutines are a technique that provides concurrency to a Python application. Concurrency basically means that we can have multiple functions that appear to be executing at the same time but are actually running one at a time in a cooperative manner. This is not parallel processing but cooperative multitasking, which can dramatically improve the scalability and performance of a Python application compared to other synchronous methods.

The general idea behind asyncio is that a developer creates several coroutines that will operate asynchronously with each other. Each coroutine is then called using a task from an event loop that schedules the tasks. This makes the coroutines and tasks nearly synonymous. The event loop will execute a task until it yields execution back to the event loop or to another coroutine. The coroutine may block waiting for an I/O operation or it may simply sleep if the coroutine wants to execute at a periodic interval. It's important to note, however, that if a coroutine is meant to be periodic, there may be jitter in the period, depending on the timing for the other tasks and when the event loop can schedule it to run again.

The general behavior for how coroutines work can be seen in the following diagram, which represents an overview of using coroutines with the asyncio library. This diagram is a modified version of the one presented by Matt Trentini at Pycon AU in 2019 during his talk on asyncio in MicroPython:

As shown in the preceding diagram, the Event Loop schedules a task to be executed that has 100% of the CPU until it reaches a yield point. A yield point is a point in the coroutine where a blocking operation (asynchronous operation) will occur and the coroutine is then willing to give up the CPU until the operation is completed. At this point, the event loop will then schedule other coroutines to run. When the asynchronous event occurs, a callback is used to notify the Event Loop that the event has occurred. The Event Loop will then mark the original coroutine as ready to run and will schedule it to resume when other coroutines have yielded the CPU. At that point, the coroutine can resume operation, but as we mentioned earlier, there could be some time that elapses between the receipt of the callback and the coroutine resuming execution, and this is by no means deterministic.

Now, let's examine how we can use asyncio to rewrite our blinky LED application using cooperative multitasking.

A cooperative multitasking blinky LED example

The first step in creating a railroad blinky LED example is to import the asyncio library. In MicroPython, there is not an asyncio library exactly, but a uasyncio library. To improve portability, many developers will import uasyncio as if it were the asyncio library by importing it at the top of their application, as follows:

import uasyncio as asyncio

Next, we can define our LEDs, just like we did in all our other examples, using the following code:

LED_RED = 1
LED_GREEN = 2
LED_BLUE = 3
LED_YELLOW = 4

If you look back at our example of writing a thread-based application, you'll recall that our task1 code looked as follows:

def task1():
while True:
pyb.LED(LED_BLUE).toggle()
time.sleep_ms(150)

def task2():
while True:
pyb.LED(LED_GREEN).toggle()
time.sleep_ms(150)

This is important to review because creating a coroutine will follow a similar structure! In fact, to tell the Python interpreter that our tasks are asynchronous coroutines, we need to add the async keyword before each of our task definitions, as shown in the following code:

async def task1():
while True:
pyb.LED(LED_BLUE).toggle()
time.sleep_ms(150)
async def task2():
while True:
pyb.LED(LED_GREEN).toggle()
time.sleep_ms(150)

The functions are now coroutines, but they are missing something very important: a yield point! If you examine each of our tasks, you can tell that we really want our coroutine to yield once we have toggled our LED and are going to wait 150 milliseconds. The problem with these functions as they are currently written is that they are making a blocking call to time.sleep_ms. We want to update this with a call to asyncio.sleep_ms and we want to let the interpreter know that we want to relinquish the CPU at this point. In order to do that, we are going to use the await keyword.

The await keyword, when reached by the coroutine, tells the event loop that it has reached a point in its execution where it will be waiting for an event to occur and it is willing to give up the CPU to another task. At this point, control is handed back to the event loop and the event loop can decide what task should be executed next. Using this syntax, our task code for the railroad blinky LED applications would be updated to the following:

async def task1():
while True:
pyb.LED(LED_BLUE).toggle()
await asyncio.sleep_ms(150)
async def task2():
while True:
pyb.LED(LED_GREEN).toggle()
await asyncio.sleep_ms(150)

For the most part, the general structure of our coroutine/task functions remains the same. The difference is that we define the function as async and then use await where we expect the asynchronous function call to be made.

At this point, we just initialize the LEDs using the following code:

pyb.LED(LED_BLUE).on()
pyb.LED(LED_GREEN).off()

Then, we create our event loop.

Creating the event loop for this application requires just four lines of code. The first line will assign the asyncio event loop to a loop variable. The next two lines create tasks that assign our coroutines to the event loop. Finally, we tell the event loop to run forever and our coroutines to execute. These four lines of code look as follows:

loop = asyncio.get_event_loop()
loop.create_task(task1())
loop.create_task(task2())
loop.run_forever()

As you can see, we can create any number of tasks and pass the desired coroutine to the create_task method in order to get them into the event loop. At this point, you could run this example and see that you have an efficiently running railroad blinky LED program that uses cooperative multitasking.

Going further with asyncio

Unfortunately, there just isn't enough time to discuss all the cool capabilities that are offered by asyncio in MicroPython applications. However, as we progress through this book, we will use asyncio and its additional capabilities as we develop our various projects. For those of you who want to dig deeper right now, I would highly recommend checking out Peter Hinch's asyncio tutorial, which also covers how you can coordinate tasks, use queues, and more, with asyncio. You can find the tutorial and some example code at https://github.com/peterhinch/micropython-async/blob/master/TUTORIAL.md#0-introduction.