/ CircuitPython_NeXT_Keyboard_RP2040 / code.py
code.py
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_hid.mouse import Mouse 14 from adafruit_pioasm import Program 15 from adafruit_ticks import ticks_add, ticks_less, ticks_ms 16 from next_keycode import ( 17 cc_value, 18 is_cc, 19 next_modifiers, 20 next_scancodes, 21 shifted_codes, 22 shift_modifiers, 23 ) 24 25 26 # Compared to a modern mouse, the DPI of the NeXT mouse is low. Increasing this 27 # number makes the pointer move further faster, but it also makes moves chunky. 28 # Customize this number according to the trade-off you want, but also check 29 # whether your operating system can assign a higher "sensitivity" or 30 # "acceleration" for the mouse. 31 MOUSE_SCALE = 8 32 33 # Customize the power key's keycode. You can change it to `Keycode.POWER` if 34 # you really want to accidentally power off your computer! 35 POWER_KEY_SENDS = Keycode.F1 36 37 # according to https://journal.spencerwnelson.com/entries/nextkb.html the 38 # keyboard's timing source is a 455MHz crystal, and the serial data rate is 39 # 1/24 the crystal frequency. This differs by a few percent from the "50us" bit 40 # time reported in other sources. 41 NEXT_SERIAL_BUS_FREQUENCY = round(455_000 / 24) 42 43 pio_program = Program( 44 """ 45 top: 46 set pins, 1 47 pull block ; wait for send request 48 out x, 1 ; trigger receive? 49 out y, 7 ; get count of bits to transmit (minus 1) 50 51 bitloop: 52 out pins, 1 [7] ; send next bit 53 jmp y--, bitloop [7] ; loop if bits left to send 54 55 set pins, 1 ; idle the bus after last bit 56 jmp !x, top ; to top if no scancode expected 57 58 set pins, 1 ; mark bus as idle so keyboard will send 59 set y, 19 ; 20 bits to receive 60 61 wait 0, pin 0 [7] ; wait for falling edge plus half bit time 62 recvloop: 63 in pins, 1 [7] ; sample in the middle of the bit 64 jmp y--, recvloop [7] ; loop until all bits read 65 66 push ; send report to CircuitPython 67 """ 68 ) 69 70 71 def pack_message(bitcount, data, trigger_receive=False): 72 if bitcount > 24: 73 raise ValueError("too many bits in message") 74 trigger_receive = bool(trigger_receive) 75 message = ( 76 (trigger_receive << 31) | ((bitcount - 1) << 24) | (data << (24 - bitcount)) 77 ) 78 return array.array("I", [message]) 79 80 81 def pack_message_str(bitstring, trigger_receive=False): 82 bitcount = len(bitstring) 83 data = int(bitstring, 2) 84 return pack_message(bitcount, data, trigger_receive=trigger_receive) 85 86 87 def set_leds(i): 88 return pack_message_str(f"0000000001110{i:02b}0000000") 89 90 91 QUERY = pack_message_str("000001000", True) 92 MOUSEQUERY = pack_message_str("010001000", True) 93 RESET = pack_message_str("0111101111110000000000") 94 95 BIT_BREAK = 1 << 11 96 BIT_MOD = 1 97 98 99 def is_make(report): 100 return not bool(report & BIT_BREAK) 101 102 103 def is_mod_report(report): 104 return not bool(report & BIT_MOD) 105 106 107 def extract_bits(report, *positions): 108 result = 0 109 for p in positions: 110 result = (result << 1) 111 if report & (1 << p): 112 result |= 1 113 #result = (result << 1) | bool(report & (1<<p)) 114 return result 115 116 # keycode bits are backwards compared to other information sources 117 # (bit 0 is first) 118 def keycode(report): 119 return extract_bits(report, 12, 13, 14, 15, 16, 17, 18) 120 121 122 def modifiers(report): 123 return (report >> 1) & 0x7F 124 125 126 sm = rp2pio.StateMachine( 127 pio_program.assembled, 128 first_in_pin=board.MISO, 129 pull_in_pin_up=1, 130 first_set_pin=board.MOSI, 131 set_pin_count=1, 132 first_out_pin=board.MOSI, 133 out_pin_count=1, 134 frequency=16 * NEXT_SERIAL_BUS_FREQUENCY, 135 in_shift_right=False, 136 wait_for_txstall=False, 137 out_shift_right=False, 138 **pio_program.pio_kwargs, 139 ) 140 141 142 def signfix(num, sign_pos): 143 """Fix a signed number if the bit with weight `sign_pos` is actually the sign bit""" 144 if num & sign_pos: 145 return num - 2*sign_pos 146 return num 147 148 class KeyboardHandler: 149 def __init__(self): 150 self.old_modifiers = 0 151 self.cc = ConsumerControl(usb_hid.devices) 152 self.kbd = Keyboard(usb_hid.devices) 153 self.mouse = Mouse(usb_hid.devices) 154 155 def set_key_state(self, key, state): 156 if state: 157 if isinstance(key, tuple): 158 old_report_modifier = self.kbd.report_modifier[0] 159 self.kbd.report_modifier[0] = 0 160 self.kbd.press(*key) 161 self.kbd.release_all() 162 self.kbd.report_modifier[0] = old_report_modifier 163 else: 164 self.kbd.press(key) 165 else: 166 if isinstance(key, tuple): 167 pass 168 else: 169 self.kbd.release(key) 170 171 def handle_mouse_report(self, report): 172 if report == 1536: # the "nothing happened" report 173 return 174 175 dx = extract_bits(report, 11,12,13,14,15,16,17) 176 dx = -signfix(dx, 64) 177 dy = extract_bits(report, 0,1,2,3,4,5,6) 178 dy = -signfix(dy, 64) 179 b1 = not extract_bits(report, 18) 180 b2 = not extract_bits(report, 7) 181 182 self.mouse.report[0] = ( 183 Mouse.MIDDLE_BUTTON if (b1 and b2) else 184 Mouse.LEFT_BUTTON if b1 else 185 Mouse.RIGHT_BUTTON if b2 186 else 0) 187 if dx or dy: 188 self.mouse.move(dx * MOUSE_SCALE, dy * MOUSE_SCALE) 189 else: 190 self.mouse._send_no_move() # pylint: disable=protected-access 191 192 def handle_report(self, report_value): 193 if report_value == 1536: # the "nothing happened" report 194 return 195 196 # Handle modifier changes 197 mods = modifiers(report_value) 198 changes = self.old_modifiers ^ mods 199 self.old_modifiers = mods 200 for i in range(7): 201 bit = 1 << i 202 if changes & bit: # Modifier key pressed or released 203 self.set_key_state(next_modifiers[i], mods & bit) 204 205 # Handle key press/release 206 code = next_scancodes.get(keycode(report_value)) 207 if mods & shift_modifiers: 208 code = shifted_codes.get(keycode(report_value), code) 209 make = is_make(report_value) 210 if code: 211 if is_cc(code): 212 if make: 213 self.cc.send(cc_value(code)) 214 else: 215 self.set_key_state(code, make) 216 217 keys = Keys([board.SCK], value_when_pressed=False) 218 219 handler = KeyboardHandler() 220 221 recv_buf = array.array("I", [0]) 222 223 time.sleep(0.1) 224 sm.write(RESET) 225 time.sleep(0.1) 226 227 for _ in range(4): 228 sm.write(set_leds(3)) 229 time.sleep(0.1) 230 sm.write(set_leds(0)) 231 time.sleep(0.1) 232 233 print("Keyboard ready!") 234 235 try: 236 while True: 237 if (event := keys.events.get()): 238 handler.set_key_state(POWER_KEY_SENDS, event.pressed) 239 240 sm.write(QUERY) 241 deadline = ticks_add(ticks_ms(), 100) 242 while ticks_less(ticks_ms(), deadline): 243 if sm.in_waiting: 244 sm.readinto(recv_buf) 245 value = recv_buf[0] 246 handler.handle_report(value) 247 break 248 else: 249 print("keyboard did not respond - resetting") 250 sm.restart() 251 sm.write(RESET) 252 time.sleep(0.1) 253 254 sm.write(MOUSEQUERY) 255 deadline = ticks_add(ticks_ms(), 100) 256 while ticks_less(ticks_ms(), deadline): 257 if sm.in_waiting: 258 sm.readinto(recv_buf) 259 value = recv_buf[0] 260 handler.handle_mouse_report(value) 261 break 262 else: 263 print("keyboard did not respond - resetting") 264 sm.restart() 265 sm.write(RESET) 266 time.sleep(0.1) 267 finally: # Release all keys before e.g., code is reloaded 268 handler.kbd.release_all()