Physical macro pad & server controller with OLED display, rotary encoder, and Hall effect sensors
Features • Hardware • Installation • Quick Start • Documentation • 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)
- Physical Button — Boot mode control & emergency actions
- Multi-level Menus — Organize commands in nested folders
- Scenario Sequences — Chain multiple actions per menu item
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
Live demonstration of menu navigation and command execution
Full Video Demo: Watch on YouTube
| Feature | Pico Commander | Stream Deck | DIY Arduino | SSH Aliases |
|---|---|---|---|---|
| Cost | ~$15 | $150+ | ~$20 | Free |
| Display | 128×32 OLED | Color LCD | None (typical) | Terminal only |
| Visual Feedback | Yes | Yes | No | No |
| Programming | JSON config | Proprietary software | C++ code | Bash scripting |
| Portability | Any OS via USB HID | Windows/Mac only | Any OS via USB HID | SSH access required |
| Menu Navigation | Yes (hierarchical) | Grid layout | N/A | Command memory |
| Extensibility | Open source | Closed ecosystem | Full control | Script-based |
| Setup Time | 10 minutes | 30 minutes | 1-2 hours | Varies |
| Emergency Triggers | Hall sensors | No | Possible | No |
| Offline Operation | Yes | Yes | Yes | No |
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
- 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
- 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 │ │
│ │ GP24 (boot) │ │ GP25 (onboard)│ │
│ └──────────────┘ └──────────────┘ │
│ │
│ USB ←──── HID Keyboard Output │
└─────────────────────────────────────────────────────────────────┘
Power: 5V via USB
Typical Current: <100mA (OLED on), <5mA (screen off)
| 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 |
| Button | Tactile Switch | For boot mode selection |
| LED | (Optional) | Built-in LED on GP25 |
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
passive.py # Hall sensors & button handler
trigger_bus.py # Scenario execution engine
screensaver.py # Screensaver effectsManual 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
├── passive.py
├── screensaver.py
├── trigger_bus.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:
{
"hardware": {
"encoder_clk": 6, // Verify pins match your wiring
"encoder_dt": 7,
"encoder_sw": 8,
"display_sda": 4,
"display_scl": 5,
// ... other pins
},
"device": {
"armed": true, // Enable Hall sensors
"cooldown_ms": 5000, // Anti-spam protection
"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 | 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)
- Disconnect Pico
- Hold GP24 button
- Connect to USB while holding button
- Release button
- CIRCUITPY drive is now writable for editing files
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": [
{"action": "type", "value": "Hello from Pico Commander!"},
{"action": "key", "combo": "enter"}
]
}Add menu item:
"active_menu": [
{
"id": "test_item",
"label": "Test Hello",
"sequence": [
{"scenario": "test_hello", "name": "Run"}
]
}
]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",
"sequence": [
{"scenario": "docker_stop", "name": "Stop"},
{"scenario": "docker_start", "name": "Start"}
]
}{
"id": "servers",
"label": "Servers",
"submenu": [
{"id": "web", "label": "Web Server", "sequence": [...]}
]
}"scenarios": {
"docker_stop": [
{"action": "enter", "count": 3},
{"action": "wait", "ms": 200},
{"action": "type", "value": "docker-compose stop"},
{"action": "key", "combo": "enter"}
]
}| Action | Description | Parameters |
|---|---|---|
type |
Type text | value |
key |
Press key combo | combo (e.g., "ctrl+c") |
wait |
Delay | ms (milliseconds) |
enter |
Press Enter N times | count |
- 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 button during boot | 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
pico-comander/
├── code.py # Main loop & menu logic
├── boot.py # USB HID initialization
├── config.py # Config/state loader
├── config.json # Menu & scenario definitions
├── display.py # SSD1306 OLED driver
├── encoder.py # KY-040 rotary encoder
├── passive.py # Hall sensors & button handler
├── trigger_bus.py # Scenario execution engine
├── screensaver.py # Screensaver effects
├── editor.html # Web-based config editor
└── lib/ # CircuitPython libraries
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!



