MAJOR BREAKTHROUGH: Discovered correct deployment method for CircuitPython Critical Discovery: - BOOTSEL mode (RPI-RP2) is ONLY for flashing .UF2 firmware files - Python files MUST be copied to CIRCUITPY drive, not RPI-RP2 - Files copied to RPI-RP2 appear to succeed but don't persist to runtime Deployment Success: - Pico running CircuitPython 9.2.1 - HID Keyboard mode with F13-F17 support confirmed - Buttons 4 and 5 tested successfully (F16, F17) - boot.py: readonly=True (enables auto-execution) - code.py: 70 lines, no _pin bug, proper debouncing Files Added: - DEPLOYMENT_GUIDE.md: Comprehensive guide with best practices from Adafruit docs - deploy_pico_robust.sh: Automated deployment script with verification - DEPLOYMENT_SUCCESS.md: Complete troubleshooting report and lessons learned Button Mappings: - Button 1 (GP28) → F13 → Volume Up - Button 2 (GP27) → F14 → Volume Down - Button 3 (GP22) → F15 → Next Tab - Button 4 (GP21) → F16 → Toggle Display ✓ TESTED - Button 5 (GP19) → F17 → Keyboard Toggle ✓ TESTED Next Steps: - Test buttons 1-3 for F13-F15 events - Update baobab-button-handler on tembo to catch F13-F17 keys - Configure actions for each button Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> |
||
|---|---|---|
| pico_firmware | ||
| testKioskHID | ||
| .claude_session_notes.md | ||
| .gitignore | ||
| code.py | ||
| code_multibutton_test.py | ||
| deploy_pico_robust.sh | ||
| deploy_to_pico.py | ||
| DEPLOYMENT.md | ||
| DEPLOYMENT_GUIDE.md | ||
| DEPLOYMENT_SUCCESS.md | ||
| diagnose_pico.py | ||
| HARDWARE.md | ||
| INTEGRATION_TESTING.md | ||
| MANUAL_INSTALL.md | ||
| QUICKSTART.md | ||
| README.md | ||
| SERVER_HANDOFF.md | ||
| TEMBO_INTEGRATION.md | ||
| test_deployment.py | ||
| TESTING.md | ||
| TROUBLESHOOTING_NOTES.md | ||
| wiring-diagram.md | ||
kioskHID - Raspberry Pi Pico HID Keyboard Controller
5-button USB HID keyboard controller for kiosk applications using F13-F17 function keys.
Overview
This project implements a simple, reliable HID keyboard device using a Raspberry Pi Pico and 5 tactile buttons. The Pico presents itself as a standard USB keyboard and sends F13-F17 key presses when buttons are pressed.
Why F13-F17?
- F13-F17 are rarely bound by applications (unlike F1-F12)
- Gives you complete control over custom key bindings
- Simpler than Consumer Control codes - direct key press mapping
- Works with any key binding system (xbindkeys, systemd, custom daemons)
Hardware
Components
- Raspberry Pi Pico (RP2040)
- 5x NINIGI TACT-64K-F tactile switches (6x6mm, SPST-NO)
- Breadboard and jumper wires
- USB cable (micro-USB to USB-A)
Wiring
Button 1 (Volume Up) : GP28 ──┬─ [Button] ─┬── GND
Button 2 (Volume Down) : GP27 ──┤ │
Button 3 (Next Tab) : GP22 ──┤ │
Button 4 (Display Toggle) : GP21 ──┤ │
Button 5 (Keyboard Toggle): GP19 ──┴────────────┘
All buttons use internal pull-up resistors (no external resistors needed).
Button Mappings
| Button | GPIO | Function Key | Intended Action | Linux Event Code |
|---|---|---|---|---|
| 1 | GP28 | F13 | Volume Up | KEY_F13 (183) |
| 2 | GP27 | F14 | Volume Down | KEY_F14 (184) |
| 3 | GP22 | F15 | Next Browser Tab | KEY_F15 (185) |
| 4 | GP21 | F16 | Toggle Display On/Off | KEY_F16 (186) |
| 5 | GP19 | F17 | Toggle On-Screen Keyboard | KEY_F17 (187) |
Firmware Architecture
Boot Modes
The firmware supports two boot modes for different use cases:
Production Mode (boot_production.py)
- Filesystem: Read-only (
readonly=True) - Auto-execution: ✅ YES - code.py runs automatically on boot
- Use case: Deployed/production use
- Updates: Must switch to development mode first
Development Mode (boot_development.py)
- Filesystem: Writable (
readonly=False) - Auto-execution: ❌ NO - boots to REPL, manual execution required
- Use case: Development and testing
- Updates: Can copy files directly via USB mass storage
Important: CircuitPython only auto-runs code.py when the filesystem is read-only for security reasons.
Code Structure
pico_firmware/
├── boot_production.py # Production boot config (readonly=True)
├── boot_development.py # Development boot config (readonly=False)
├── code.py # Main firmware (Keyboard HID with F13-F17)
├── adafruit_hid/ # HID library (precompiled .mpy modules)
│ ├── keyboard.mpy
│ ├── keycode.mpy
│ └── ...
Deployment
Quick Deploy (Production Mode)
# 1. Find the Pico device
lsblk | grep CIRCUITPY
# 2. Mount the Pico
sudo mount /dev/sdb1 /mnt/circuitpy # Adjust device as needed
# 3. Copy files
sudo cp pico_firmware/boot_production.py /mnt/circuitpy/boot.py
sudo cp pico_firmware/code.py /mnt/circuitpy/code.py
sudo cp -r pico_firmware/adafruit_hid /mnt/circuitpy/lib/
# 4. Sync and unmount
sync
sudo umount /mnt/circuitpy
# 5. Unplug and replug the Pico
# The code should now run automatically!
Verify Deployment
# Check serial output for startup banner
timeout 3 cat /dev/ttyACM0
# Expected output:
# ============================================================
# KIOSK HID KEYBOARD CONTROLLER - READY
# ============================================================
# Button Configuration:
# Button 1 (GP28) → F13 → Volume Up
# Button 2 (GP27) → F14 → Volume Down
# ...
Testing HID Events
# Find the Pico keyboard device
ls -l /dev/input/by-id/ | grep -i Pico
# Monitor key events with evtest
sudo evtest /dev/input/eventX
# Press buttons and verify F13-F17 events appear
Integration with tembo
On the computer side (tembo), you'll need to configure a key binding daemon or systemd service to catch F13-F17 events and execute the appropriate actions.
Example Event Mapping
# Pseudo-config for button handler on tembo
key_bindings:
F13: # Button 1
command: "pactl set-sink-volume @DEFAULT_SINK@ +5%"
description: "Volume Up"
F14: # Button 2
command: "pactl set-sink-volume @DEFAULT_SINK@ -5%"
description: "Volume Down"
F15: # Button 3
command: "/usr/local/bin/chrome-next-tab.sh"
description: "Next Browser Tab"
F16: # Button 4
command: "/usr/local/bin/toggle-display.sh"
description: "Toggle Display On/Off"
F17: # Button 5
command: "/usr/local/bin/toggle-onscreen-keyboard.sh"
description: "Toggle On-Screen Keyboard"
Python Event Handler Example
import evdev
# Find Pico keyboard device
device = None
for path in evdev.list_devices():
dev = evdev.InputDevice(path)
if 'Pico' in dev.name and 'Keyboard' in dev.name:
device = dev
break
# Key mappings (F13-F17 event codes)
KEY_ACTIONS = {
183: lambda: os.system("pactl set-sink-volume @DEFAULT_SINK@ +5%"), # F13
184: lambda: os.system("pactl set-sink-volume @DEFAULT_SINK@ -5%"), # F14
185: lambda: os.system("/usr/local/bin/chrome-next-tab.sh"), # F15
186: lambda: os.system("/usr/local/bin/toggle-display.sh"), # F16
187: lambda: os.system("/usr/local/bin/toggle-onscreen-keyboard.sh"), # F17
}
# Event loop
for event in device.read_loop():
if event.type == evdev.ecodes.EV_KEY and event.value == 1: # Key press
action = KEY_ACTIONS.get(event.code)
if action:
action()
Testing
Layer 1: Hardware Test (Standalone)
Watch the onboard LED:
- Startup: 3 quick blinks (ready)
- Button press: LED turns on
- Button release: LED turns off
Connect to serial console:
screen /dev/ttyACM0 115200
Press each button and verify serial output shows correct F-key.
Layer 2: HID Events (Linux Kernel)
Test with evtest (no daemon needed):
sudo evtest /dev/input/eventX
Verify each button generates correct event:
- Button 1 → KEY_F13 (183)
- Button 2 → KEY_F14 (184)
- Button 3 → KEY_F15 (185)
- Button 4 → KEY_F16 (186)
- Button 5 → KEY_F17 (187)
Layer 3: Full Integration
Test with your button handler daemon/service on tembo to verify actions execute correctly.
Troubleshooting
Code doesn't auto-run after plugging in Pico
Cause: Likely using development boot.py (readonly=False)
Solution:
# Deploy production boot.py
sudo mount /dev/sdb1 /mnt/circuitpy
sudo cp pico_firmware/boot_production.py /mnt/circuitpy/boot.py
sync
sudo umount /mnt/circuitpy
# Unplug and replug Pico
No events appear in evtest
Possible causes:
- Code not running (check serial output)
- Wrong event device selected
- Permissions issue
Debugging:
# Check if Pico keyboard is detected
lsusb | grep -i "Raspberry Pi Pico"
# Find correct event device
ls -l /dev/input/by-id/ | grep -i Pico
# Check serial output
timeout 3 cat /dev/ttyACM0
Buttons don't respond
Check hardware:
- Verify wiring (GP28, GP27, GP22, GP21, GP19 to one side of buttons, GND to other)
- Check for loose connections
- Verify buttons work (multimeter continuity test)
Need to update firmware but can't modify files
Cause: Production mode has readonly filesystem
Solution: Temporarily switch to development mode:
sudo mount /dev/sdb1 /mnt/circuitpy
sudo cp pico_firmware/boot_development.py /mnt/circuitpy/boot.py
sync
sudo umount /mnt/circuitpy
# Now you can modify files
# After changes, switch back to production mode
Development
Making Changes
- Switch to development mode
- Make changes to code.py
- Copy to Pico
- Test via REPL or serial
- Switch back to production mode
Adding More Buttons
Edit code.py and add to the BUTTONS list:
BUTTONS = [
(board.GP28, Keycode.F13, "Volume Up"),
(board.GP27, Keycode.F14, "Volume Down"),
(board.GP22, Keycode.F15, "Next Tab"),
(board.GP21, Keycode.F16, "Toggle Display"),
(board.GP19, Keycode.F17, "Keyboard Toggle"),
(board.GP20, Keycode.F18, "New Function"), # New button
]
Available F-keys: F13-F24 (Keycode.F13 through Keycode.F24)
Technical Details
Debouncing
Software debouncing with 20ms delay:
- Detect button press (HIGH → LOW transition)
- Wait 20ms
- Verify still pressed
- Send HID key press/release
- Wait for button release before next detection
HID Protocol
- Device Type: USB HID Keyboard
- Keys Used: F13-F17 (function keys 13-17)
- Linux Keycodes: 183-187
- Press/Release: Both press and immediate release sent (simulates key tap)
Serial Console
Debug output available on /dev/ttyACM0 at 115200 baud:
- Startup banner
- Button press/release logs
- F-key sent confirmation
Repository
Git repository: ssh://git@git.baobab.band:7577/sjat/kioskHID.git
License
MIT License - Free for personal and commercial use
Credits
- CircuitPython by Adafruit Industries
- adafruit_hid library