Physical macro pad & server controller with OLED display, rotary encoder, and Hall effect sensors
Features • Hardware • Installation • Quick Start • Documentation • Changelog • License
Pico Commander transforms Raspberry Pi Pico into a powerful physical automation tool that acts as a USB HID keyboard. Control Docker containers, execute complex command sequences, and automate your workflow with a tactile interface featuring:
- OLED Display (128×32) — Visual menu navigation with screensavers
- Rotary Encoder — Browse menus, execute actions with click/long-press
- Hall Effect Sensors — Trigger emergency scenarios (e.g., safe shutdown)
- INA226 Power Monitor — Battery voltage/current tracking with low-power warning and auto-screensaver notifications
- Physical Button — Boot mode control & emergency actions
- Multi-level Menus — Organize commands in nested folders
- Scenario Pipelines — Chain multiple actions with loop/toggle support
For Developers: Pico Commander isn't a framework you configure around, and it's not a library you import — think of it as an automation skeleton or trigger-pipeline sequencer. It's a small runtime (Input → Pipeline → Output) that ships with working modules already wired in: Hall sensors, battery monitor, HID keyboard emulation, GPIO control for relays and optocouplers. The contract between modules is simple enough that adding your own input or output type doesn't require touching the core loop — just drop in a new handler class and reference it in config.json.
See Pipelines Guide for how Input → Pipeline → Output actually fits together.
Technical reference: Raspberry Pi Pico, SSD1306 OLED display, KY-040 rotary encoder, Hall effect sensors
Real-world deployment and operational view
Web-based JSON editor for visual configuration
Full Video Demo: Watch on YouTube
Use Pico Commander when:
- You need physical control without SSH
- Visual feedback is important
- Budget is limited
- You prefer open-source solutions
- Emergency hardware triggers are needed
Consider alternatives when:
- You need more than 20-30 frequently-used commands (Stream Deck has more buttons)
- Color display is required (Stream Deck)
- You need mouse control (not yet implemented)
- WiFi remote triggers are essential (requires Pico W variant)
- HID Keyboard Emulation — Works on any OS without drivers
- Configurable Menu System — JSON-based hierarchical menus
- Action Sequences — Toggle services (start/stop) with single click
- Emergency Triggers — Hall sensor activation for critical scenarios
- Battery Monitoring — INA226-based voltage/current tracking with configurable low-battery warnings and screensaver notifications
- Screen Management — Auto-sleep and customizable screensavers (Tesseract, Starfield, Matrix)
- State Persistence — Remembers menu position and sequence states
- Cooldown Protection — Prevents accidental double-execution (configurable)
- Priority System — Hall sensors bypass cooldown for emergency actions
- Debouncing — Hardware and software anti-bounce for all inputs
- Low-Battery Warning — Blinking OLED alert with encoder-dismiss and cooldown protection (configurable)
- USB Detection — Skips scenarios when USB is disconnected
┌─────────────────────────────────────────────────────────────────┐
│ Raspberry Pi Pico (RP2040) │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌─────────────┐ │
│ │ SSD1306 OLED │ │ KY-040 │ │ Hall Sensor│ │
│ │ 128×32 px │ │ Encoder │ │ (×2) │ │
│ │ │ │ │ │ │ │
│ │ SDA ──── GP4 │ │ CLK ─── GP6 │ │ OUT1 ── GP15│ │
│ │ SCL ──── GP5 │ │ DT ─── GP7 │ │ OUT2 ── GP16│ │
│ └──────────────┘ │ SW ─── GP8 │ └─────────────┘ │
│ └──────────────┘ │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌─────────────┐ │
│ │ Button │ │ LED │ │ GPIO Output │ │
│ │ GP24 (boot) │ │ GP25 (onboard)│ │ GP14 (opt.) │ │
│ └──────────────┘ └──────────────┘ └─────────────┘ │
│ │ │
│ USB ←──── HID Keyboard Output └─→ Relay/ │
│ Opto │
└─────────────────────────────────────────────────────────────────┘
Power: 5V via USB
Typical Current: <100mA (OLED on), <5mA (screen off)
Note: GPIO output (optional) for controlling relays, optocouplers, or power buttons
| Component | Model/Type | Notes |
|---|---|---|
| Microcontroller | Raspberry Pi Pico | RP2040-based board |
| Display | SSD1306 OLED 128×32 | I2C interface (0x3C) |
| Encoder | KY-040 Rotary Encoder | With push button |
| Hall Sensors | Hall Effect Sensors (×2) | Active-low or active-high (optional) |
| Button | Tactile Switch | For boot mode selection |
| LED | (Optional) | Built-in LED on GP25 |
| GPIO Output Module | Relay / Optocoupler | For power control automation (optional) |
1.1. Download CircuitPython
Download the latest CircuitPython 9.0+ UF2 file for Raspberry Pi Pico:
- Official page: https://circuitpython.org/board/raspberry_pi_pico/
- Direct link: circuitpython-9.x.x-raspberry_pi_pico.uf2
1.2. Enter Bootloader Mode
- Disconnect Raspberry Pi Pico from USB
- Hold down the BOOTSEL button on the Pico board
- While holding BOOTSEL, connect Pico to computer via USB cable
- Release BOOTSEL button
- Pico appears as a USB mass storage device named RPI-RP2
1.3. Flash Firmware
- Copy the downloaded
.uf2file to the RPI-RP2 drive - The file copies and Pico automatically reboots
- After reboot, Pico appears as CIRCUITPY drive
- If you see CIRCUITPY, installation succeeded ✓
Verification:
# Linux/macOS - check mounted drive
ls /media/$USER/CIRCUITPY # or check /Volumes/CIRCUITPY on macOS
# Windows - drive letter (e.g., D:, E:, F:)
dir D:\ # Replace D: with actual drive letter2.1. Download Adafruit CircuitPython Library Bundle
-
Check your CircuitPython version:
- Open
boot_out.txton CIRCUITPY drive - Look for line like:
Adafruit CircuitPython 9.0.0 on 2024-... - Note the major version (8.x or 9.x)
- Open
-
Download matching bundle:
- Go to: https://circuitpython.org/libraries
- Click "Download Bundle" button
- Or direct link: https://github.com/adafruit/Adafruit_CircuitPython_Bundle/releases/latest
- Download:
adafruit-circuitpython-bundle-9.x-mpy-YYYYMMDD.zip(or 8.x if using older CircuitPython)
-
Extract the ZIP file to a temporary folder
2.2. Locate Required Libraries in Bundle
After extracting, navigate to the lib/ folder inside the bundle. You need these 3 items:
From bundle lib/ folder, copy to CIRCUITPY/lib/:
1. adafruit_displayio_ssd1306.mpy (single file)
2. adafruit_display_text/ (entire folder)
3. adafruit_hid/ (entire folder)
2.3. Copy Libraries to Pico
Manual Method (drag and drop):
- Open CIRCUITPY drive in file explorer
- Create
lib/folder if it doesn't exist - Copy the 1 file + 2 folders from bundle to
CIRCUITPY/lib/
Command Line Method:
# Linux/macOS (adjust paths to your bundle location)
cp /path/to/bundle/lib/adafruit_displayio_ssd1306.mpy /media/$USER/CIRCUITPY/lib/
cp -r /path/to/bundle/lib/adafruit_display_text /media/$USER/CIRCUITPY/lib/
cp -r /path/to/bundle/lib/adafruit_hid /media/$USER/CIRCUITPY/lib/
# Windows (PowerShell)
Copy-Item C:\path\to\bundle\lib\adafruit_displayio_ssd1306.mpy D:\lib\
Copy-Item -Recurse C:\path\to\bundle\lib\adafruit_display_text D:\lib\
Copy-Item -Recurse C:\path\to\bundle\lib\adafruit_hid D:\lib\Quick Install (if cloning this repository):
The lib/ folder in this repo already contains all required libraries. Simply copy the entire lib/ folder to your CIRCUITPY drive:
# Linux/macOS
cp -r lib/* /media/$USER/CIRCUITPY/lib/
# Windows (PowerShell)
Copy-Item -Recurse lib\* D:\lib\ # Replace D: with your drive2.4. Verify Library Installation
Check that CIRCUITPY/lib/ contains:
CIRCUITPY/lib/
├── adafruit_displayio_ssd1306.mpy ✓ Single .mpy file
├── adafruit_display_text/ ✓ Folder with 6 .mpy files
│ ├── __init__.mpy
│ ├── bitmap_label.mpy
│ ├── label.mpy
│ ├── outlined_label.mpy
│ ├── scrolling_label.mpy
│ └── text_box.mpy
└── adafruit_hid/ ✓ Folder with 8 .mpy files
├── __init__.mpy
├── consumer_control.mpy
├── consumer_control_code.mpy
├── keyboard.mpy
├── keyboard_layout_base.mpy
├── keyboard_layout_us.mpy
├── keycode.mpy
└── mouse.mpy
If libraries missing: Re-download bundle and verify version match.
3.1. Copy Python Files
Copy all Python source files to the root of CIRCUITPY drive:
# Required files (copy to CIRCUITPY root):
boot.py # Boot configuration & USB mode
code.py # Main application entry point
config.py # Configuration loader
config.json # Menu & scenarios configuration
display.py # OLED display manager
encoder.py # Rotary encoder handler
inputs_manager.py # Unified input handlers (Hall, INA226, button)
output_base.py # Base class for output modules
output_hid.py # USB HID keyboard output
output_gpio.py # GPIO output (relays, optocouplers)
trigger_bus.py # Pipeline execution engine
screensaver.py # Screensaver effects (Tesseract, Starfield, Matrix)
ina226_monitor.py # INA226 power monitor driver
splash_screen.py # Battery status HUD overlay
splash_trigger.py # Splash screen interval timer
warning_screen.py # Low-battery warning overlayManual Copy:
- Open CIRCUITPY drive in file explorer
- Drag and drop all
.pyand.jsonfiles to the root - Ensure
lib/folder exists with libraries from Step 2
Linux/macOS Quick Deploy:
# From project directory
cp *.py *.json /media/$USER/CIRCUITPY/
cp -r lib /media/$USER/CIRCUITPY/Windows Quick Deploy (PowerShell):
# From project directory
Copy-Item *.py, *.json D:\ # Replace D: with drive letter
Copy-Item -Recurse lib D:\3.2. Verify File Structure
Check that CIRCUITPY drive has this structure:
CIRCUITPY/
├── boot.py
├── code.py
├── config.py
├── config.json
├── display.py
├── encoder.py
├── inputs_manager.py
├── output_base.py
├── output_hid.py
├── output_gpio.py
├── trigger_bus.py
├── screensaver.py
├── ina226_monitor.py
├── splash_screen.py
├── splash_trigger.py
├── warning_screen.py
├── lib/
│ ├── adafruit_displayio_ssd1306.mpy
│ ├── adafruit_display_text/
│ └── adafruit_hid/
└── (other CircuitPython system files)
4.1. Connect Components
Follow the hardware scheme above. Pin connections:
| Component | Pin Connection | Notes |
|---|---|---|
| OLED Display (SSD1306) | ||
| SDA | GP4 | I2C Data |
| SCL | GP5 | I2C Clock |
| VCC | 3.3V | Power |
| GND | GND | Ground |
| Rotary Encoder (KY-040) | ||
| CLK | GP6 | Encoder clock |
| DT | GP7 | Encoder data |
| SW | GP8 | Push button |
| + | 3.3V | Power |
| GND | GND | Ground |
| Hall Sensors | ||
| Sensor 1 OUT | GP15 | Active-low or high |
| Sensor 2 OUT | GP16 | Active-low or high |
| VCC | 3.3V | Power |
| GND | GND | Ground |
| Boot Button | GP24 | Tactile switch to GND |
| LED | GP25 | Built-in (no wiring needed) |
4.2. Power Connection
Connect Raspberry Pi Pico to computer or USB power supply via micro-USB cable.
Safety Notes:
- Use 3.3V power for all components
- Do not connect 5V directly to GPIO pins
- Verify polarity before powering on
5.1. Edit config.json
Use the web-based Config Studio editor (editor.html) or manually edit config.json:
Option A: Visual Editor (Recommended)
- Open
editor.htmlin a web browser - Click "Load config.json" and select the file from CIRCUITPY drive
- Configure hardware pins, menus, and scenarios
- Click "Save config.json"
- Copy saved file back to CIRCUITPY drive
Option B: Manual Editing
- Open
config.jsonin a text editor - Modify
hardwaresection if using different pins - Customize
active_menuwith your menu items - Add
scenariosfor your automation tasks - Save and copy to CIRCUITPY drive
See Configuration Guide for detailed instructions.
5.2. Verify Configuration
Check critical settings in config.json:
{
"inputs": {
"hall_sensor_1": {
"type": "hall",
"pin": 15,
"active_low": true
}
},
"outputs": {
"hid": {
"type": "hid",
"enabled": true
},
"opto_pwr": {
"type": "gpio",
"pin": 14,
"enabled": true
}
},
"hardware": {
"encoder_clk": 6,
"encoder_dt": 7,
"encoder_sw": 8,
"display_sda": 4,
"display_scl": 5
},
"device": {
"armed": true,
"cooldown_ms": 5000,
"screen_timeout_s": 15
}
}6.1. Boot Modes
Pico Commander has two boot modes:
| Mode | How to Enter | Purpose |
|---|---|---|
| Normal Mode | Boot without pressing GP24 button | Runtime operation - file system writable from code |
| Development Mode | Hold GP24 button during boot, OR use REPL storage.remount() |
File editing - USB drive writable, code read-only |
6.2. Test Boot
- Disconnect Pico from USB
- Reconnect to power (no button pressed = Normal Mode)
- OLED display should show "Booting..." then menu
Expected Behavior:
- OLED displays menu items
- Rotary encoder changes selection with swipe animation
- Click encoder executes actions
- LED blinks on startup
6.3. Enter Development Mode (for editing files)
Method 1: Hardware Button (Recommended)
- Disconnect Pico
- Hold GP24 button
- Connect to USB while holding button
- Release button
- CIRCUITPY drive is now writable for editing files
Method 2: Software via REPL (Alternative)
If you can't physically access the GP24 button, use REPL to remount filesystem:
- Connect to Pico's serial console (screen, minicom, Mu editor, or Thonny)
- Press
Ctrl+Cto interrupt running code and enter REPL - Run this code:
import storage
storage.remount("/", readonly=False)
# Now you can edit files via REPL or use storage.erase_filesystem() if needed- To make filesystem writable on next boot, modify
boot.py:
with open("/boot.py", "a") as f:
f.write('\n# Enable USB write access\nimport storage\nstorage.remount("/", readonly=False)\n')
import microcontroller
microcontroller.reset()Warning: Making filesystem always writable from USB reduces safety (code can't reliably write state.json). Use Method 1 when possible.
Note: In Normal Mode, state.json is auto-created for saving menu position and sequence states.
7.1. Basic Navigation Test
- Rotate encoder → Menu items scroll with animation
- Click encoder → Executes action or enters folder
- Long press (>1s) → Returns to parent menu
7.2. Test Scenario
Create a simple test scenario:
"scenarios": {
"test_hello": [
{"output": "hid", "action": "type", "value": "Hello from Pico Commander!"},
{"output": "hid", "action": "key", "combo": "enter"}
]
}Add menu item:
"active_menu": [
{
"id": "test_item",
"label": "Test Hello",
"pipeline": ["test_hello"],
"loop": false
}
]Open a text editor on your computer, navigate to test item on Pico, click encoder. Text should appear.
7.3. Test Hall Sensor
- Verify sensor wiring and trigger binding in
config.json - Trigger sensor (bring magnet near Hall sensor)
- Bound scenario should execute
| Issue | Solution |
|---|---|
| CIRCUITPY drive doesn't appear | Re-flash CircuitPython, try different USB cable/port |
| "No module named..." error | Check library files in lib/ folder, verify bundle version matches CircuitPython |
| OLED display blank | Verify I2C wiring (SDA/SCL), check I2C address (0x3C), test with i2c scanner |
| Encoder doesn't respond | Check CLK/DT/SW pins, verify encoder_clk, encoder_dt, encoder_sw in config |
| USB keyboard not working | Check boot.py enables HID, verify usb_hid.enable() line |
| File system read-only error | Boot in Development Mode (hold GP24 button during boot) |
| Scenarios don't execute | Check USB connection, verify scenario name in menu sequence, test manually |
This project uses the following Adafruit CircuitPython libraries:
| Library | Version | Purpose | License |
|---|---|---|---|
| adafruit_displayio_ssd1306 | Latest | SSD1306 OLED display driver | MIT |
| adafruit_display_text | Latest | Text rendering and labels | MIT |
| adafruit_hid | Latest | USB HID keyboard emulation | MIT |
System Dependencies:
displayio— Built-in CircuitPython display frameworkbusio— Built-in I2C/SPI communicationboard— Built-in GPIO pin definitionsdigitalio— Built-in GPIO controlrotaryio— Built-in rotary encoder supportusb_hid— Built-in USB HID interfaceterminalio— Built-in terminal font
All Adafruit libraries are available under the MIT License.
Download from: https://circuitpython.org/libraries
Library Documentation:
- CircuitPython Core: https://docs.circuitpython.org/
- Adafruit DisplayIO Guide: https://learn.adafruit.com/circuitpython-display-support-using-displayio
- Adafruit HID Guide: https://learn.adafruit.com/circuitpython-essentials/circuitpython-hid-keyboard-and-mouse
Comprehensive guides available in the docs/ directory:
- Config Editor Guide — Web-based visual configuration tool
- Configuration Examples — See
config.jsonfor reference implementation
Use editor.html for a graphical configuration interface with drag-and-drop menu builder, scenario editor, and validation. See Config Editor Guide for complete documentation.
{
"id": "docker_service",
"label": "Docker App",
"pipeline": [
{"scenario": "scenario_docker_stop", "label": "Stop"},
{"scenario": "scenario_docker_start", "label": "Start"}
],
"loop": true
}Notes:
pipeline: Array of scenarios to executeloop: true: Toggle mode — each click cycles through pipeline (Stop → Start → Stop...)loop: false: Chain mode — executes entire pipeline in one click
{
"id": "servers",
"label": "Servers",
"submenu": [
{
"id": "web",
"label": "Web Server",
"pipeline": ["scenario_restart_web"],
"loop": false
}
]
}"scenarios": {
"scenario_docker_stop": [
{"output": "hid", "action": "enter", "count": 3},
{"wait": 200},
{"output": "hid", "action": "type", "value": "docker-compose stop"},
{"output": "hid", "action": "key", "combo": "enter"}
],
"scenario_test_gpio": [
{"output": "hid", "action": "type", "value": "Pressing power button..."},
{"output": "hid", "action": "key", "combo": "enter"},
{"wait": 500},
{"output": "opto_pwr", "action": "gpio_pulse"},
{"wait": 500},
{"output": "hid", "action": "type", "value": "Done!"},
{"output": "hid", "action": "key", "combo": "enter"}
]
}Notes:
- Each step specifies
"output"field (defaults to"hid"if omitted for backward compatibility) - HID and GPIO actions can be freely mixed in one scenario (see
scenario_test_gpioexample) {"wait": ms}pauses execution between steps
| Action | Output | Description | Parameters |
|---|---|---|---|
type |
hid |
Type text string | value (string) |
key |
hid |
Press key combination | combo (e.g., "ctrl+c", "super+l") |
enter |
hid |
Press Enter N times | count (number, default: 1) |
wait |
— | Pause execution | ms (milliseconds) or wait (alias) |
gpio_pulse |
gpio |
Short pulse (250ms) | None (fixed duration) |
gpio_set |
gpio |
Set pin to HIGH/LOW | value: "high" or "low" |
gpio_hold |
gpio |
Hold HIGH for duration | duration_ms (milliseconds) |
Multi-Output Scenarios:
See scenario_test_opto_pulse in the included config.json for a real example of mixing HID keyboard output (status messages) with GPIO output (physical button press) in a single scenario.
- Rotate Encoder — Scroll through menu items
- Click Encoder — Execute current action / Enter submenu
- Long Press (>1s) — Go back to parent menu
- Hall Sensor Trigger — Execute emergency scenario
| Mode | How to Enter | Behavior |
|---|---|---|
| Normal | Boot without button pressed | File system writable from code, USB read-only |
| Development | Hold GP24 during boot OR REPL storage.remount() |
File system writable from USB, read-only from code |
- Homelab Management — Control Docker stacks, restart services
- Server Administration — Quick SSH commands, safe shutdowns
- DevOps Automation — Deploy scripts, restart containers
- Smart Home — Trigger MQTT commands, control devices
- Emergency Actions — Hall sensor activated safe shutdown
| File | Purpose |
|---|---|
boot.py |
USB HID initialization and boot mode configuration |
code.py |
Main application loop — menu navigation, encoder handling, display updates |
config.py |
Configuration and state file loader/saver |
config.json |
User configuration — inputs, outputs, menus, scenarios, hardware pins |
display.py |
OLED display manager (SSD1306) with menu rendering and screensaver support |
encoder.py |
Rotary encoder handler (KY-040) with debouncing and event callbacks |
inputs_manager.py |
Unified input sensor manager — Hall sensors, INA226 power monitor, button |
output_base.py |
Base class for output modules (extensibility contract) |
output_hid.py |
USB HID keyboard output handler — types text and key combinations |
output_gpio.py |
GPIO output handler — controls relays, optocouplers, physical buttons |
trigger_bus.py |
Pipeline execution engine — routes scenario steps to appropriate outputs |
screensaver.py |
Screensaver animations — Tesseract, Starfield, Matrix effects |
ina226_monitor.py |
INA226 power monitor I2C driver — voltage/current/battery percentage |
ina226_debug.py |
INA226 diagnostic utility for I2C troubleshooting |
splash_screen.py |
Battery status HUD overlay during screensaver with animated percentage |
splash_trigger.py |
Timer for periodic splash screen display during idle periods |
warning_screen.py |
Low-battery warning overlay with blinking alert and encoder dismiss |
editor.html |
Web-based Config Studio — visual editor for config.json with drag-and-drop |
settings.toml.example |
Example CircuitPython settings file for WiFi/environment config |
lib/ |
CircuitPython libraries (Adafruit HID, display drivers) |
Each input/output type follows a small extensible contract — see Inputs and Outputs developer guides if you want to add your own.
Contributions welcome! Please:
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
This project is licensed under the MIT License - see the LICENSE file for details.
Copyright (c) 2024 makepkg
- Adafruit — CircuitPython libraries
- Raspberry Pi Foundation — RP2040 microcontroller
- Community contributors and testers
- Issues: GitHub Issues
- Documentation: See inline code comments and
config.jsonexamples
If you find this project useful, consider supporting its development:
USDT BEP-20: 0xd03499C9c6100Af624603b4D6fb185A65694745C
USDT TRC-20: TUAzeSrKeDYbt6HCs9PL6q1t5amHHdnnwR
USDT SOLANA: 2cecCCh8pzUNmEpjLQ3aa9sfPL5KXqANrmSfiiDWubCj
Made with ❤️ for the maker community
Contributing Guidelines • Report Bug • Request Feature
⭐ Star this repo if you find it useful!


