A premium programmable MIDI pedalboard built from scratch on a Raspberry Pi Zero 2 W, designed for live guitar rigs running MainStage.
footswitches + expression pedal → Pi Zero 2 W → USB + BLE MIDI → MainStage → MIDI feedback → 1920×480 display
- Ten footswitches, four menus — 36 primary slots plus an optional hold-for-secondary action on each, sending MIDI notes, control changes, and program changes over USB and Bluetooth LE simultaneously.
- Honest display — incoming MIDI feedback drives the 1920×480 screen, so effect states, the current patch, and tempo always reflect what the DAW actually thinks.
- Expression pedal — a potentiometer through an SPI ADC adds continuous control with configurable range, deadband, and an auto-return “home” mode — behavior that later seeded mini-midi, a compact three-switch sibling rebuilt on a Pico microcontroller.
- Browser configuration — a dependency-free web app served from the pedal itself, with per-button editors, a shared color palette, savable presets, undo/redo, and image upload for button artwork (resized client-side so the Zero 2 W never needs an image library).
- Stage-proof service — the Python app runs as a hardened systemd service with watchdog heartbeats, automatic MIDI reconnects, and an error screen instead of a crash mid-set — backed by 200+ unit tests, booting silently into full-screen artwork instead of a scrolling terminal.
The hard part was making a desktop-free embedded Linux device behave like an appliance.
- The black screen — the UI renders with pygame on KMSDRM and had to be presented through a GLES2 shader after discovering the Pi’s display pipeline scans out plain 2D frames with zero alpha — software looked healthy while the physical screen stayed black. The production panel turned out to be portrait-scan only, so the renderer rotates its canvas in the shader.
- Bluetooth MIDI — required working around a kernel/BlueZ advertising incompatibility on the Pi’s legacy-only radio.
- Inverted buttons — a debounce bug that inverted button state was traced to edges swallowed inside the debounce window and fixed with a reconcile timer that re-reads the real pin level.
Render throttling and zero-copy GL uploads keep the whole thing smooth on a single small core.







