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
This commit is contained in:
parent
cee73a11c7
commit
5b4ba29cc2
4 changed files with 807 additions and 0 deletions
|
|
@ -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)
|
||||||
|
|
@ -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)
|
||||||
222
Not_A_Typewriter/Desktop_Not_A_Typewriter/keyboard_sender.py
Normal file
222
Not_A_Typewriter/Desktop_Not_A_Typewriter/keyboard_sender.py
Normal file
|
|
@ -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)
|
||||||
387
Not_A_Typewriter/USB_Host_Not_A_Typewriter/code.py
Normal file
387
Not_A_Typewriter/USB_Host_Not_A_Typewriter/code.py
Normal file
|
|
@ -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}")
|
||||||
Loading…
Reference in a new issue