Skip to content

Commit 97befbc

Browse files
committed
Add NeXT to USB adapter with CircuitPython & RP2040
1 parent 527a999 commit 97befbc

2 files changed

Lines changed: 273 additions & 0 deletions

File tree

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
import array
2+
import time
3+
4+
import board
5+
import rp2pio
6+
import usb_hid
7+
from adafruit_hid.consumer_control import ConsumerControl
8+
from adafruit_hid.consumer_control_code import ConsumerControlCode
9+
from adafruit_hid.keyboard import Keyboard
10+
from adafruit_pioasm import Program
11+
from adafruit_ticks import ticks_add, ticks_less, ticks_ms
12+
13+
from next_keycode import cc_value, is_cc, next_modifiers, next_scancodes
14+
15+
NEXT_SERIAL_BUS_FREQUENCY = (
16+
18958 # 455kHz/24 https://journal.spencerwnelson.com/entries/nextkb.html
17+
)
18+
19+
pio_program = Program(
20+
"""
21+
top:
22+
pull block ; wait for send request
23+
out x, 1 ; trigger receive?
24+
out y, 7 ; get count of bits to transmit (minus 1)
25+
26+
set pins, 1
27+
bitloop:
28+
out pins, 1 [7] ; send next bit
29+
jmp y--, bitloop [7] ; loop if bits left to send
30+
31+
set pins, 1 ; idle the bus after last bit
32+
jmp !x, top ; to top if no scancode expected
33+
34+
set y, 19 ; 20 bits to receive
35+
36+
wait 0, pin 0 [7] ; wait for falling edge plus half bit time
37+
recvloop:
38+
in pins, 1 [7] ; sample in the middle of the bit
39+
jmp y--, recvloop [7] ; loop until all bits read
40+
41+
push ; send report to CircuitPython
42+
"""
43+
)
44+
45+
def pack_message(bitcount, data, trigger_receive=False):
46+
if bitcount > 24:
47+
raise ValueError("too many bits in message")
48+
trigger_receive = bool(trigger_receive)
49+
message = (
50+
(trigger_receive << 31) | ((bitcount - 1) << 24) | (data << (24 - bitcount))
51+
)
52+
return array.array("I", [message])
53+
54+
55+
def pack_message_str(bitstring, trigger_receive=False):
56+
bitcount = len(bitstring)
57+
data = int(bitstring, 2)
58+
return pack_message(bitcount, data, trigger_receive=trigger_receive)
59+
60+
61+
def set_leds(i):
62+
return pack_message_str(f"0000000001110{i:02b}0000000")
63+
64+
65+
TEST1 = pack_message_str("010100")
66+
TEST2 = pack_message_str("001010")
67+
QUERY = pack_message_str("000001000", 1)
68+
RESET = pack_message_str("0111101111110000000000")
69+
70+
BIT_BREAK = 1 << 11
71+
BIT_MOD = 1
72+
73+
74+
def is_make(report):
75+
return not bool(report & BIT_BREAK)
76+
77+
78+
def is_mod_report(report):
79+
return not (report & 1)
80+
81+
82+
# keycode bits are backwards compared to other information sources
83+
# (bit 0 is first)
84+
def keycode(report):
85+
b = f"{report >> 12:07b}"
86+
b = "".join(reversed(b))
87+
return int(b, 2)
88+
89+
90+
def modifiers(report):
91+
return (report >> 1) & 0x7F
92+
93+
94+
sm = rp2pio.StateMachine(
95+
pio_program.assembled,
96+
first_sideset_pin=board.D11,
97+
first_in_pin=board.D12,
98+
pull_in_pin_up=1,
99+
first_set_pin=board.D13,
100+
set_pin_count=1,
101+
first_out_pin=board.D13,
102+
out_pin_count=1,
103+
frequency=16 * NEXT_SERIAL_BUS_FREQUENCY,
104+
in_shift_right=False,
105+
wait_for_txstall=False,
106+
out_shift_right=False,
107+
**pio_program.pio_kwargs,
108+
)
109+
110+
class KeyboardHandler:
111+
def __init__(self):
112+
self.old_modifiers = 0
113+
self.cc = ConsumerControl(usb_hid.devices)
114+
self.kbd = Keyboard(usb_hid.devices)
115+
116+
def set_key_state(self, key, state):
117+
print("set_key_state", key, state)
118+
if state:
119+
self.kbd.press(key)
120+
else:
121+
self.kbd.release(key)
122+
123+
def handle_report(self, value):
124+
if value == 1536:
125+
return
126+
127+
if is_mod_report(value):
128+
mods = modifiers(value)
129+
changes = self.old_modifiers ^ mods
130+
self.old_modifiers = mods
131+
for i in range(7):
132+
bit = 1 << i
133+
if changes & bit: # Modifier key pressed or released
134+
self.set_key_state(next_modifiers[i], mods & bit)
135+
else:
136+
code = next_scancodes.get(keycode(value))
137+
make = is_make(value)
138+
if code:
139+
if is_cc(code):
140+
if make:
141+
self.cc.send(cc_value(code))
142+
else:
143+
self.set_key_state(code, make)
144+
145+
146+
handler = KeyboardHandler()
147+
148+
recv_buf = array.array("I", [0])
149+
150+
sm.write(RESET)
151+
time.sleep(0.1)
152+
sm.write(set_leds(0))
153+
time.sleep(0.1)
154+
155+
print("Keyboard ready!")
156+
157+
while True:
158+
sm.write(QUERY)
159+
deadline = ticks_add(ticks_ms(), 100)
160+
while ticks_less(ticks_ms(), deadline):
161+
if sm.in_waiting:
162+
sm.readinto(recv_buf)
163+
value = recv_buf[0]
164+
handler.handle_report(value)
165+
break
166+
else:
167+
print("keyboard did not respond - resetting")
168+
sm.restart()
169+
sm.write(RESET)
170+
time.sleep(0.1)
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
from adafruit_hid.consumer_control_code import ConsumerControlCode as C
2+
from adafruit_hid.keycode import Keycode as K
3+
4+
MASK_CC = 1 << 15
5+
6+
7+
def is_cc(value):
8+
return value & MASK_CC
9+
10+
11+
def cc_value(value):
12+
return value & ~MASK_CC
13+
14+
15+
next_modifiers = [
16+
K.RIGHT_ALT,
17+
K.ALT,
18+
K.APPLICATION, # right command
19+
K.COMMAND,
20+
K.RIGHT_SHIFT,
21+
K.SHIFT,
22+
K.CONTROL,
23+
]
24+
25+
next_scancodes = {
26+
3: K.BACKSLASH,
27+
4: K.RIGHT_BRACKET,
28+
5: K.LEFT_BRACKET,
29+
6: K.I,
30+
7: K.O,
31+
8: K.P,
32+
9: K.LEFT_ARROW,
33+
11: K.KEYPAD_ZERO,
34+
12: K.KEYPAD_PERIOD,
35+
13: K.KEYPAD_ENTER,
36+
15: K.DOWN_ARROW,
37+
16: K.RIGHT_ARROW,
38+
17: K.KEYPAD_ONE,
39+
18: K.KEYPAD_FOUR,
40+
19: K.KEYPAD_SIX,
41+
20: K.KEYPAD_THREE,
42+
21: K.KEYPAD_PLUS,
43+
22: K.UP_ARROW,
44+
23: K.KEYPAD_TWO,
45+
24: K.KEYPAD_FIVE,
46+
27: K.BACKSPACE,
47+
28: K.EQUALS,
48+
29: K.MINUS,
49+
30: K.EIGHT,
50+
31: K.NINE,
51+
32: K.ZERO,
52+
33: K.KEYPAD_SEVEN,
53+
34: K.KEYPAD_EIGHT,
54+
35: K.KEYPAD_NINE,
55+
36: K.KEYPAD_MINUS,
56+
37: K.KEYPAD_ASTERISK,
57+
38: K.GRAVE_ACCENT,
58+
39: K.KEYPAD_EQUALS,
59+
40: K.KEYPAD_FORWARD_SLASH,
60+
42: K.RETURN,
61+
43: K.QUOTE,
62+
44: K.SEMICOLON,
63+
45: K.L,
64+
46: K.COMMA,
65+
47: K.PERIOD,
66+
48: K.FORWARD_SLASH,
67+
49: K.Z,
68+
50: K.X,
69+
51: K.C,
70+
52: K.V,
71+
53: K.B,
72+
54: K.M,
73+
55: K.N,
74+
56: K.SPACE,
75+
57: K.A,
76+
58: K.S,
77+
59: K.D,
78+
60: K.F,
79+
61: K.G,
80+
62: K.K,
81+
63: K.J,
82+
64: K.H,
83+
65: K.TAB,
84+
66: K.Q,
85+
67: K.W,
86+
68: K.E,
87+
69: K.R,
88+
70: K.U,
89+
71: K.Y,
90+
72: K.T,
91+
73: K.ESCAPE,
92+
74: K.ONE,
93+
75: K.TWO,
94+
76: K.THREE,
95+
77: K.FOUR,
96+
78: K.SEVEN,
97+
79: K.SIX,
98+
80: K.FIVE,
99+
26: C.VOLUME_INCREMENT | MASK_CC,
100+
2: C.VOLUME_DECREMENT | MASK_CC,
101+
25: C.BRIGHTNESS_INCREMENT | MASK_CC,
102+
1: C.BRIGHTNESS_DECREMENT | MASK_CC,
103+
}

0 commit comments

Comments
 (0)