RP2350/Pimoroni Presto MicroPython Development
Overview
This skill provides comprehensive guidance for developing embedded applications on RP2350-based hardware (particularly the Pimoroni Presto) using MicroPython. It covers hardware interfaces, async programming patterns, memory-efficient display rendering, BLE server implementation, MQTT communication, and ADHD-friendly UI design.
When to Use This Skill
Use this skill when:
- Developing for RP2350 or Pimoroni Presto boards
- Implementing touchscreen interfaces with gesture detection
- Creating BLE GATT servers for peripheral role
- Building MQTT clients for IoT communication
- Controlling RGB backlights with PWM and animations
- Rendering graphics on memory-constrained displays
- Implementing async/concurrent task patterns
- Designing ADHD-friendly visual interfaces
- Managing WiFi connectivity with auto-reconnect
- Optimizing memory usage for framebuffer operations
Hardware Context
The Pimoroni Presto is an RP2350-based development board featuring:
- Processor: Dual ARM Cortex-M33 @ 150MHz with 520KB RAM
- Display: 480×480 IPS touchscreen (typically ST7789 controller)
- Touch: Capacitive touch (CST816S or FT6236)
- RGB Backlight: PWM-controlled 3-channel LED
- Connectivity: BLE built-in, WiFi via external module (board-dependent)
- Memory: 4MB flash, 520KB SRAM (limited for full framebuffers)
Core Capabilities
1. Touch Input Handling
Use scripts/touch_handler.py for debounced touch polling and gesture detection.
Key Classes:
TouchHandler: Debounced touch polling with TOUCH_DOWN, TOUCH_UP, TOUCH_DRAG eventsTouchEvent: Event objects with coordinates and timestampsGestureDetector: Swipe and tap gesture recognition
Usage:
from touch_handler import TouchHandler, GestureDetector
touch = TouchHandler(touch_device, debounce_ms=50)
gesture = GestureDetector(min_swipe_distance=50)
while True:
event = touch.poll()
if event:
if event.type == TouchHandler.TOUCH_DOWN:
gesture.start(event.x, event.y)
elif event.type == TouchHandler.TOUCH_UP:
detected = gesture.end(event.x, event.y)
if detected == GestureDetector.SWIPE_LEFT:
handle_swipe_left()
2. BLE GATT Server
Use scripts/ble_gatt_server.py for peripheral role with custom services.
Key Classes:
BLETimerServer: Complete BLE server with timer and battery services- Pre-defined UUIDs for timer state, control, and battery level
- Connection management and automatic re-advertising
Usage:
from ble_gatt_server import BLETimerServer, TIMER_CMD_START, TIMER_CMD_PAUSE
ble = BLETimerServer("Presto-Timer")
ble.start_advertising()
while True:
# Update clients with current state
ble.update_timer_state(remaining_seconds=300, is_running=True)
# Check for commands from clients
command = ble.get_command()
if command == TIMER_CMD_START:
start_timer()
elif command == TIMER_CMD_PAUSE:
pause_timer()
3. MQTT Communication
Use scripts/mqtt_client.py for reliable MQTT with auto-reconnect.
Key Classes:
ReliableMQTTClient: MQTT client with auto-reconnect and offline queuingWiFiManager: WiFi connection manager with signal strength monitoring
Usage:
from mqtt_client import ReliableMQTTClient, WiFiManager
def message_callback(topic, msg):
print(f"Received: {topic} -> {msg}")
# Connect WiFi
wifi = WiFiManager("SSID", "password", hostname="presto-001")
wifi.connect()
# Setup MQTT
mqtt = ReliableMQTTClient(
client_id="presto-001",
broker="192.168.1.100",
callback=message_callback
)
mqtt.connect()
mqtt.subscribe("productivity/timer/control/#")
while True:
mqtt.publish("productivity/status", "online")
mqtt.check_msg() # Non-blocking message check
time.sleep(1)
4. RGB Backlight Control
Use scripts/rgb_backlight.py for PWM-based color control with animations.
Key Classes:
RGBBacklight: PWM-based RGB control with color transitionsPresetColors: ADHD-friendly color constants- Methods for fading, pulsing, rainbow cycling, and urgency gradients
Usage:
from rgb_backlight import RGBBacklight, PresetColors
rgb = RGBBacklight(red_pin=16, green_pin=17, blue_pin=18)
# Instant color change
rgb.set_color(255, 0, 0) # Red
# Smooth fade
rgb.fade_to(0, 255, 0, duration_ms=1000) # Fade to green
# Pulse effect
rgb.pulse(PresetColors.FOCUS_BLUE, period_ms=2000, iterations=5)
# Urgency gradient (timer countdown)
r, g, b = rgb.urgency_gradient(remaining_seconds=300, total_seconds=1500)
rgb.fade_to(r, g, b, duration_ms=500)
Development Workflow
Step 1: Hardware Initialization
Refer to references/presto_hardware.md for complete pin configurations and initialization sequences.
Critical Considerations:
- Verify GPIO pin assignments for your specific board (check schematic)
- SPI clock speed: Use maximum 62.5MHz for ST7789 displays
- I2C frequency: 400kHz for touch controllers
- PWM frequency: 1000Hz typical for RGB backlight
Example Initialization:
from machine import Pin, SPI, I2C, PWM
# Display SPI
spi = SPI(1, baudrate=62_500_000, polarity=0, phase=0,
sck=Pin(10), mosi=Pin(11))
dc = Pin(8, Pin.OUT)
cs = Pin(9, Pin.OUT)
rst = Pin(12, Pin.OUT)
# Touch I2C
i2c = I2C(1, scl=Pin(15), sda=Pin(14), freq=400000)
# RGB Backlight PWM
rgb_r = PWM(Pin(16))
rgb_g = PWM(Pin(17))
rgb_b = PWM(Pin(18))
rgb_r.freq(1000)
rgb_g.freq(1000)
rgb_b.freq(1000)
Step 2: Implement Async Architecture
For responsive applications with multiple concurrent tasks, use asyncio patterns from references/micropython_async.md.
Typical Task Structure:
- UI Task: Poll touch input every 10ms, handle gestures
- Timer Task: Countdown logic every 1 second
- BLE Sync Task: Update BLE clients every 500ms
- MQTT Task: Check messages every 100ms, reconnect if needed
- Display Task: Update display every 100ms (10 FPS)
- Backlight Task: Update RGB based on state every 5 seconds
Example Async Application:
import asyncio
from touch_handler import TouchHandler
from ble_gatt_server import BLETimerServer
from mqtt_client import ReliableMQTTClient, WiFiManager
from rgb_backlight import RGBBacklight
async def touch_task(touch, state):
while True:
event = touch.poll()
if event and event.type == TouchHandler.TOUCH_DOWN:
state.toggle()
await asyncio.sleep_ms(10)
async def ble_task(ble, state):
while True:
ble.update_timer_state(state.remaining, state.running)
command = ble.get_command()
if command is not None:
state.handle_command(command)
await asyncio.sleep_ms(500)
async def backlight_task(rgb, state):
while True:
if state.running:
r, g, b = rgb.urgency_gradient(state.remaining, state.total)
rgb.fade_to(r, g, b, duration_ms=500)
await asyncio.sleep(5)
async def main():
# Initialize hardware
touch = TouchHandler(touch_device)
ble = BLETimerServer("Presto")
rgb = RGBBacklight(16, 17, 18)
state = AppState()
ble.start_advertising()
# Run all tasks concurrently
await asyncio.gather(
touch_task(touch, state),
ble_task(ble, state),
backlight_task(rgb, state)
)
asyncio.run(main())
Step 3: Optimize Display Rendering
Critical Memory Constraint: A full 480×480 RGB565 framebuffer requires ~460KB, nearly exhausting available RAM.
Solutions (choose based on UI complexity):
Option 1: Tile-Based Rendering (for complex full-screen graphics)
- Render display in 60×60 tiles (~7KB per tile)
- Update one tile at a time
- See
TiledDisplayclass inreferences/display_rendering.md
Option 2: Partial Updates (for simple UIs with distinct regions)
- Create small buffers for specific UI elements
- Timer display: 100×100 buffer
- Status bar: 200×30 buffer
- Only update changed regions
- See
PartialUpdateDisplayinreferences/display_rendering.md
Option 3: Reduced Color Depth (for minimal graphics)
- Use 1-bit monochrome for simple timer displays
- Use 8-bit grayscale (10× less memory than RGB565)
Refer to references/display_rendering.md for complete implementation details, drawing primitives, and animation techniques.
Step 4: Implement ADHD-Friendly UI
Follow these principles when designing the interface:
Visual Hierarchy:
- Large, simple elements (big touch targets)
- High contrast colors (white on black, vivid colors)
- Minimal clutter (one primary action per screen)
Color Coding:
- Idle: Gray
(64, 64, 64)- low intensity - Focus: Blue
(30, 144, 255)- calm, engaging - Break: Green
(50, 205, 50)- restful - Urgent: Red
(220, 20, 60)- attention-grabbing - Paused: Amber
(255, 191, 0)- intermediate state
Use the PresetColors class from rgb_backlight.py for consistency.
Progressive Disclosure:
- Default view: Large timer digits, minimal information
- Tap to expand: Show progress bar, stats, controls
- Gestures for quick actions: Swipe to pause, double-tap to reset
Smooth Transitions:
- Always fade between colors (300-500ms duration)
- Use easing functions for animations
- Avoid jarring changes that break hyperfocus
Common Patterns
Pattern: Timer Countdown with Visual Feedback
Combine timer logic, BLE sync, and RGB backlight:
class TimerState:
def __init__(self, duration_seconds):
self.total = duration_seconds
self.remaining = duration_seconds
self.running = False
async def timer_countdown(state):
while True:
if state.running and state.remaining > 0:
state.remaining -= 1
await asyncio.sleep(1)
async def visual_feedback(state, rgb):
while True:
if state.running:
r, g, b = rgb.urgency_gradient(state.remaining, state.total)
rgb.fade_to(r, g, b, duration_ms=500)
await asyncio.sleep(5)
async def ble_sync(state, ble):
while True:
ble.update_timer_state(state.remaining, state.running)
await asyncio.sleep_ms(500)
Pattern: Touch-Based Timer Control
Start/pause timer with tap, reset with long press:
async def touch_control(touch, gesture, state):
touch_start_time = 0
while True:
event = touch.poll()
if event:
if event.type == TouchHandler.TOUCH_DOWN:
gesture.start(event.x, event.y)
touch_start_time = time.ticks_ms()
elif event.type == TouchHandler.TOUCH_UP:
duration = time.ticks_diff(time.ticks_ms(), touch_start_time)
if duration > 1000:
# Long press: reset timer
state.remaining = state.total
state.running = False
else:
# Tap: toggle running
detected = gesture.end(event.x, event.y)
if detected == GestureDetector.TAP:
state.running = not state.running
await asyncio.sleep_ms(10)
Pattern: MQTT Command Handling
Subscribe to control topics and update state:
def mqtt_callback(topic, msg):
"""Handle incoming MQTT messages."""
global app_state
topic_str = topic.decode()
msg_str = msg.decode()
if topic_str == "productivity/timer/control/start":
app_state.running = True
elif topic_str == "productivity/timer/control/pause":
app_state.running = False
elif topic_str == "productivity/timer/control/reset":
app_state.remaining = app_state.total
mqtt = ReliableMQTTClient(
client_id="presto-001",
broker="192.168.1.100",
callback=mqtt_callback
)
mqtt.subscribe("productivity/timer/control/#")
Performance Optimization
Memory Management
Critical Rules:
- Force garbage collection before allocating large buffers
- Delete large objects immediately after use
- Reuse buffers instead of creating new ones
- Monitor memory with
gc.mem_free()during development
Example:
import gc
# Before allocating framebuffer
gc.collect()
print(f"Free before: {gc.mem_free()}")
# Use buffer
buffer = bytearray(100 * 100 * 2)
# ... use buffer ...
del buffer
# Clean up
gc.collect()
print(f"Free after: {gc.mem_free()}")
Task Scheduling
Balance responsiveness vs CPU usage with appropriate sleep times:
- Touch input: 10ms (100 checks/second) - high responsiveness
- Display updates: 100ms (10 FPS) - smooth enough for UI
- BLE sync: 500ms - adequate for state updates
- MQTT check: 100ms - responsive to commands
- Backlight updates: 5000ms - slow changes prevent distraction
Troubleshooting
Touch Not Responding
- Verify I2C connection:
i2c.scan()should show touch controller address - Check touch controller I2C address (typically 0x15 for CST816S)
- Verify GPIO pins match schematic
- Add pull-up resistors to SDA/SCL if needed (typically 4.7kΩ)
BLE Connection Drops
- Reduce BLE notification frequency (increase sleep time in sync task)
- Check for WiFi/BLE interference (shared radio on some modules)
- Verify keepalive interval matches client expectations
- Monitor connection events in
_irq_handler
Display Artifacts or Tearing
- Reduce SPI clock speed if using long cables
- Implement double buffering for small regions
- Use partial updates instead of full screen redraws
- Ensure display CS line is properly controlled
Memory Exhaustion
- Check
gc.mem_free()at critical points - Reduce framebuffer size (use tiles or partial updates)
- Call
gc.collect()before large allocations - Monitor for memory leaks (allocations without deletion)
Resources
This skill includes:
scripts/
Executable Python modules for core functionality:
touch_handler.py: Touch input and gesture detectionble_gatt_server.py: BLE GATT server implementationmqtt_client.py: Reliable MQTT client with auto-reconnectrgb_backlight.py: RGB LED control with animations
references/
Detailed documentation for hardware and software:
presto_hardware.md: Complete hardware reference, pinout, initializationmicropython_async.md: Asyncio patterns and concurrent task managementdisplay_rendering.md: Memory-efficient rendering techniques and UI design
All scripts are production-ready and can be used directly or customized for specific applications.