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'slib
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!

These User Actors might be useful to you:
heartbeat-generator-v2.iua4
hold-range-and-scale.iua4
Here's our tutorial on User Actors as well:Ā https://support.troikatronix.com/support/solutions/articles/13000091626-isadora-101-tutorial-11-creating-user-actors
@dbini Thank you so much. IĀ will give this all a try. IĀ appreciate the quick response.

Does the MIDI stream show up in Isadora's MIDI Setup window? Does it appear as MIDI CC (a controller value based on the BPM of your heart)Ā or MIDI notes (a momentary key press every heartbeat)? In MIDI setup, select your heartrate monitor as input and Isadora Virtual Out as output.
I'm going to assume that you are getting MIDI CC into Isadora. The workflow of MIDI CC - Isadora - Ableton is the way that I would do it, so that I have control over the scaling and smoothing of the data.Ā
Use a Control Watcher actor to receive the data from your monitor. Add a Limit/Scale Value actor so that you can set the minimum heart rate and maximum heart rate, also the minimum and maximum volume. Add a Control Send actor to send the scaled MIDI to Ableton on a particular controller number. Remember to connect the trigger input as well.
In Ableton prefs, select Isadora Virtual Out as a MIDI controller and you should now be able to map your MIDI CC from Isadora to the volume slider of your soundtrack.
@georgie_done Here is a number guessing game I made for a friend while learning python from "Automate the Boring Stuff with Python"Ā tv.pngĀ number-guesser.izzĀ
Its simple and silly, and I don't have an active license so I cant edit it.
Greetings, everyone!
Iām new to the forum and still finding my footing with both Isadora and Ableton Live, so I appreciate your patience.
Iāve been reading through several posts but havenāt quite found a solution to the issue Iām working through. Iām currently wearing a MIDI-enabled heart rate monitor and would like to use Isadora to dynamically control the volume of a corresponding MIDI track based on my heart rate. The idea is simple: as my heart rate increases, the volume of the sound increases, and as it decreases, the volume fades accordingly.
Has anyone here tried something similar? Iād love any guidance on how to map the heart rate data to volume levels within Isadora, or if thereās a better way to route this data through Ableton or another method.
Thanks in advance!
Re: [[ANSWERED] Ableton Live > Isadora](/topic/8552/answered-ableton-live-isadora)

@dillthekraut said:
needing to change all the numbers in the addresses manually.
Ā Yes, this has been driving me nuts as well.

@dubbindavidson said:
DoesĀ OSCĀ seem appropriate forĀ controllingĀ ArduinosĀ fromĀ Isadora?
Ā If you have done that before, then sure. OSC allows you more distance between the PC and Arduino. If you used USB/serial you would be limited by the USB cable length.Ā
@dusx ThankĀ you! WeĀ willĀ continueĀ downĀ the ArduinoĀ pathĀ andĀ thenĀ comeĀ backĀ to Isadora.Ā In our previous installations, OSC data from a Unity simulation is controlling anĀ Arduino (relays/airĀ solenoids> softĀ robotĀ manta). DoesĀ OSCĀ seem appropriate forĀ controllingĀ ArduinosĀ fromĀ Isadora?