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
- Install Thonny from the official website.
- Connect the Raspberry Pi Pico 2 W via USB.
- In Thonny:
- Go to Tools > Options > Interpreter
- Select MicroPython (Raspberry Pi Pico)
- Choose the correct port
- Confirm the REPL opens successfully.
- Save scripts to the Pico using:
main.pyfor auto-run code- Separate
.pymodules 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:
- Periodic task wrapper
-
runs a coroutine every fixed interval
-
Async poller
-
checks a sensor or input repeatedly
-
Event-driven component
-
reacts to state changes such as button presses
-
Hardware abstraction
- 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
uasyncioto 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
- Why is packaging async hardware logic useful?
- What makes a component reusable?
- Why is
await asyncio.sleep()preferred overtime.sleep()? - How does debouncing improve button input reliability?
- How can
AsyncLEDandAsyncButtonbe 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
PeriodicTaskhelper 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.