This version of the code lets you plug a USB keyboard into the USB-A port on the Feather RP2040 USB Host. The Feather hosts the keyboard and sends its keystrokes over USB to your computer. The solenoids are triggered whenever you type on the attached keyboard.
There are two code options for this project. This page goes over using the Not A Typewriter as a USB Host.
Once you've finished setting up your Feather RP2040 USB Host with CircuitPython, you can access the code and necessary libraries by downloading the Project Bundle.
To do this, click on the Download Project Bundle button in the window below. It will download to your computer as a zipped folder.
# SPDX-FileCopyrightText: 2025 Liz Clark for Adafruit Industries
#
# SPDX-License-Identifier: MIT
import array
import time
import board
from adafruit_mcp230xx.mcp23017 import MCP23017
import usb
import adafruit_usb_host_descriptors
import usb_hid
from adafruit_hid.keyboard import Keyboard
from adafruit_hid.keycode import Keycode
# Typewriter configuration
KEYSTROKE_BELL_INTERVAL = 25 # Ring bell every 25 keystrokes
SOLENOID_STRIKE_TIME = 0.03 # Duration in seconds for solenoid activation (reduced)
SOLENOID_DELAY = 0.01 # Small delay between solenoid operations (reduced)
ENTER_KEY_CODE = 0x28 # HID code for Enter key
ESCAPE_KEY_CODE = 0x29 # HID code for Escape key
BACKSPACE_KEY_CODE = 0x2A # HID code for Backspace key
TAB_KEY_CODE = 0x2B # HID code for Tab key
bell_keys = {ENTER_KEY_CODE, ESCAPE_KEY_CODE, TAB_KEY_CODE}
# Set up USB HID keyboard
hid_keyboard = Keyboard(usb_hid.devices)
# HID to Keycode mapping dictionary
hid_to_keycode = {
0x04: Keycode.A,
0x05: Keycode.B,
0x06: Keycode.C,
0x07: Keycode.D,
0x08: Keycode.E,
0x09: Keycode.F,
0x0A: Keycode.G,
0x0B: Keycode.H,
0x0C: Keycode.I,
0x0D: Keycode.J,
0x0E: Keycode.K,
0x0F: Keycode.L,
0x10: Keycode.M,
0x11: Keycode.N,
0x12: Keycode.O,
0x13: Keycode.P,
0x14: Keycode.Q,
0x15: Keycode.R,
0x16: Keycode.S,
0x17: Keycode.T,
0x18: Keycode.U,
0x19: Keycode.V,
0x1A: Keycode.W,
0x1B: Keycode.X,
0x1C: Keycode.Y,
0x1D: Keycode.Z,
0x1E: Keycode.ONE,
0x1F: Keycode.TWO,
0x20: Keycode.THREE,
0x21: Keycode.FOUR,
0x22: Keycode.FIVE,
0x23: Keycode.SIX,
0x24: Keycode.SEVEN,
0x25: Keycode.EIGHT,
0x26: Keycode.NINE,
0x27: Keycode.ZERO,
0x28: Keycode.ENTER,
0x29: Keycode.ESCAPE,
0x2A: Keycode.BACKSPACE,
0x2B: Keycode.TAB,
0x2C: Keycode.SPACE,
0x2D: Keycode.MINUS,
0x2E: Keycode.EQUALS,
0x2F: Keycode.LEFT_BRACKET,
0x30: Keycode.RIGHT_BRACKET,
0x31: Keycode.BACKSLASH,
0x33: Keycode.SEMICOLON,
0x34: Keycode.QUOTE,
0x35: Keycode.GRAVE_ACCENT,
0x36: Keycode.COMMA,
0x37: Keycode.PERIOD,
0x38: Keycode.FORWARD_SLASH,
0x39: Keycode.CAPS_LOCK,
0x3A: Keycode.F1,
0x3B: Keycode.F2,
0x3C: Keycode.F3,
0x3D: Keycode.F4,
0x3E: Keycode.F5,
0x3F: Keycode.F6,
0x40: Keycode.F7,
0x41: Keycode.F8,
0x42: Keycode.F9,
0x43: Keycode.F10,
0x44: Keycode.F11,
0x45: Keycode.F12,
0x4F: Keycode.RIGHT_ARROW,
0x50: Keycode.LEFT_ARROW,
0x51: Keycode.DOWN_ARROW,
0x52: Keycode.UP_ARROW,
}
# Modifier mapping
modifier_to_keycode = {
0x01: Keycode.LEFT_CONTROL,
0x02: Keycode.LEFT_SHIFT,
0x04: Keycode.LEFT_ALT,
0x08: Keycode.LEFT_GUI,
0x10: Keycode.RIGHT_CONTROL,
0x20: Keycode.RIGHT_SHIFT,
0x40: Keycode.RIGHT_ALT,
0x80: Keycode.RIGHT_GUI,
}
#interface index, and endpoint addresses for USB Device instance
kbd_interface_index = None
kbd_endpoint_address = None
keyboard = None
i2c = board.STEMMA_I2C()
mcp = MCP23017(i2c)
noid_2 = mcp.get_pin(0) # Key strike solenoid
noid_1 = mcp.get_pin(1) # Bell solenoid
noid_1.switch_to_output(value=False)
noid_2.switch_to_output(value=False)
# Typewriter state tracking
keystroke_count = 0
previous_keys = set() # Track previously pressed keys to detect new presses
previous_modifiers = 0 # Track modifier state
#interface index, and endpoint addresses for USB Device instance
kbd_interface_index = None
kbd_endpoint_address = None
keyboard = None
# scan for connected USB devices
for device in usb.core.find(find_all=True):
# check for boot keyboard endpoints on this device
kbd_interface_index, kbd_endpoint_address = (
adafruit_usb_host_descriptors.find_boot_keyboard_endpoint(device)
)
# if a boot keyboard interface index and endpoint address were found
if kbd_interface_index is not None and kbd_interface_index is not None:
keyboard = device
# detach device from kernel if needed
if keyboard.is_kernel_driver_active(0):
keyboard.detach_kernel_driver(0)
# set the configuration so it can be used
keyboard.set_configuration()
if keyboard is None:
raise RuntimeError("No boot keyboard endpoint found")
buf = array.array("b", [0] * 8)
def strike_key_solenoid():
"""Activate the key strike solenoid briefly"""
noid_1.value = True
time.sleep(SOLENOID_STRIKE_TIME)
noid_1.value = False
def ring_bell_solenoid():
"""Activate the bell solenoid briefly"""
noid_2.value = True
time.sleep(SOLENOID_STRIKE_TIME)
noid_2.value = False
def get_pressed_keys(report_data):
"""Extract currently pressed keys from HID report"""
pressed_keys = set()
# Check bytes 2-7 for key codes (up to 6 simultaneous keys)
for i in range(2, 8):
k = report_data[i]
# Skip if no key (0) or error rollover (1)
if k > 1:
pressed_keys.add(k)
return pressed_keys
def print_keyboard_report(report_data):
# Dictionary for modifier keys (first byte)
modifier_dict = {
0x01: "LEFT_CTRL",
0x02: "LEFT_SHIFT",
0x04: "LEFT_ALT",
0x08: "LEFT_GUI",
0x10: "RIGHT_CTRL",
0x20: "RIGHT_SHIFT",
0x40: "RIGHT_ALT",
0x80: "RIGHT_GUI",
}
# Dictionary for key codes (main keys)
key_dict = {
0x04: "A",
0x05: "B",
0x06: "C",
0x07: "D",
0x08: "E",
0x09: "F",
0x0A: "G",
0x0B: "H",
0x0C: "I",
0x0D: "J",
0x0E: "K",
0x0F: "L",
0x10: "M",
0x11: "N",
0x12: "O",
0x13: "P",
0x14: "Q",
0x15: "R",
0x16: "S",
0x17: "T",
0x18: "U",
0x19: "V",
0x1A: "W",
0x1B: "X",
0x1C: "Y",
0x1D: "Z",
0x1E: "1",
0x1F: "2",
0x20: "3",
0x21: "4",
0x22: "5",
0x23: "6",
0x24: "7",
0x25: "8",
0x26: "9",
0x27: "0",
0x28: "ENTER",
0x29: "ESC",
0x2A: "BACKSPACE",
0x2B: "TAB",
0x2C: "SPACE",
0x2D: "MINUS",
0x2E: "EQUAL",
0x2F: "LBRACKET",
0x30: "RBRACKET",
0x31: "BACKSLASH",
0x33: "SEMICOLON",
0x34: "QUOTE",
0x35: "GRAVE",
0x36: "COMMA",
0x37: "PERIOD",
0x38: "SLASH",
0x39: "CAPS_LOCK",
0x4F: "RIGHT_ARROW",
0x50: "LEFT_ARROW",
0x51: "DOWN_ARROW",
0x52: "UP_ARROW",
}
# Add F1-F12 keys to the dictionary
for i in range(12):
key_dict[0x3A + i] = f"F{i + 1}"
# First byte contains modifier keys
modifiers = report_data[0]
# Print modifier keys if pressed
if modifiers > 0:
print("Modifiers:", end=" ")
# Check each bit for modifiers and print if pressed
for b, name in modifier_dict.items():
if modifiers & b:
print(name, end=" ")
print()
# Bytes 2-7 contain up to 6 key codes (byte 1 is reserved)
keys_pressed = False
for i in range(2, 8):
k = report_data[i]
# Skip if no key or error rollover
if k in {0, 1}:
continue
if not keys_pressed:
print("Keys:", end=" ")
keys_pressed = True
# Print key name based on dictionary lookup
if k in key_dict:
print(key_dict[k], end=" ")
else:
# For keys not in the dictionary, print the HID code
print(f"0x{k:02X}", end=" ")
if keys_pressed:
print()
elif modifiers == 0:
print("No keys pressed")
print("USB Typewriter starting...")
print(f"Bell will ring every {KEYSTROKE_BELL_INTERVAL} keystrokes or when Enter is pressed")
while True:
# try to read data from the keyboard
try:
count = keyboard.read(kbd_endpoint_address, buf, timeout=10)
# if there is no data it will raise USBTimeoutError
except usb.core.USBTimeoutError:
# Nothing to do if there is no data for this keyboard
continue
# Get currently pressed keys and modifiers
current_keys = get_pressed_keys(buf)
current_modifiers = buf[0]
# Find newly pressed keys (not in previous scan)
new_keys = current_keys - previous_keys
# Find released keys for HID pass-through
released_keys = previous_keys - current_keys
# Handle modifier changes
if current_modifiers != previous_modifiers:
# Build list of modifier keycodes to press/release
for bit, keycode in modifier_to_keycode.items():
if current_modifiers & bit and not previous_modifiers & bit:
# Modifier newly pressed
hid_keyboard.press(keycode)
elif not (current_modifiers & bit) and (previous_modifiers & bit):
# Modifier released
hid_keyboard.release(keycode)
# Release any keys that were let go
for key in released_keys:
if key in hid_to_keycode:
hid_keyboard.release(hid_to_keycode[key])
# Process each newly pressed key
for key in new_keys:
# Increment keystroke counter
keystroke_count += 1
# Strike the key solenoid for typewriter effect
strike_key_solenoid()
# Pass through the key press via USB HID
if key in hid_to_keycode:
hid_keyboard.press(hid_to_keycode[key])
# Check if special keys were pressed
if key == ENTER_KEY_CODE:
ring_bell_solenoid()
keystroke_count = 0 # Reset counter for new line
elif key == ESCAPE_KEY_CODE:
ring_bell_solenoid()
keystroke_count = 0 # Reset counter
elif key == TAB_KEY_CODE:
ring_bell_solenoid()
keystroke_count = 0 # Reset counter
elif key == BACKSPACE_KEY_CODE:
keystroke_count = 0 # Reset counter but no bell
elif keystroke_count % KEYSTROKE_BELL_INTERVAL == 0:
print(f"\n*** DING! ({keystroke_count} keystrokes) ***\n")
ring_bell_solenoid()
# Special handling for bell keys that are still held
# check if they were released and re-pressed
# This handles rapid double-taps where the key might not fully release
for key in bell_keys:
if key in current_keys and key in previous_keys and key not in new_keys:
# Key is being held, check if it was briefly released by looking at the raw state
# For held keys, we'll check if this is a repeat event
if len(current_keys) != len(previous_keys) or current_keys != previous_keys:
# Something changed, might be a repeat
continue
# Update previous keys and modifiers for next scan
previous_keys = current_keys
previous_modifiers = current_modifiers
# Still print the keyboard report for debugging
if new_keys: # Only print if there are new key presses
print_keyboard_report(buf)
print(f"Total keystrokes: {keystroke_count}")
Upload the Code and Libraries to the Feather RP2040 USB Host
After downloading the Project Bundle, plug your Feather RP2040 USB Host into the computer's USB port with a known good USB data+power cable. You should see a new flash drive appear in the computer's File Explorer or Finder (depending on your operating system) called CIRCUITPY. Unzip the folder and copy the following items to the Feather RP2040 USB Host's CIRCUITPY drive.
- lib folder
- code.py
Your Feather RP2040 USB Host CIRCUITPY drive should look like this after copying the lib folder and code.py file:
Page last edited June 17, 2025
Text editor powered by tinymce.