MicroPython: ESP32/ESP8266 Asynchronous Programming – Run Multiple Tasks

In this guide, we’ll take a look at the basics of MicroPython asynchronous programming with the ESP32 and ESP8266 NodeMCU using the asyncio module. You’ll learn to run multiple tasks concurrently, making the illusion of multitasking and avoiding blocking your code on long-running tasks.

MicroPython: ESP32/ESP8266 Asynchronous Programming - Run Multiple Tasks

New to MicroPython? Check out our eBook: MicroPython Programming with ESP32 and ESP8266 eBook

For example, your program can be waiting for the response of a server and still be able to do other tasks like checking if a button was pressed or blinking an LED at the same time.

Asynchronous programming can be useful in projects that include: interacting with databases, communicating over networks (like when requesting data from a server, or when the ESP32/ESP8266 acts as a web server), reading sensor data, displaying output to a screen, receiving inputs from users, and much more.

Table of Contents:

In this tutorial, we’ll cover the following topics:

Prerequisites

To follow this tutorial you need MicroPython firmware installed in your ESP32 or ESP8266 boards. You also need an IDE to write and upload the code to your board. We suggest using Thonny IDE or uPyCraft IDE:

Parts Required

To follow this tutorial you need the following parts:

You can use the preceding links or go directly to MakerAdvisor.com/tools to find all the parts for your projects at the best price!

Introducing Asynchronous Programming

Asynchronous programming is a technique that enables your program to start a potentially long-running task and still be responsive to other events while that task runs, rather than having to wait until that task has finished.

This is achieved by executing tasks in a non-blocking manner and using callback functions to handle the results. This way, the program can continue executing other tasks while waiting for the results of the asynchronous task. On the other hand, in synchronous programming, each task must wait for the previous task to complete before starting.

In summary…

  • Asynchronous is a non-blocking architecture, so the execution of one task isn’t dependent on another. Tasks can run simultaneously.
  • Synchronous is a blocking architecture, so the execution of each operation depends on completing the one before it.
Synchronous vs Asynchronous Programming

The asyncio MicroPython Module

MicroPython provides the asyncio module, which is a lightweight asynchronous I/O framework inspired by Python’s asyncio module. You can check all the details about this MicroPython module on the following link:

Let’s take a quick look at some basic concepts you need to know to start using asyncio for asynchronous programming:

  • Event Loop: it’s a loop that continually checks for events (tasks or coroutines) and executes them.
  • Tasks: individual units of work or coroutines that are scheduled to run concurrently within the event loop
  • Asynchronous Functions: also known as coroutines, these are functions that can be paused and resumed without blocking other operations, enabling concurrent execution of multiple tasks.
  • await: is a keyword used inside coroutines to pause the execution of the current coroutine until a specific event or operation completes, allowing other coroutines to run in the meantime.

Now, let’s take a more detailed look at each of those concepts and the workflow and methods from the asyncio module to write an asynchronous program.

Event Loop

An event loop is the core of asynchronous programming. It’s a loop that continually checks for events (tasks or coroutines) and executes them. In asyncio, you create an event loop using asyncio.get_event_loop().

The following line creates an instance of the event loop called loop. You can use that loop to manage and schedule asynchronous tasks.

loop = asyncio.get_event_loop()

Creating Tasks

In asynchronous programming, tasks represent units of work. You create tasks to execute coroutines concurrently. Coroutines are functions defined with the async keyword, that can be paused and resumed, allowing asynchronous programming.

For example:

async def blink_led():
    #Code to blink an LED

Then, we can use loop.create_task() to schedule this coroutine as a task to be executed by the event loop.

loop.create_task(blink_led())

Running the Event Loop

Once you’ve created tasks, you start the event loop to execute them. The loop will continually check for scheduled tasks and execute them. The following line initiates the event loop, as we’ve seen previously.

loop = asyncio.get_event_loop()

Then, loop.run_forever() makes it run indefinitely, constantly checking for tasks to execute.

loop.run_forever()

Asynchronous Functions – async def

An asynchronous function is defined using the async def syntax. These functions, also known as coroutines, can be paused with the await keyword, allowing other coroutines to run in the meantime. For example:

async def blink_led():
    while True:
        led.toggle()  # Toggle LED state
        await asyncio.sleep(1)

Here, blink_led() is an asynchronous function. It toggles an LED and then pauses for 1 second using await asyncio.sleep(1). During this pause, other tasks can run.

await asyncio.sleep(1)

Asynchronous Delay – acyncio.sleep()

asyncio.sleep() is a coroutine provided by the asyncio module and it is used to introduce a delay in the execution of a coroutine for a specified duration without blocking the entire event loop. So, to create an asynchronous function, you must replace all your time.sleep() with asyncio.sleep().

When asyncio.sleep() is called within a coroutine, it temporarily suspends the execution of that coroutine, allowing other coroutines to run in the meantime.

The event loop continues to run while the coroutine is paused, checking for other tasks and events.

After the specified duration (seconds), the paused coroutine resumes execution from the point where asyncio.sleep() was called.

await asyncio.sleep() is a non-blocking way to yield control to other coroutines in the event loop without introducing any actual delay. It effectively allows other coroutines to run immediately.

await

The await keyword is used inside coroutines to indicate a point where the coroutine can be temporarily suspended until a specific event is completed.

In the following example, the coroutine pauses for 1 second without blocking the entire event loop.

await asyncio.sleep(1)

In Summary…

When you run an asynchronous program, the event loop runs continuously, executing the scheduled tasks concurrently. Each coroutine gets a turn to run, and the await statements allow the event loop to switch between tasks, creating the appearance of parallelism.

As a result, you can perform multiple tasks concurrently without having to wait for each one to complete before moving on to the next.

Basic Example of an Asynchronous Program

