DIY USB Keyboard with Raspberry Pi Pico for Isadora 4 - Complete Tutorial
Build a custom 20-key USB keyboard using a Raspberry Pi Pico and CircuitPython - perfect for triggering scenes, cues, and effects in Isadora!
🎯
What This Project Does
This tutorial shows you how to transform a $4 Raspberry Pi Pico into a fully functional USB keyboard with 20 customizable keys. The keyboard sends the first 20 letters of the alphabet (a-t) and works seamlessly with Isadora 4 for triggering scenes, controlling parameters, or any other keyboard shortcuts you need.
Key Features:
✅ 20 programmable keys (easily expandable)
✅ Plug-and-play USB HID device (no drivers needed)
✅ Visual feedback with onboard LED
✅ Robust error handling and debugging
✅ Works with any computer (Windows, Mac, Linux)
✅ Perfect for live performance setups
🛠
Hardware Requirements
Raspberry Pi Pico (any variant: Pico, Pico W, Pico 2) - ~$4
20x Push buttons/momentary switches - any normally-open type
Breadboard or perfboard for connections
Jumper wires
USB cable (data-capable, not charge-only!)
Total cost: Under $15!
🔌
Wiring Diagram
The wiring is incredibly simple - no resistors needed!
GP0 ←→ Button ←→ GND (sends 'a')
GP1 ←→ Button ←→ GND (sends 'b')
GP2 ←→ Button ←→ GND (sends 'c')
...
GP19 ←→ Button ←→ GND (sends 't')
Important: Each button connects between a GPIO pin and any GND pin. The Pico's internal pull-up resistors handle the rest!
📥
Software Installation
Step 1: Install CircuitPython
Download CircuitPython firmware:
For
Pico:
https://circuitpython.org/boar...
For
Pico W:
https://circuitpython.org/boar...
For
Pico 2:
https://circuitpython.org/boar...
Install firmware:
Hold BOOTSEL button on Pico
Connect USB cable (keep BOOTSEL pressed)
Release BOOTSEL → "RPI-RP2" drive appears
Drag the downloaded .UF2 file to RPI-RP2
Drive disappears and "CIRCUITPY" appears
Step 2: Install Required Library
Download the library bundle:
Go to:
https://circuitpython.org/libr...
Download the bundle matching your CircuitPython version
Install adafruit_hid:
Extract the downloaded ZIP file
Copy the adafruit_hid folder from the bundle's lib directory
Paste it into the lib folder on your CIRCUITPY drive
Your CIRCUITPY drive should look like:
CIRCUITPY/
├── code.py
└── lib/ └── adafruit_hid/ ├── __init__.py ├── keyboard.py ├── keycode.py └── ...
💻
The Complete Code
Save this as code.py on your CIRCUITPY drive:
"""
Raspberry Pi Pico USB Keyboard Emulator for Isadora 4
======================================================
FUNCTION:
---------
This program transforms the Raspberry Pi Pico into a USB keyboard with 20 keys.
The keys correspond to the first 20 lowercase letters of the alphabet (a-t).
The onboard LED lights up briefly with each key press for visual feedback.
HARDWARE WIRING:
---------------
Each button is connected between a GPIO pin and GND (Ground):
GP0 <-> Button <-> GND (sends 'a')
GP1 <-> Button <-> GND (sends 'b')
GP2 <-> Button <-> GND (sends 'c')
...
GP19 <-> Button <-> GND (sends 't')
IMPORTANT NOTES:
---------------
- No external pull-up resistors needed (internal pull-ups are enabled)
- Use simple momentary switches/buttons between pin and GND
- Onboard LED shows every key press
- Debouncing is already implemented
- Only works with CircuitPython (not standard MicroPython)
INSTALLATION:
------------
1. Install CircuitPython on Pi Pico
2. Copy adafruit_hid library to lib/ folder
3. Save this code as code.py
AUTHOR: Pi Pico Keyboard Project for Isadora
VERSION: 1.1 (with LED feedback and robust error handling)
"""
import board
import digitalio
import usb_hid
from adafruit_hid.keyboard import Keyboard
from adafruit_hid.keycode import Keycode
import time
# Error handling for USB HID initialization
try: kbd = Keyboard(usb_hid.devices) print("✓ USB HID Keyboard successfully initialized")
except Exception as e: print(f"✗ ERROR initializing USB keyboard: {e}") print("Make sure CircuitPython is properly installed!") raise
# Initialize onboard LED
try: led = digitalio.DigitalInOut(board.LED) led.direction = digitalio.Direction.OUTPUT led.value = False # LED off print("✓ Onboard LED initialized")
except Exception as e: print(f"✗ ERROR initializing LED: {e}") led = None # Disable LED if error occurs
# GPIO pins for the 20 buttons and their states
button_pins = []
button_states = []
button_last_states = []
# First 20 lowercase letters of the alphabet (a-t)
keycodes = [ Keycode.A, Keycode.B, Keycode.C, Keycode.D, Keycode.E, Keycode.F, Keycode.G, Keycode.H, Keycode.I, Keycode.J, Keycode.K, Keycode.L, Keycode.M, Keycode.N, Keycode.O, Keycode.P, Keycode.Q, Keycode.R, Keycode.S, Keycode.T
]
# Configure GPIO pins (GP0-GP19)
print("Initializing GPIO pins...")
pin_numbers = list(range(20))
failed_pins = []
for i, pin_num in enumerate(pin_numbers): try: # Configure pin as input with pull-up resistor pin = digitalio.DigitalInOut(getattr(board, f'GP{pin_num}')) pin.direction = digitalio.Direction.INPUT pin.pull = digitalio.Pull.UP button_pins.append(pin) button_states.append(False) button_last_states.append(True) # Pull-up: not pressed = True except Exception as e: print(f"✗ ERROR with pin GP{pin_num}: {e}") failed_pins.append(pin_num) # Placeholder for failed pins button_pins.append(None) button_states.append(False) button_last_states.append(True)
if failed_pins: print(f"⚠ Warning: Pins {failed_pins} could not be initialized")
else: print("✓ All 20 GPIO pins successfully configured")
# LED feedback configuration
LED_ON_TIME = 0.1 # LED lights up for 100ms on key press
led_off_time = 0
print("\n" + "="*50)
print("🎹 USB KEYBOARD READY!")
print("="*50)
print("Key mapping:")
for i in range(20): char = chr(ord('a') + i) if i < len(failed_pins) and pin_numbers[i] in failed_pins: print(f" GP{i:2d} -> '{char}' (ERROR)") else: print(f" GP{i:2d} -> '{char}'")
print("\nConnect buttons between GPx and GND")
print("Onboard LED shows each key press")
print("="*50)
# Main loop with robust error handling
try: while True: current_time = time.monotonic() # Automatically turn off LED after defined time if led and led.value and current_time >= led_off_time: led.value = False # Scan all available buttons for i in range(20): # Skip pin if initialization failed if button_pins[i] is None: continue try: # Read current pin state (Pull-up: LOW = pressed) current_state = not button_pins[i].value last_state = button_states[i] # Button was pressed (edge from LOW to HIGH) if current_state and not last_state: char = chr(ord('a') + i) # Send key press kbd.send(keycodes[i]) print(f"🔤 Key '{char}' pressed (GP{i})") # Turn on LED with timer if led: led.value = True led_off_time = current_time + LED_ON_TIME button_states[i] = True # Button was released (edge from HIGH to LOW) elif not current_state and last_state: char = chr(ord('a') + i) print(f" Key '{char}' released") button_states[i] = False except Exception as e: print(f"✗ ERROR reading GP{i}: {e}") # Mark pin as faulty button_pins[i] = None # Short pause for debouncing and CPU relief time.sleep(0.005) # 5ms - good balance between responsiveness and debouncing
except KeyboardInterrupt: print("\n🛑 Program terminated by user") if led: led.value = False except Exception as e: print(f"\n💥 UNEXPECTED ERROR: {e}") print("Restart required...") if led: led.value = False raise finally: # Cleanup: turn off LED if led: led.value = False print("🔧 Cleanup completed")
🎭
Using with Isadora 4
Once connected, your Pico keyboard works like any standard keyboard:
Scene Triggers: Assign keys a-t to trigger different scenes
Parameter Control: Use keyboard shortcuts to control actor parameters
Cue Sequences: Create complex cue sequences triggered by your custom keys
Live Performance: Perfect for hands-on control during performances
Tip: You can easily modify the keycodes array in the code to send different keys, function keys, or even key combinations!