Skip to content

Commit 29044fb

Browse files
authored
Merge pull request #2337 from jepler/next-keyboard-circuitpython
Add NeXT to USB adapter with CircuitPython & RP2040
2 parents 536aadd + 04ca48e commit 29044fb

2 files changed

Lines changed: 321 additions & 0 deletions

File tree

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
# SPDX-FileCopyrightText: 2022 Jeff Epler for Adafruit Industries
2+
# SPDX-License-Identifier: MIT
3+
import array
4+
import time
5+
6+
import board
7+
import rp2pio
8+
import usb_hid
9+
from keypad import Keys
10+
from adafruit_hid.consumer_control import ConsumerControl
11+
from adafruit_hid.keyboard import Keyboard
12+
from adafruit_hid.keyboard import Keycode
13+
from adafruit_pioasm import Program
14+
from adafruit_ticks import ticks_add, ticks_less, ticks_ms
15+
from next_keycode import (
16+
cc_value,
17+
is_cc,
18+
next_modifiers,
19+
next_scancodes,
20+
shifted_codes,
21+
shift_modifiers,
22+
)
23+
24+
# Customize the power key's keycode. You can change it to `Keycode.POWER` if
25+
# you really want to accidentally power off your computer!
26+
POWER_KEY_SENDS = Keycode.F1
27+
28+
# according to https://journal.spencerwnelson.com/entries/nextkb.html the
29+
# keyboard's timing source is a 455MHz crystal, and the serial data rate is
30+
# 1/24 the crystal frequency. This differs by a few percent from the "50us" bit
31+
# time reported in other sources.
32+
NEXT_SERIAL_BUS_FREQUENCY = round(455_000 / 24)
33+
34+
pio_program = Program(
35+
"""
36+
top:
37+
set pins, 1
38+
pull block ; wait for send request
39+
out x, 1 ; trigger receive?
40+
out y, 7 ; get count of bits to transmit (minus 1)
41+
42+
bitloop:
43+
out pins, 1 [7] ; send next bit
44+
jmp y--, bitloop [7] ; loop if bits left to send
45+
46+
set pins, 1 ; idle the bus after last bit
47+
jmp !x, top ; to top if no scancode expected
48+
49+
set pins, 1 ; mark bus as idle so keyboard will send
50+
set y, 19 ; 20 bits to receive
51+
52+
wait 0, pin 0 [7] ; wait for falling edge plus half bit time
53+
recvloop:
54+
in pins, 1 [7] ; sample in the middle of the bit
55+
jmp y--, recvloop [7] ; loop until all bits read
56+
57+
push ; send report to CircuitPython
58+
"""
59+
)
60+
61+
62+
def pack_message(bitcount, data, trigger_receive=False):
63+
if bitcount > 24:
64+
raise ValueError("too many bits in message")
65+
trigger_receive = bool(trigger_receive)
66+
message = (
67+
(trigger_receive << 31) | ((bitcount - 1) << 24) | (data << (24 - bitcount))
68+
)
69+
return array.array("I", [message])
70+
71+
72+
def pack_message_str(bitstring, trigger_receive=False):
73+
bitcount = len(bitstring)
74+
data = int(bitstring, 2)
75+
return pack_message(bitcount, data, trigger_receive=trigger_receive)
76+
77+
78+
def set_leds(i):
79+
return pack_message_str(f"0000000001110{i:02b}0000000")
80+
81+
82+
QUERY = pack_message_str("000001000", 1)
83+
RESET = pack_message_str("0111101111110000000000")
84+
85+
BIT_BREAK = 1 << 11
86+
BIT_MOD = 1
87+
88+
89+
def is_make(report):
90+
return not bool(report & BIT_BREAK)
91+
92+
93+
def is_mod_report(report):
94+
return not bool(report & BIT_MOD)
95+
96+
97+
# keycode bits are backwards compared to other information sources
98+
# (bit 0 is first)
99+
def keycode(report):
100+
b = f"{report >> 12:07b}"
101+
b = "".join(reversed(b))
102+
return int(b, 2)
103+
104+
105+
def modifiers(report):
106+
return (report >> 1) & 0x7F
107+
108+
109+
sm = rp2pio.StateMachine(
110+
pio_program.assembled,
111+
first_in_pin=board.MISO,
112+
pull_in_pin_up=1,
113+
first_set_pin=board.MOSI,
114+
set_pin_count=1,
115+
first_out_pin=board.MOSI,
116+
out_pin_count=1,
117+
frequency=16 * NEXT_SERIAL_BUS_FREQUENCY,
118+
in_shift_right=False,
119+
wait_for_txstall=False,
120+
out_shift_right=False,
121+
**pio_program.pio_kwargs,
122+
)
123+
124+
125+
class KeyboardHandler:
126+
def __init__(self):
127+
self.old_modifiers = 0
128+
self.cc = ConsumerControl(usb_hid.devices)
129+
self.kbd = Keyboard(usb_hid.devices)
130+
131+
def set_key_state(self, key, state):
132+
if state:
133+
if isinstance(key, tuple):
134+
old_report_modifier = self.kbd.report_modifier[0]
135+
self.kbd.report_modifier[0] = 0
136+
self.kbd.press(*key)
137+
self.kbd.release_all()
138+
self.kbd.report_modifier[0] = old_report_modifier
139+
else:
140+
self.kbd.press(key)
141+
else:
142+
if isinstance(key, tuple):
143+
pass
144+
else:
145+
self.kbd.release(key)
146+
147+
def handle_report(self, report_value):
148+
if report_value == 1536: # the "nothing happened" report
149+
return
150+
151+
# Handle modifier changes
152+
mods = modifiers(report_value)
153+
changes = self.old_modifiers ^ mods
154+
self.old_modifiers = mods
155+
for i in range(7):
156+
bit = 1 << i
157+
if changes & bit: # Modifier key pressed or released
158+
self.set_key_state(next_modifiers[i], mods & bit)
159+
160+
# Handle key press/release
161+
code = next_scancodes.get(keycode(report_value))
162+
if mods & shift_modifiers:
163+
code = shifted_codes.get(keycode(report_value), code)
164+
make = is_make(report_value)
165+
if code:
166+
if is_cc(code):
167+
if make:
168+
self.cc.send(cc_value(code))
169+
else:
170+
self.set_key_state(code, make)
171+
172+
keys = Keys([board.SCK], value_when_pressed=False)
173+
174+
handler = KeyboardHandler()
175+
176+
recv_buf = array.array("I", [0])
177+
178+
time.sleep(0.1)
179+
sm.write(RESET)
180+
time.sleep(0.1)
181+
182+
for _ in range(4):
183+
sm.write(set_leds(3))
184+
time.sleep(0.1)
185+
sm.write(set_leds(0))
186+
time.sleep(0.1)
187+
188+
print("Keyboard ready!")
189+
190+
try:
191+
while True:
192+
if (event := keys.events.get()):
193+
handler.set_key_state(POWER_KEY_SENDS, event.pressed)
194+
195+
sm.write(QUERY)
196+
deadline = ticks_add(ticks_ms(), 100)
197+
while ticks_less(ticks_ms(), deadline):
198+
if sm.in_waiting:
199+
sm.readinto(recv_buf)
200+
value = recv_buf[0]
201+
handler.handle_report(value)
202+
break
203+
else:
204+
print("keyboard did not respond - resetting")
205+
sm.restart()
206+
sm.write(RESET)
207+
time.sleep(0.1)
208+
finally: # Release all keys before e.g., code is reloaded
209+
handler.kbd.release_all()
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
# SPDX-FileCopyrightText: 2022 Jeff Epler for Adafruit Industries
2+
# SPDX-License-Identifier: MIT
3+
from adafruit_hid.consumer_control_code import ConsumerControlCode as C
4+
from adafruit_hid.keycode import Keycode as K
5+
6+
MASK_CC = 1 << 15
7+
8+
9+
def is_cc(value):
10+
return isinstance(value, int) and (value & MASK_CC)
11+
12+
13+
def cc_value(value):
14+
return value & ~MASK_CC
15+
16+
17+
next_modifiers = [
18+
K.RIGHT_ALT,
19+
K.ALT,
20+
K.APPLICATION, # right command
21+
K.COMMAND,
22+
K.RIGHT_SHIFT,
23+
K.SHIFT,
24+
K.CONTROL,
25+
]
26+
27+
shift_modifiers = (1<<4) | (1<<5)
28+
29+
next_scancodes = {
30+
3: K.BACKSLASH,
31+
4: K.RIGHT_BRACKET,
32+
5: K.LEFT_BRACKET,
33+
6: K.I,
34+
7: K.O,
35+
8: K.P,
36+
9: K.LEFT_ARROW,
37+
11: K.KEYPAD_ZERO,
38+
12: K.KEYPAD_PERIOD,
39+
13: K.KEYPAD_ENTER,
40+
15: K.DOWN_ARROW,
41+
16: K.RIGHT_ARROW,
42+
17: K.KEYPAD_ONE,
43+
18: K.KEYPAD_FOUR,
44+
19: K.KEYPAD_SIX,
45+
20: K.KEYPAD_THREE,
46+
21: K.KEYPAD_PLUS,
47+
22: K.UP_ARROW,
48+
23: K.KEYPAD_TWO,
49+
24: K.KEYPAD_FIVE,
50+
27: K.BACKSPACE,
51+
28: K.EQUALS,
52+
29: K.MINUS,
53+
30: K.EIGHT,
54+
31: K.NINE,
55+
32: K.ZERO,
56+
33: K.KEYPAD_SEVEN,
57+
34: K.KEYPAD_EIGHT,
58+
35: K.KEYPAD_NINE,
59+
36: K.KEYPAD_MINUS,
60+
37: K.KEYPAD_ASTERISK,
61+
38: K.GRAVE_ACCENT,
62+
39: K.KEYPAD_EQUALS,
63+
40: K.KEYPAD_FORWARD_SLASH,
64+
42: K.RETURN,
65+
43: K.QUOTE,
66+
44: K.SEMICOLON,
67+
45: K.L,
68+
46: K.COMMA,
69+
47: K.PERIOD,
70+
48: K.FORWARD_SLASH,
71+
49: K.Z,
72+
50: K.X,
73+
51: K.C,
74+
52: K.V,
75+
53: K.B,
76+
54: K.M,
77+
55: K.N,
78+
56: K.SPACE,
79+
57: K.A,
80+
58: K.S,
81+
59: K.D,
82+
60: K.F,
83+
61: K.G,
84+
62: K.K,
85+
63: K.J,
86+
64: K.H,
87+
65: K.TAB,
88+
66: K.Q,
89+
67: K.W,
90+
68: K.E,
91+
69: K.R,
92+
70: K.U,
93+
71: K.Y,
94+
72: K.T,
95+
73: K.ESCAPE,
96+
74: K.ONE,
97+
75: K.TWO,
98+
76: K.THREE,
99+
77: K.FOUR,
100+
78: K.SEVEN,
101+
79: K.SIX,
102+
80: K.FIVE,
103+
26: C.VOLUME_INCREMENT | MASK_CC,
104+
2: C.VOLUME_DECREMENT | MASK_CC,
105+
25: C.BRIGHTNESS_INCREMENT | MASK_CC,
106+
1: C.BRIGHTNESS_DECREMENT | MASK_CC,
107+
}
108+
109+
shifted_codes = {
110+
39: K.BACKSLASH, # already shifted
111+
40: (K.BACKSLASH,), # will temporarily undo shift
112+
}

0 commit comments

Comments
 (0)