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()