Now that you know the basic concepts of asynchronous programming and the basic methods of the asyncio module let’s create a simple example to apply the concepts learned.

We’ll create two different coroutines. Each coroutine will blink a different LED at a different rate.

  • Green LED: GPIO 14 >> blinks every two seconds;
  • Blue LED: GPIO 12 >> blinks every half a second.

To visually check the final result, wire two LEDs to the ESP32 or ESP8266, one to GPIO 14 and the other to GPIO 12.

ESP32

ESP32 Asynchronous Programming MicroPython - blink multiple LEDs

ESP8266 NodeMCU

ESP32 Asynchronous Programming MicroPython - blink multiple LEDs

Here’s our example code.

# Rui Santos & Sara Santos - Random Nerd Tutorials
# Complete project details at https://RandomNerdTutorials.com/micropython-esp32-esp8266-asynchronous-programming/

import asyncio
from machine import Pin

green_led_pin = 14
green_led = Pin(green_led_pin, Pin.OUT)
blue_led_pin = 12
blue_led = Pin(blue_led_pin, Pin.OUT)

# Define coroutine function
async def blink_green_led():
    while True:
        green_led.value(not green_led.value() )
        await asyncio.sleep(2) 

# Define coroutine function
async def blink_blue_led():
    while True:
        blue_led.value(not blue_led.value())
        await asyncio.sleep(0.5)

# Define the main function to run the event loop
async def main():
    # Create tasks for blinking two LEDs concurrently
    asyncio.create_task(blink_green_led())
    asyncio.create_task(blink_blue_led())

# Create and run the event loop
loop = asyncio.get_event_loop()  
loop.create_task(main())  # Create a task to run the main function
loop.run_forever()  # Run the event loop indefinitely

View raw code

How the Code Works

Let’s take a quick look at the code.

We create two coroutines. One for each LED:

# Define coroutine function
async def blink_green_led():
    while True:
        green_led.value(not green_led.value())
        await asyncio.sleep(2) 

# Define coroutine function
async def blink_blue_led():
    while True:
        blue_led.value(not blue_led.value())
        await asyncio.sleep(0.5)

Inside each coroutine, we toggle the LED state in a loop and await (asynchronous waiting) for a specific interval await asyncio.sleep(0.5).

We create another coroutine called main() that serves as a central point where you can organize and coordinate the execution of those tasks.

In the main() coroutine, we create tasks for both blink_green_led() and blink_blue_led() functions to run concurrently.

# Define the main function to run the event loop
async def main():
    # Create tasks for blinking two LEDs concurrently
    asyncio.create_task(blink_green_led())
    asyncio.create_task(blink_blue_led())

Then, we create an event loop.

loop = asyncio.get_event_loop()  

The loop.create_task(main()) creates a task to run the main() coroutine function.

loop.create_task(main())  # Create a task to run the main function

Finally, the run_forever() method will start running the event loop indefinitely. The event loop continually checks for tasks to execute, runs them concurrently, and manages their execution.

loop.run_forever()  # Run the event loop indefinitely

Testing the Code

Run the previous code on your ESP32 or ESP8266. The result will be two blinking LEDs at different rates.

Wrapping Up

This is just a simple example to show you how to write an asynchronous program in MicroPython for the ESP32 and ESP8266. Now, you should understand that instead of blinking an LED, you can do more complex tasks like reading a file, requesting data from the internet, handle a web server with multiple clients, and more.

We hope you find this guide useful. To learn more about MicroPython with the ESP32/ESP8266 make sure to take a look at our resources:



Learn how to build a home automation system and we’ll cover the following main subjects: Node-RED, Node-RED Dashboard, Raspberry Pi, ESP32, ESP8266, MQTT, and InfluxDB database DOWNLOAD »
Learn how to build a home automation system and we’ll cover the following main subjects: Node-RED, Node-RED Dashboard, Raspberry Pi, ESP32, ESP8266, MQTT, and InfluxDB database DOWNLOAD »

Enjoyed this project? Stay updated by subscribing our newsletter!

7 thoughts on “MicroPython: ESP32/ESP8266 Asynchronous Programming – Run Multiple Tasks”

  1. time.sleep() needs to be time.sleep(0)

    Create and run the event loop

    loop = asyncio.get_event_loop()
    loop.create_task(main()) # Create a task to run the main function
    loop.run_forever() # Run the event loop indefinitely

    Is helpful as it shows what is happening in the background. In recent examples you now see a “one-liner”:

    asyncio.run(main())

    Reply
      • AFAIK C/C++ does not have a coroutine structure built-in to the language. I do see a few attempts to define a coroutine system for Arduino C/C++ using macros and ‘Duff’s device’ but I find the resulting code rather ugly and hard to read.

        There are a number of libraries of task schedulers that have been written for Arduino C/C++ but I don’t know if any of them have gained any acceptance as the ‘right way to do it’.

        Reply
  2. Hi Sara, This tutorial code has the same problem seen in the similar tutorial you just published for the RPi Pico. Namely the example code shows:

    Define coroutine function

    async def blink_blue_led():
    while True:
    blue_led.value(not blue_led.value())
    await asyncio.sleep(1)

    The line “await asyncio.sleep(1)” should have a sleep time of 2.0 Sec to agree with the description Green LED: GPIO 14 >> blinks every two seconds;

    Note: The code snippet in the section “How the Code Works” shows the correct value.

    Reply
  3. Great tutorial!! Would this work or be the beginning for a network attached print server? I have a older printer that has a usb port for connection. I would like to attach a ESP32 usb port and allow it to pass the print commands to the printer over my local net work

    Reply

Leave a Comment

Download Our Free eBooks and Resources

Get instant access to our FREE eBooks, Resources, and Exclusive Electronics Projects by entering your email address below.