Skip to content

Commit 0434974

Browse files
authored
Merge pull request #3063 from adafruit/typewriter
code for not a typewriter
2 parents f393f27 + dbf2a43 commit 0434974

File tree

4 files changed

+808
-0
lines changed

4 files changed

+808
-0
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# SPDX-FileCopyrightText: 2025 Liz Clark for Adafruit Industries
2+
#
3+
# SPDX-License-Identifier: MIT
4+
5+
import usb_cdc
6+
7+
# Enable USB CDC (serial) communication
8+
usb_cdc.enable(console=True, data=True)
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
# SPDX-FileCopyrightText: 2025 Liz Clark for Adafruit Industries
2+
#
3+
# SPDX-License-Identifier: MIT
4+
5+
"""
6+
USB Typewriter Feather-side Script
7+
Converts incoming keystrokes to solenoid clicks
8+
"""
9+
10+
import time
11+
import struct
12+
import usb_cdc
13+
import board
14+
from adafruit_mcp230xx.mcp23017 import MCP23017
15+
16+
# Typewriter configuration
17+
KEYSTROKE_BELL_INTERVAL = 25 # Ring bell every 25 keystrokes
18+
SOLENOID_STRIKE_TIME = 0.03 # Duration in seconds for solenoid activation
19+
ENTER_KEY_CODE = 0x28 # HID code for Enter key
20+
ESCAPE_KEY_CODE = 0x29 # HID code for Escape key
21+
BACKSPACE_KEY_CODE = 0x2A # HID code for Backspace key
22+
TAB_KEY_CODE = 0x2B # HID code for Tab key
23+
24+
# Key name mapping for debug output
25+
key_names = {
26+
0x04: "A", 0x05: "B", 0x06: "C", 0x07: "D",
27+
0x08: "E", 0x09: "F", 0x0A: "G", 0x0B: "H",
28+
0x0C: "I", 0x0D: "J", 0x0E: "K", 0x0F: "L",
29+
0x10: "M", 0x11: "N", 0x12: "O", 0x13: "P",
30+
0x14: "Q", 0x15: "R", 0x16: "S", 0x17: "T",
31+
0x18: "U", 0x19: "V", 0x1A: "W", 0x1B: "X",
32+
0x1C: "Y", 0x1D: "Z",
33+
0x1E: "1", 0x1F: "2", 0x20: "3", 0x21: "4",
34+
0x22: "5", 0x23: "6", 0x24: "7", 0x25: "8",
35+
0x26: "9", 0x27: "0",
36+
0x28: "ENTER", 0x29: "ESC", 0x2A: "BACKSPACE",
37+
0x2B: "TAB", 0x2C: "SPACE", 0x2D: "MINUS",
38+
0x2E: "EQUAL", 0x2F: "LBRACKET", 0x30: "RBRACKET",
39+
0x31: "BACKSLASH", 0x33: "SEMICOLON", 0x34: "QUOTE",
40+
0x35: "GRAVE", 0x36: "COMMA", 0x37: "PERIOD",
41+
0x38: "SLASH", 0x39: "CAPS_LOCK",
42+
0x4F: "RIGHT", 0x50: "LEFT", 0x51: "DOWN", 0x52: "UP",
43+
}
44+
45+
# Add F1-F12 keys
46+
for i in range(12):
47+
key_names[0x3A + i] = f"F{i + 1}"
48+
49+
# Set up I2C and MCP23017
50+
i2c = board.STEMMA_I2C()
51+
mcp = MCP23017(i2c)
52+
53+
# Configure solenoid pins
54+
noid_1 = mcp.get_pin(0) # Bell solenoid
55+
noid_2 = mcp.get_pin(1) # Key strike solenoid
56+
noid_1.switch_to_output(value=False)
57+
noid_2.switch_to_output(value=False)
58+
59+
# Typewriter state tracking
60+
keystroke_count = 0
61+
current_keys = set() # Track currently pressed keys
62+
63+
# Check if USB CDC data is available
64+
if usb_cdc.data is None:
65+
print("ERROR: USB CDC data not enabled!")
66+
print("Please create a boot.py file with:")
67+
print(" import usb_cdc")
68+
print(" usb_cdc.enable(console=True, data=True)")
69+
print("\nThen reset the board.")
70+
while True:
71+
time.sleep(1)
72+
73+
serial = usb_cdc.data
74+
75+
def strike_key_solenoid():
76+
"""Activate the key strike solenoid briefly"""
77+
noid_2.value = True
78+
time.sleep(SOLENOID_STRIKE_TIME)
79+
noid_2.value = False
80+
81+
def ring_bell_solenoid():
82+
"""Activate the bell solenoid briefly"""
83+
noid_1.value = True
84+
time.sleep(SOLENOID_STRIKE_TIME)
85+
noid_1.value = False
86+
87+
def process_key_event(mod, code, p): # pylint: disable=too-many-branches
88+
"""Process a key event from the computer"""
89+
global keystroke_count # pylint: disable=global-statement
90+
91+
# Debug output
92+
key_name = key_names.get(code, f"0x{code:02X}")
93+
action = "pressed" if p else "released"
94+
95+
# Handle modifier display
96+
if mod > 0:
97+
mod_str = []
98+
if mod & 0x01:
99+
mod_str.append("L_CTRL")
100+
if mod & 0x02:
101+
mod_str.append("L_SHIFT")
102+
if mod & 0x04:
103+
mod_str.append("L_ALT")
104+
if mod & 0x08:
105+
mod_str.append("L_GUI")
106+
if mod & 0x10:
107+
mod_str.append("R_CTRL")
108+
if mod & 0x20:
109+
mod_str.append("R_SHIFT")
110+
if mod & 0x40:
111+
mod_str.append("R_ALT")
112+
if mod & 0x80:
113+
mod_str.append("R_GUI")
114+
print(f"[{'+'.join(mod_str)}] {key_name} {action}")
115+
else:
116+
print(f"{key_name} {action}")
117+
118+
# Only process key presses (not releases) for solenoid activation
119+
if p and code > 0: # key_code 0 means modifier-only update
120+
# Check if this is a new key press
121+
if code not in current_keys:
122+
current_keys.add(code)
123+
124+
# Increment keystroke counter
125+
keystroke_count += 1
126+
127+
# Strike the key solenoid
128+
strike_key_solenoid()
129+
130+
# Check for special keys
131+
if code == ENTER_KEY_CODE:
132+
ring_bell_solenoid()
133+
keystroke_count = 0 # Reset counter for new line
134+
elif code == ESCAPE_KEY_CODE:
135+
ring_bell_solenoid()
136+
keystroke_count = 0 # Reset counter
137+
elif code == TAB_KEY_CODE:
138+
ring_bell_solenoid()
139+
keystroke_count = 0 # Reset counter
140+
elif code == BACKSPACE_KEY_CODE:
141+
keystroke_count = 0 # Reset counter but no bell
142+
elif keystroke_count % KEYSTROKE_BELL_INTERVAL == 0:
143+
print(f"\n*** DING! ({keystroke_count} keystrokes) ***\n")
144+
ring_bell_solenoid()
145+
146+
print(f"Total keystrokes: {keystroke_count}")
147+
148+
elif not p and code > 0:
149+
# Remove key from pressed set when released
150+
current_keys.discard(code)
151+
152+
print("USB Typewriter Receiver starting...")
153+
print(f"Bell will ring every {KEYSTROKE_BELL_INTERVAL} keystrokes or on special keys")
154+
print("Waiting for key events from computer...")
155+
print("-" * 40)
156+
157+
# Buffer for incoming data
158+
buffer = bytearray(4)
159+
buffer_pos = 0
160+
161+
while True:
162+
# Check for incoming serial data
163+
if serial.in_waiting > 0:
164+
# Read available bytes
165+
data = serial.read(serial.in_waiting)
166+
167+
for byte in data:
168+
# Look for start marker
169+
if buffer_pos == 0:
170+
if byte == 0xAA:
171+
buffer[0] = byte
172+
buffer_pos = 1
173+
else:
174+
# Fill buffer
175+
buffer[buffer_pos] = byte
176+
buffer_pos += 1
177+
178+
# Process complete message
179+
if buffer_pos >= 4:
180+
# Unpack the message
181+
_, modifier, key_code, pressed = struct.unpack('BBBB', buffer)
182+
183+
# Process the key event
184+
process_key_event(modifier, key_code, pressed)
185+
186+
# Reset buffer
187+
buffer_pos = 0
188+
189+
# Small delay to prevent busy-waiting
190+
time.sleep(0.001)

0 commit comments

Comments
 (0)