From 5b4ba29cc2b99236b377ce2d8a97a902d7d48e27 Mon Sep 17 00:00:00 2001 From: Liz Date: Fri, 13 Jun 2025 14:17:55 -0400 Subject: [PATCH 1/2] code for not a typewriter both versions of the not a typewriter code. one uses only circuitpython with usb host for a usb keyboard. the second has a desktop python script that communicates with the feather over serial --- .../CircuitPython_Serial_Typewriter/boot.py | 8 + .../CircuitPython_Serial_Typewriter/code.py | 190 +++++++++ .../keyboard_sender.py | 222 ++++++++++ .../USB_Host_Not_A_Typewriter/code.py | 387 ++++++++++++++++++ 4 files changed, 807 insertions(+) create mode 100644 Not_A_Typewriter/Desktop_Not_A_Typewriter/CircuitPython_Serial_Typewriter/boot.py create mode 100644 Not_A_Typewriter/Desktop_Not_A_Typewriter/CircuitPython_Serial_Typewriter/code.py create mode 100644 Not_A_Typewriter/Desktop_Not_A_Typewriter/keyboard_sender.py create mode 100644 Not_A_Typewriter/USB_Host_Not_A_Typewriter/code.py diff --git a/Not_A_Typewriter/Desktop_Not_A_Typewriter/CircuitPython_Serial_Typewriter/boot.py b/Not_A_Typewriter/Desktop_Not_A_Typewriter/CircuitPython_Serial_Typewriter/boot.py new file mode 100644 index 000000000..d4645f413 --- /dev/null +++ b/Not_A_Typewriter/Desktop_Not_A_Typewriter/CircuitPython_Serial_Typewriter/boot.py @@ -0,0 +1,8 @@ +# SPDX-FileCopyrightText: 2025 Liz Clark for Adafruit Industries +# +# SPDX-License-Identifier: MIT + +import usb_cdc + +# Enable USB CDC (serial) communication +usb_cdc.enable(console=True, data=True) \ No newline at end of file diff --git a/Not_A_Typewriter/Desktop_Not_A_Typewriter/CircuitPython_Serial_Typewriter/code.py b/Not_A_Typewriter/Desktop_Not_A_Typewriter/CircuitPython_Serial_Typewriter/code.py new file mode 100644 index 000000000..06f3bb68d --- /dev/null +++ b/Not_A_Typewriter/Desktop_Not_A_Typewriter/CircuitPython_Serial_Typewriter/code.py @@ -0,0 +1,190 @@ +# 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) diff --git a/Not_A_Typewriter/Desktop_Not_A_Typewriter/keyboard_sender.py b/Not_A_Typewriter/Desktop_Not_A_Typewriter/keyboard_sender.py new file mode 100644 index 000000000..019f63648 --- /dev/null +++ b/Not_A_Typewriter/Desktop_Not_A_Typewriter/keyboard_sender.py @@ -0,0 +1,222 @@ +# 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, + } + + def find_feather_port(self): + """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) diff --git a/Not_A_Typewriter/USB_Host_Not_A_Typewriter/code.py b/Not_A_Typewriter/USB_Host_Not_A_Typewriter/code.py new file mode 100644 index 000000000..9ea2b36fc --- /dev/null +++ b/Not_A_Typewriter/USB_Host_Not_A_Typewriter/code.py @@ -0,0 +1,387 @@ +# 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}") From dbf2a4301e53fb5c04518410c2c8f1458cf3c04a Mon Sep 17 00:00:00 2001 From: Liz Date: Fri, 13 Jun 2025 14:24:50 -0400 Subject: [PATCH 2/2] lint --- .../CircuitPython_Serial_Typewriter/boot.py | 2 +- Not_A_Typewriter/Desktop_Not_A_Typewriter/keyboard_sender.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Not_A_Typewriter/Desktop_Not_A_Typewriter/CircuitPython_Serial_Typewriter/boot.py b/Not_A_Typewriter/Desktop_Not_A_Typewriter/CircuitPython_Serial_Typewriter/boot.py index d4645f413..ff9f9e96e 100644 --- a/Not_A_Typewriter/Desktop_Not_A_Typewriter/CircuitPython_Serial_Typewriter/boot.py +++ b/Not_A_Typewriter/Desktop_Not_A_Typewriter/CircuitPython_Serial_Typewriter/boot.py @@ -5,4 +5,4 @@ import usb_cdc # Enable USB CDC (serial) communication -usb_cdc.enable(console=True, data=True) \ No newline at end of file +usb_cdc.enable(console=True, data=True) diff --git a/Not_A_Typewriter/Desktop_Not_A_Typewriter/keyboard_sender.py b/Not_A_Typewriter/Desktop_Not_A_Typewriter/keyboard_sender.py index 019f63648..f8f3ab543 100644 --- a/Not_A_Typewriter/Desktop_Not_A_Typewriter/keyboard_sender.py +++ b/Not_A_Typewriter/Desktop_Not_A_Typewriter/keyboard_sender.py @@ -71,7 +71,8 @@ def __init__(self): keyboard.Key.cmd_r: 0x80, } - def find_feather_port(self): + @staticmethod + def find_feather_port(): """Find the Feather's serial port""" ports = serial.tools.list_ports.comports()