Skip to content

Session 4: Packaging Reusable Async Components

Synopsis

Teaches how to turn project-specific solutions into reusable modules and frameworks with cleaner APIs and better long-term maintainability.

Session Content

Session 4: Packaging Reusable Async Components

Session Overview

Duration: ~45 minutes
Audience: Python developers with basic programming knowledge, learning MicroPython on Raspberry Pi Pico 2 W
Focus: Designing, organizing, and reusing asynchronous code by packaging common patterns into clean, testable components


Learning Objectives

By the end of this session, learners will be able to:

  • Structure MicroPython projects for reuse and maintainability
  • Create reusable asynchronous components using uasyncio
  • Encapsulate hardware logic into classes and modules
  • Package common async patterns such as periodic tasks, event polling, and cooperative scheduling
  • Build a small reusable async component for an LED/button system
  • Prepare code for future expansion into larger IoT projects

Prerequisites

  • Raspberry Pi Pico 2 W
  • USB cable
  • Thonny IDE or similar MicroPython editor
  • MicroPython firmware installed on the Pico 2 W
  • Basic familiarity with Python functions, loops, and classes
  • Basic understanding of uasyncio
  • Optional hardware:
  • 1 LED
  • 1 resistor (220Ω to 330Ω)
  • 1 push button
  • Breadboard and jumper wires

Development Environment Setup

Thonny Setup

  1. Install Thonny from the official website.
  2. Connect the Raspberry Pi Pico 2 W via USB.
  3. In Thonny:
  4. Go to Tools > Options > Interpreter
  5. Select MicroPython (Raspberry Pi Pico)
  6. Choose the correct port
  7. Confirm the REPL opens successfully.
  8. Save scripts to the Pico using:
  9. main.py for auto-run code
  10. Separate .py modules for reusable components

Suggested Project Structure

Use a simple module-based layout:

/
├── main.py
├── async_button.py
├── async_led.py
└── app.py

Session Plan

1. Introduction: Why Package Async Code? (5 minutes)

Theory

As async projects grow, inline uasyncio code becomes hard to read and reuse. Packaging code into modules and classes helps:

  • keep responsibilities separate
  • improve readability
  • reduce duplication
  • make hardware behavior reusable across projects
  • simplify debugging and testing

Key Idea

A reusable async component should: - expose a small public API - manage its own internal state - avoid blocking calls - cooperate cleanly with other tasks


2. Core Design Patterns for Async Components (10 minutes)

Theory

Common reusable async patterns in MicroPython:

  1. Periodic task wrapper
  2. runs a coroutine every fixed interval

  3. Async poller

  4. checks a sensor or input repeatedly

  5. Event-driven component

  6. reacts to state changes such as button presses

  7. Hardware abstraction

  8. hides pin and timing details behind methods

Best Practices

  • Keep I/O and logic separated
  • Use non-blocking delays with await asyncio.sleep()
  • Minimize work inside tight loops
  • Name modules clearly by responsibility
  • Prefer small, composable components

Hands-On Exercise 1: Reusable Async LED Component (15 minutes)

Goal

Create a reusable class that controls an LED asynchronously with simple methods for on, off, and blink.

Wiring

  • Connect LED anode to GPIO 15 through a resistor
  • Connect LED cathode to GND

Create async_led.py

# async_led.py
# Reusable asynchronous LED component for Raspberry Pi Pico 2 W

import uasyncio as asyncio
from machine import Pin


class AsyncLED:
    """
    Reusable LED helper that supports synchronous and asynchronous control.
    """

    def __init__(self, pin_num, active_high=True):
        """
        Initialize the LED on the given GPIO pin.

        Args:
            pin_num (int): GPIO pin number.
            active_high (bool): True if LED turns on with logic HIGH.
        """
        self.pin = Pin(pin_num, Pin.OUT)
        self.active_high = active_high
        self._state = False
        self.off()

    def on(self):
        """Turn the LED on."""
        self._state = True
        self.pin.value(1 if self.active_high else 0)

    def off(self):
        """Turn the LED off."""
        self._state = False
        self.pin.value(0 if self.active_high else 1)

    def toggle(self):
        """Toggle the LED state."""
        if self._state:
            self.off()
        else:
            self.on()

    async def blink(self, on_time=0.2, off_time=0.2, count=5):
        """
        Blink the LED asynchronously.

        Args:
            on_time (float): Time LED stays on.
            off_time (float): Time LED stays off.
            count (int): Number of blinks.
        """
        for _ in range(count):
            self.on()
            await asyncio.sleep(on_time)
            self.off()
            await asyncio.sleep(off_time)

Create main.py

# main.py
# Demo for reusable asynchronous LED component

import uasyncio as asyncio
from async_led import AsyncLED


async def main():
    led = AsyncLED(15)

    print("Starting LED blink demo...")
    await led.blink(on_time=0.3, off_time=0.3, count=5)
    print("Blink demo complete.")

    while True:
        led.toggle()
        print("LED toggled")
        await asyncio.sleep(1)


