This version of the code lets you plug in the Not A Typewriter to your computer without having to plug your keyboard into the Feather USB host port. A CPython script runs on your computer sending your keyboard inputs via serial to the attached Feather. The Feather runs CircuitPython code that is listening for those key presses. When a key press is received, the solenoids are triggered.
There are two code options for this project. This page goes over using the Not A Typewriter as a keyboard listener.
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 """ USB Typewriter Feather-side Script Converts incoming keystrokes to solenoid clicks """ import time import struct import usb_cdc import board from adafruit_mcp230xx.mcp23017 import MCP23017 # Typewriter configuration KEYSTROKE_BELL_INTERVAL = 25 # Ring bell every 25 keystrokes SOLENOID_STRIKE_TIME = 0.03 # Duration in seconds for solenoid activation 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 # Key name mapping for debug output key_names = { 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", 0x50: "LEFT", 0x51: "DOWN", 0x52: "UP", } # Add F1-F12 keys for i in range(12): key_names[0x3A + i] = f"F{i + 1}" # Set up I2C and MCP23017 i2c = board.STEMMA_I2C() mcp = MCP23017(i2c) # Configure solenoid pins noid_1 = mcp.get_pin(0) # Bell solenoid noid_2 = mcp.get_pin(1) # Key strike solenoid noid_1.switch_to_output(value=False) noid_2.switch_to_output(value=False) # Typewriter state tracking keystroke_count = 0 current_keys = set() # Track currently pressed keys # Check if USB CDC data is available if usb_cdc.data is None: print("ERROR: USB CDC data not enabled!") print("Please create a boot.py file with:") print(" import usb_cdc") print(" usb_cdc.enable(console=True, data=True)") print("\nThen reset the board.") while True: time.sleep(1) serial = usb_cdc.data def strike_key_solenoid(): """Activate the key strike solenoid briefly""" noid_2.value = True time.sleep(SOLENOID_STRIKE_TIME) noid_2.value = False def ring_bell_solenoid(): """Activate the bell solenoid briefly""" noid_1.value = True time.sleep(SOLENOID_STRIKE_TIME) noid_1.value = False def process_key_event(mod, code, p): # pylint: disable=too-many-branches """Process a key event from the computer""" global keystroke_count # pylint: disable=global-statement # Debug output key_name = key_names.get(code, f"0x{code:02X}") action = "pressed" if p else "released" # Handle modifier display if mod > 0: mod_str = [] if mod & 0x01: mod_str.append("L_CTRL") if mod & 0x02: mod_str.append("L_SHIFT") if mod & 0x04: mod_str.append("L_ALT") if mod & 0x08: mod_str.append("L_GUI") if mod & 0x10: mod_str.append("R_CTRL") if mod & 0x20: mod_str.append("R_SHIFT") if mod & 0x40: mod_str.append("R_ALT") if mod & 0x80: mod_str.append("R_GUI") print(f"[{'+'.join(mod_str)}] {key_name} {action}") else: print(f"{key_name} {action}") # Only process key presses (not releases) for solenoid activation if p and code > 0: # key_code 0 means modifier-only update # Check if this is a new key press if code not in current_keys: current_keys.add(code) # Increment keystroke counter keystroke_count += 1 # Strike the key solenoid strike_key_solenoid() # Check for special keys if code == ENTER_KEY_CODE: ring_bell_solenoid() keystroke_count = 0 # Reset counter for new line elif code == ESCAPE_KEY_CODE: ring_bell_solenoid() keystroke_count = 0 # Reset counter elif code == TAB_KEY_CODE: ring_bell_solenoid() keystroke_count = 0 # Reset counter elif code == 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() print(f"Total keystrokes: {keystroke_count}") elif not p and code > 0: # Remove key from pressed set when released current_keys.discard(code) print("USB Typewriter Receiver starting...") print(f"Bell will ring every {KEYSTROKE_BELL_INTERVAL} keystrokes or on special keys") print("Waiting for key events from computer...") print("-" * 40) # Buffer for incoming data buffer = bytearray(4) buffer_pos = 0 while True: # Check for incoming serial data if serial.in_waiting > 0: # Read available bytes data = serial.read(serial.in_waiting) for byte in data: # Look for start marker if buffer_pos == 0: if byte == 0xAA: buffer[0] = byte buffer_pos = 1 else: # Fill buffer buffer[buffer_pos] = byte buffer_pos += 1 # Process complete message if buffer_pos >= 4: # Unpack the message _, modifier, key_code, pressed = struct.unpack('BBBB', buffer) # Process the key event process_key_event(modifier, key_code, pressed) # Reset buffer buffer_pos = 0 # Small delay to prevent busy-waiting time.sleep(0.001)
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
- boot.py
Your Feather RP2040 USB Host CIRCUITPY drive should look like this after copying the lib folder, boot.py file and code.py file:

