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 DoesThis 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 DiagramThe 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 CircuitPythonDownload 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 LibraryDownload the library bundle:
Go to: https://circuitpython.org/libr... Download the bundle matching your CircuitPython versionInstall 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 driveYour CIRCUITPY drive should look like:
CIRCUITPY/ āāā code.py āāā lib/ āāā adafruit_hid/ āāā __init__.py āāā keyboard.py āāā keycode.py āāā ... š» The Complete CodeSave 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 4Once 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 performancesTip: You can easily modify the keycodes array in the code to send different keys, function keys, or even key combinations!