asyncio.run(main())

Example Output

Starting LED blink demo...
Blink demo complete.
LED toggled
LED toggled
LED toggled

Discussion Points

  • Why the blink method is async
  • Why toggling is kept separate from blinking
  • How this class can be reused in future projects

Hands-On Exercise 2: Reusable Async Button Component (15 minutes)

Goal

Create a reusable async button class that detects presses with debouncing.

Wiring

  • Connect one side of the push button to GPIO 14
  • Connect the other side to GND
  • Use the Pico’s internal pull-up resistor

Create async_button.py

# async_button.py
# Reusable asynchronous button component for Pico 2 W

import uasyncio as asyncio
from machine import Pin


class AsyncButton:
    """
    Async button reader with basic debounce handling.
    """

    def __init__(self, pin_num, pull=Pin.PULL_UP, active_low=True, debounce_ms=50):
        """
        Initialize the button.

        Args:
            pin_num (int): GPIO pin number.
            pull: Internal pull resistor configuration.
            active_low (bool): True if pressed when pin reads 0.
            debounce_ms (int): Debounce delay in milliseconds.
        """
        self.pin = Pin(pin_num, Pin.IN, pull)
        self.active_low = active_low
        self.debounce_ms = debounce_ms
        self._last_state = self.is_pressed()

    def is_pressed(self):
        """Return True if the button is currently pressed."""
        value = self.pin.value()
        return value == 0 if self.active_low else value == 1

    async def wait_for_press(self):
        """
        Wait until the button is pressed and stable.
        """
        while True:
            if self.is_pressed():
                await asyncio.sleep_ms(self.debounce_ms)
                if self.is_pressed():
                    return
            await asyncio.sleep_ms(10)

Create app.py

# app.py
# Demo app using reusable async button and LED components

import uasyncio as asyncio
from async_led import AsyncLED
from async_button import AsyncButton


async def main():
    led = AsyncLED(15)
    button = AsyncButton(14)

    print("Press the button to toggle the LED.")

    while True:
        await button.wait_for_press()
        led.toggle()
        print("Button pressed, LED state changed.")

        # Wait for release to avoid repeated triggers
        while button.is_pressed():
            await asyncio.sleep_ms(10)


asyncio.run(main())

Example Output

Press the button to toggle the LED.
Button pressed, LED state changed.
Button pressed, LED state changed.

Discussion Points

  • Why debouncing matters
  • How wait_for_press() fits into cooperative multitasking
  • Why the release loop prevents duplicate triggers

Mini Project: Combine Components into a Simple Async App (5 minutes)

Goal

Use both reusable components together in one small app.

main.py

# main.py
# Combined reusable async component demo

import uasyncio as asyncio
from async_led import AsyncLED
from async_button import AsyncButton


async def blinker(led):
    """Background task that blinks LED slowly."""
    while True:
        led.on()
        await asyncio.sleep(1)
        led.off()
        await asyncio.sleep(1)


async def button_task(button, led):
    """Toggle LED when button is pressed."""
    while True:
        await button.wait_for_press()
        led.toggle()
        print("Manual toggle")
        while button.is_pressed():
            await asyncio.sleep_ms(10)


async def main():
    led = AsyncLED(15)
    button = AsyncButton(14)

    asyncio.create_task(blinker(led))
    asyncio.create_task(button_task(button, led))

    while True:
        await asyncio.sleep(5)
        print("App still running...")


asyncio.run(main())

Learning Point

This structure demonstrates how reusable async components can be composed into larger applications without changing their internal implementation.


Review and Best Practices (3 minutes)

Key Takeaways

  • Package related async behavior into modules and classes
  • Keep hardware logic reusable and focused
  • Use uasyncio to avoid blocking the event loop
  • Build small components that can be combined into larger applications
  • Separate interface, state, and timing behavior

Common Mistakes to Avoid

  • Using time.sleep() in async code
  • Mixing hardware setup with app logic
  • Writing large monolithic loops
  • Ignoring button debounce
  • Failing to release hardware resources when needed

Checkpoint Questions

  1. Why is packaging async hardware logic useful?
  2. What makes a component reusable?
  3. Why is await asyncio.sleep() preferred over time.sleep()?
  4. How does debouncing improve button input reliability?
  5. How can AsyncLED and AsyncButton be combined in a larger project?

Optional Extension Activity

Extend the reusable component design by creating one of the following:

  • an async buzzer class
  • an async temperature polling class
  • an async status reporter that sends logs over Wi-Fi
  • a general PeriodicTask helper for repeated async callbacks

Reference Code Summary

async_led.py

  • reusable LED control
  • synchronous and async methods
  • blinking support

async_button.py

  • async button waiting
  • debounce handling
  • active-low support

app.py / main.py

  • task composition
  • cooperative multitasking
  • event-based control flow

Outcome

Learners leave this session with a practical understanding of how to package reusable asynchronous MicroPython components for the Pico 2 W, preparing them for larger hardware and IoT projects.


Back to Chapter | Back to Master Plan | Previous Session