Session 3: Testing Async Code and Hardware-Adjacent Logic
Synopsis
Covers testable design, isolating hardware dependencies, validating coroutine behavior, and using host-side and device-side strategies to verify logic.
Session Content
Session 3: Testing Async Code and Hardware-Adjacent Logic
Session Overview
Duration: ~45 minutes
Audience: Python developers with basic programming knowledge learning MicroPython on Raspberry Pi Pico 2 W
Focus: Testing asynchronous code, validating timing-sensitive logic, and building confidence in hardware-adjacent behavior without requiring full physical integration every time
Learning Objectives
By the end of this session, learners will be able to:
- Explain why asynchronous code is harder to test than synchronous code
- Separate hardware-adjacent logic from hardware access for easier testing
- Test async coroutines using MicroPython-friendly patterns
- Use simulated inputs to validate behavior before connecting real devices
- Build a simple debounced button-and-LED async logic module
- Understand practical strategies for testing on Pico 2 W with Thonny
Prerequisites
- Raspberry Pi Pico 2 W with MicroPython installed
- Thonny IDE installed and configured
- Basic understanding of:
- variables, functions, loops
async/awaitconcepts- GPIO input/output
- Breadboard, LED, resistor, and push button for the hardware exercise
Development Environment Setup
Thonny Setup
- Install Thonny.
- Connect the Pico 2 W via USB.
- In Thonny:
- Go to Tools > Options > Interpreter
- Select MicroPython (Raspberry Pi Pico)
- Choose the correct port
- Confirm the REPL responds with a MicroPython prompt.
Recommended Project Structure
main.py
async_button_logic.py
test_async_button_logic.py
hardware_demo.py
Session Agenda
- Theory: Why async code needs special testing — 10 min
- Designing testable hardware-adjacent logic — 10 min
- Hands-on exercise: Simulated async button/LED behavior — 15 min
- Hands-on exercise: Run on real Pico hardware — 7 min
- Wrap-up and review — 3 min
1) Theory: Why Async Code Needs Special Testing
Key Challenges
Async code often includes: - timing dependencies - concurrent tasks - state changes over time - interactions with I/O that are not immediate
This makes testing harder than ordinary function calls.
Common issues
- Race conditions: logic behaves differently depending on timing
- Flaky tests: tests pass sometimes and fail at others
- Hardware dependence: direct GPIO calls make tests difficult to isolate
- Sleep-heavy code: long delays slow down feedback
Testing Strategy
A good pattern is to separate: - Pure logic: decisions, state transitions, counters, debouncing rules - Hardware layer: GPIO reads/writes, PWM, UART, Wi-Fi, etc.
Example separation
read_button()→ hardware-specificupdate_state()→ testable logicset_led(value)→ hardware-specifichandle_button_press()→ async orchestration
2) Designing Testable Hardware-Adjacent Logic
Principle: Depend on Interfaces, Not GPIO Directly
Instead of calling GPIO inside all logic, pass in small objects or functions.
Benefits
- easier to simulate inputs
- easier to test state transitions
- logic can be reused for real hardware and mocks
Example: Async Button Debounce Controller
This example models: - button input - debouncing - LED toggle on valid press
3) Hands-on Exercise: Simulated Async Button/LED Behavior
Goal
Create an async logic module that responds to simulated button presses and toggles an LED state.
File: async_button_logic.py
# async_button_logic.py
# MicroPython-compatible async logic for button debounce and LED toggle.
import uasyncio as asyncio
class ButtonLedController:
def __init__(self, button_reader, led_writer, debounce_ms=50):
"""
button_reader: callable that returns 0 or 1
led_writer: callable that accepts 0 or 1
debounce_ms: debounce time in milliseconds
"""
self.button_reader = button_reader
self.led_writer = led_writer
self.debounce_ms = debounce_ms
self.led_state = 0
self._last_button_state = 0
def toggle_led(self):
"""Toggle internal LED state and write it out."""
self.led_state = 1 - self.led_state
self.led_writer(self.led_state)
async def poll_button(self):
"""
Poll the button and toggle LED on a rising edge.
Intended to be called repeatedly from an async loop.
"""
current = self.button_reader()
# Rising edge detection: 0 -> 1
if current == 1 and self._last_button_state == 0:
await asyncio.sleep_ms(self.debounce_ms)
# Re-read after debounce delay
if self.button_reader() == 1:
self.toggle_led()
self._last_button_state = current
async def run(self, interval_ms=20):
"""Run polling loop forever."""
while True:
await self.poll_button()
await asyncio.sleep_ms(interval_ms)
Simulated Test Harness
File: test_async_button_logic.py
# test_async_button_logic.py
# Simple simulation-based test harness for MicroPython async logic.
import uasyncio as asyncio
from async_button_logic import ButtonLedController
class FakeButton:
def __init__(self, sequence):
self.sequence = sequence
self.index = 0
def __call__(self):
if self.index < len(self.sequence):
value = self.sequence[self.index]
self.index += 1
return value
return self.sequence[-1] if self.sequence else 0
class FakeLED:
def __init__(self):
self.history = []
def __call__(self, value):
self.history.append(value)
print("LED set to:", value)
async def test_toggle_once():
# Simulate button idle, then press, then hold, then release
button = FakeButton([0, 0, 1, 1, 1, 0, 0])
led = FakeLED()
controller = ButtonLedController(button, led, debounce_ms=10)
# Poll several times to consume the sequence
for _ in range(7):
await controller.poll_button()
print("LED history:", led.history)
print("Final LED state:", controller.led_state)
async def main():
await test_toggle_once()
asyncio.run(main())
Example Output
LED set to: 1
LED history: [1]
Final LED state: 1
Hands-on Tasks
- Copy
async_button_logic.pyinto Thonny. - Copy
test_async_button_logic.pyinto a second tab or separate file. - Run the test script.
- Observe:
- LED toggles only once
- bouncing/holding does not trigger repeated toggles immediately
- Modify
FakeButton.sequenceto simulate multiple presses:python [0, 1, 1, 0, 0, 1, 1, 0] - Re-run and compare outputs.
4) Hands-on Exercise: Run on Real Pico Hardware
Wiring
Components
- 1 LED
- 1 resistor (220–330 ohms)
- 1 push button
- breadboard and jumper wires
Suggested Connections
- LED anode → GP15 through resistor
- LED cathode → GND
- Button one side → GP14
- Button other side → GND
- Use internal pull-up resistor in code
File: hardware_demo.py
# hardware_demo.py
# Async button and LED demo for Raspberry Pi Pico 2 W.
from machine import Pin
import uasyncio as asyncio
from async_button_logic import ButtonLedController
# Button uses internal pull-up, so pressed = 0
button_pin = Pin(14, Pin.IN, Pin.PULL_UP)
led_pin = Pin(15, Pin.OUT)
def read_button():
# Return 1 when pressed, 0 when released
return 0 if button_pin.value() == 1 else 1
def write_led(value):
led_pin.value(value)
controller = ButtonLedController(read_button, write_led, debounce_ms=50)
async def main():
print("Starting async button/LED demo...")
print("Press the button to toggle the LED.")
await controller.run(interval_ms=20)
try:
asyncio.run(main())
finally:
asyncio.new_event_loop()
Expected Behavior
- Press button once: LED toggles ON
- Press again: LED toggles OFF
- Brief bounce should not cause multiple toggles
Optional Verification Steps
- Open the REPL in Thonny.
- Reset the Pico.
- Watch printed startup messages.
- Press the button repeatedly and observe LED behavior.
5) Mini Testing Checklist for Async Hardware Logic
Before Connecting Hardware
- [ ] Can the logic be exercised with fake inputs?
- [ ] Are delays small and controlled?
- [ ] Is hardware access isolated to small functions?
- [ ] Can state be inspected after each step?
During Hardware Testing
- [ ] Button wiring correct
- [ ] LED resistor present
- [ ] GPIO pins match code
- [ ] Internal pull-up/pull-down matches circuit
- [ ] No repeated triggers on one press
6) Common Mistakes
- putting all logic directly inside GPIO callbacks
- using long
sleep()calls instead of async scheduling - mixing simulated tests and hardware setup too tightly
- forgetting pull-up/pull-down configuration
- assuming one button press always equals one clean signal
7) Hands-on Extension Challenge
Add a Long-Press Detector
Modify the controller so that:
- short press toggles LED
- long press prints "LONG PRESS"
Hint
Track press start time with ticks_ms() and compare duration on release.
Suggested Discussion Questions
- Why is debouncing important in hardware input handling?
- What parts of your code are easiest to test?
- How does async improve responsiveness compared to blocking delays?
- Which logic should remain hardware-independent?
Session Summary
- Async code is best tested when logic is separated from I/O
- Simulation with fake objects makes testing faster and safer
- Debounce and state transitions can be validated without physical hardware
- The same logic can then be connected to Pico 2 W GPIO for real-world use
Takeaway Code Pattern
controller = ButtonLedController(read_button, write_led)
await controller.run()
This pattern keeps hardware access small and testable while preserving async behavior.
Back to Chapter | Back to Master Plan | Previous Session | Next Session