Skip to content

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 / await concepts
  • GPIO input/output
  • Breadboard, LED, resistor, and push button for the hardware exercise

Development Environment Setup

Thonny Setup

  1. Install Thonny.
  2. Connect the 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 responds with a MicroPython prompt.
main.py
async_button_logic.py
test_async_button_logic.py
hardware_demo.py

Session Agenda

  1. Theory: Why async code needs special testing — 10 min
  2. Designing testable hardware-adjacent logic — 10 min
  3. Hands-on exercise: Simulated async button/LED behavior — 15 min
  4. Hands-on exercise: Run on real Pico hardware — 7 min
  5. 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-specific
  • update_state() → testable logic
  • set_led(value) → hardware-specific
  • handle_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

  1. Copy async_button_logic.py into Thonny.
  2. Copy test_async_button_logic.py into a second tab or separate file.
  3. Run the test script.
  4. Observe:
  5. LED toggles only once
  6. bouncing/holding does not trigger repeated toggles immediately
  7. Modify FakeButton.sequence to simulate multiple presses: python [0, 1, 1, 0, 0, 1, 1, 0]
  8. 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

  1. Open the REPL in Thonny.
  2. Reset the Pico.
  3. Watch printed startup messages.
  4. 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

  1. Why is debouncing important in hardware input handling?
  2. What parts of your code are easiest to test?
  3. How does async improve responsiveness compared to blocking delays?
  4. 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