CPython Keyboard Sender Code
To run the script you will need a desktop or laptop computer with Python 3 installed.
# SPDX-FileCopyrightText: 2025 Liz Clark for Adafruit Industries # # SPDX-License-Identifier: MIT #!/usr/bin/env python3 """ USB Typewriter Computer-side Script Captures keyboard input and sends it to the Feather via serial """ import struct import time import threading import queue import sys import serial import serial.tools.list_ports from pynput import keyboard class TypewriterSender: def __init__(self): self.serial_port = None self.key_queue = queue.Queue() self.running = True self.modifier_state = 0 # Map pynput keys to HID keycodes self.key_to_hid = { # Letters 'a': 0x04, 'b': 0x05, 'c': 0x06, 'd': 0x07, 'e': 0x08, 'f': 0x09, 'g': 0x0A, 'h': 0x0B, 'i': 0x0C, 'j': 0x0D, 'k': 0x0E, 'l': 0x0F, 'm': 0x10, 'n': 0x11, 'o': 0x12, 'p': 0x13, 'q': 0x14, 'r': 0x15, 's': 0x16, 't': 0x17, 'u': 0x18, 'v': 0x19, 'w': 0x1A, 'x': 0x1B, 'y': 0x1C, 'z': 0x1D, # Numbers '1': 0x1E, '2': 0x1F, '3': 0x20, '4': 0x21, '5': 0x22, '6': 0x23, '7': 0x24, '8': 0x25, '9': 0x26, '0': 0x27, # Special keys keyboard.Key.enter: 0x28, keyboard.Key.esc: 0x29, keyboard.Key.backspace: 0x2A, keyboard.Key.tab: 0x2B, keyboard.Key.space: 0x2C, '-': 0x2D, '=': 0x2E, '[': 0x2F, ']': 0x30, '\\': 0x31, ';': 0x33, "'": 0x34, '`': 0x35, ',': 0x36, '.': 0x37, '/': 0x38, keyboard.Key.caps_lock: 0x39, # Arrow keys keyboard.Key.right: 0x4F, keyboard.Key.left: 0x50, keyboard.Key.down: 0x51, keyboard.Key.up: 0x52, } # Add function keys for i in range(1, 13): self.key_to_hid[getattr(keyboard.Key, f'f{i}')] = 0x3A + i - 1 # Modifier bits self.modifier_bits = { keyboard.Key.ctrl_l: 0x01, keyboard.Key.shift_l: 0x02, keyboard.Key.alt_l: 0x04, keyboard.Key.cmd_l: 0x08, # Windows/Command key keyboard.Key.ctrl_r: 0x10, keyboard.Key.shift_r: 0x20, keyboard.Key.alt_r: 0x40, keyboard.Key.cmd_r: 0x80, } @staticmethod def find_feather_port(): """Find the Feather's serial port""" ports = serial.tools.list_ports.comports() print("Available serial ports:") for i, port in enumerate(ports): print(f"{i}: {port.device} - {port.description}") feather_port = None if not feather_port: # Manual selection try: choice = int(input("\nSelect port number: ")) if 0 <= choice < len(ports): feather_port = ports[choice].device else: print("Invalid selection") return None except (ValueError, IndexError): print("Invalid input") return None return feather_port def connect(self): """Connect to the Feather via serial""" port = self.find_feather_port() if not port: return False try: self.serial_port = serial.Serial(port, 115200, timeout=0.1) time.sleep(2) # Wait for connection to stabilize print(f"Connected to {port}") return True except Exception as e: # pylint: disable=broad-except print(f"Failed to connect: {e}") return False def send_key_event(self, hid_code, pressed): """Send a key event to the Feather""" if self.serial_port and self.serial_port.is_open: try: # Protocol: [0xAA][modifier_byte][key_code][pressed] # 0xAA is a start marker data = struct.pack('BBBB', 0xAA, self.modifier_state, hid_code, 1 if pressed else 0) self.serial_port.write(data) self.serial_port.flush() except Exception as e: # pylint: disable=broad-except print(f"Error sending data: {e}") def on_press(self, key): """Handle key press events""" # Check for modifier keys if key in self.modifier_bits: self.modifier_state |= self.modifier_bits[key] self.send_key_event(0, True) # Send modifier update return # Get HID code for the key hid_code = None # Check if it's a special key if hasattr(key, 'value') and key in self.key_to_hid: hid_code = self.key_to_hid[key] # Check if it's a regular character elif hasattr(key, 'char') and key.char: hid_code = self.key_to_hid.get(key.char.lower()) if hid_code: self.key_queue.put((hid_code, True)) def on_release(self, key): """Handle key release events""" # Check for modifier keys if key in self.modifier_bits: self.modifier_state &= ~self.modifier_bits[key] self.send_key_event(0, False) # Send modifier update return None # Get HID code for the key hid_code = None # Check if it's a special key if hasattr(key, 'value') and key in self.key_to_hid: hid_code = self.key_to_hid[key] # Check if it's a regular character elif hasattr(key, 'char') and key.char: hid_code = self.key_to_hid.get(key.char.lower()) if hid_code: self.key_queue.put((hid_code, False)) # Check for escape to quit if key == keyboard.Key.esc: print("\nESC pressed - exiting...") self.running = False return False return None def process_queue(self): """Process queued key events""" while self.running: try: hid_code, pressed = self.key_queue.get(timeout=0.1) self.send_key_event(hid_code, pressed) # Debug output action = "pressed" if pressed else "released" print(f"Key {action}: 0x{hid_code:02X}") except queue.Empty: continue def run(self): """Main run loop""" if not self.connect(): print("Failed to connect to Feather") return print("\nNot A Typewriter") print("Press keys to send to typewriter") print("Press ESC to exit") print("-" * 30) # Start queue processor thread queue_thread = threading.Thread(target=self.process_queue) queue_thread.daemon = True queue_thread.start() # Start keyboard listener with keyboard.Listener( on_press=self.on_press, on_release=self.on_release) as listener: listener.join() # Cleanup if self.serial_port: self.serial_port.close() print("Disconnected") if __name__ == "__main__": try: sender = TypewriterSender() sender.run() except KeyboardInterrupt: print("\nInterrupted") sys.exit(0)
pip install pyserial pip install pynput
Use
First, you'll plug the Feather running the CircuitPython code into a USB port on your computer. This mounts the USB CDC port to your computer, which the CPython script needs to access.
To run the CPython script, open a terminal window and navigate to the directory where you have the script. Run the script with:
python keyboard_sender.py
When you launch the script, you'll be prompted to select the USB CDC port on the Feather. The boot.py file on the Feather allows for two COM ports to be opened on the Feather. These ports are numbered consecutively and the CDC port will always be the second one. For example, in the screenshot you can see ports COM53 and COM54 are available. The CDC port is COM54.
As you type, you'll hear the solenoids begin triggering. In the terminal where you launched the script, you'll see the keycodes printed out as you type.
Page last edited June 17, 2025
Text editor powered by tinymce.