code.py
  1  # SPDX-FileCopyrightText: 2022 Jeff Epler for Adafruit Industries
  2  # SPDX-License-Identifier: MIT
  3  
  4  # Commodore 16 to USB HID adapter with Adafruit KB2040
  5  #
  6  # Note that:
  7  #  * This matrix is different than the (more common) Commodore 64 matrix
  8  #  * There are no diodes, not even on modifiers, so there's only 2-key rollover.
  9  
 10  import asyncio.core
 11  import board
 12  import keypad
 13  from adafruit_hid.keycode import Keycode as K
 14  from adafruit_hid.keyboard import Keyboard
 15  import usb_hid
 16  
 17  # True to use a more POSITIONAL mapping, False to use a more PC-style mapping
 18  POSITIONAL = True
 19  
 20  # Keyboard schematic from
 21  # https://archive.org/details/SAMS_Computerfacts_Commodore_C16_1984-12_Howard_W_Sams_Co_CC8/page/n9/mode/2up
 22  # 1  3  6  7  8  9  10 11 12  13   14   15  16 17 18 19  # connector pins
 23  # R5 C7 R7 C4 R1 C5 C6 R3 R2  R4   C2   C1  R6 C3 C0 R0  # row/column in schematic
 24  # D2 D3 D4 D5 D6 D7 D8 D9 D10 MOSI MISO SCK A0 A1 A2 A3  # conencted to kb2040 at
 25  # results in the the following assignment of rows and columns:
 26  rows = [board.A3, board.D6, board.D10, board.D9, board.MOSI, board.D2, board.A0, board.D4]
 27  cols = [board.A2, board.SCK, board.MISO, board.A1, board.D5, board.D7, board.D8, board.D3]
 28  
 29  # ROM listing of key values from ed7.src in
 30  # http://www.zimmers.net/anonftp/pub/cbm/src/plus4/ted_kernal_basic_src.tar.gz
 31  # shows key matrix arrangement (it's nuts)
 32  # del   return  £       f8      f1      f2      f3      @
 33  # 3     w       a       4       z       s       e       shift
 34  # 5     r       d       6       c       f       t       x
 35  # 7     y       g       8       b       h       u       v
 36  # 9     i       j       0       m       k       o       n
 37  # down  p       l       up      .       :       -       ,
 38  # left  *       ;       right   escape  =       +       /
 39  # 1     home    control 2       space   c=key   q       stop
 40  
 41  # Implement an FN-key for some keys not present on the default keyboard
 42  class FnState:
 43      def __init__(self):
 44          self.state = False
 45  
 46      def fn_event(self, event):
 47          self.state = event.pressed
 48  
 49      def fn_modify(self, keycode):
 50          if self.state:
 51              return self.mods.get(keycode, keycode)
 52          return keycode
 53  
 54      mods = {
 55              K.ONE: K.F1,
 56              K.TWO: K.F2,
 57              K.THREE: K.F3,
 58              K.FOUR: K.F4,
 59              K.FIVE: K.F5,
 60              K.SIX: K.F6,
 61              K.SEVEN: K.F7,
 62              K.EIGHT: K.F8,
 63              K.NINE: K.F9,
 64              K.ZERO: K.F10,
 65              K.F1: K.F11,
 66              K.F2: K.F12,
 67              K.UP_ARROW: K.PAGE_UP,
 68              K.DOWN_ARROW: K.PAGE_DOWN,
 69              K.LEFT_ARROW: K.HOME,
 70              K.RIGHT_ARROW: K.END,
 71              K.BACKSPACE: K.DELETE,
 72              K.F3: K.INSERT,
 73      }
 74  fn_state = FnState()
 75  
 76  K_FN = fn_state.fn_event
 77  
 78  # A tuple is special, it:
 79  # * Clears shift modifiers & pressed keys
 80  # * Presses the given sequence
 81  # * Releases all pressed keys
 82  # * Restores the original modifiers
 83  # It's mostly used to send a key that requires a shift keypress on a standard
 84  # keyboard (or which is mapped to a shifted key but requires that shift NOT
 85  # be pressed)
 86  #
 87  # A consequence of this is that the key will not repeat, even if it is held
 88  # down.  So for example in the positional mapping, shift-1 will repeat "!"
 89  # but shift-7 will not repeat "'" and shift-0 will not repeat "^".
 90  K_AT = (K.SHIFT, K.TWO)
 91  K_PLUS = (K.SHIFT, K.EQUALS)
 92  K_ASTERISK = (K.SHIFT, K.EIGHT)
 93  K_COLON = (K.SHIFT, K.SEMICOLON)
 94  
 95  # We need these mask values for the reasons discussed above
 96  MASK_LEFT_SHIFT = K.modifier_bit(K.LEFT_SHIFT)
 97  MASK_RIGHT_SHIFT = K.modifier_bit(K.RIGHT_SHIFT)
 98  MASK_ANY_SHIFT = (MASK_LEFT_SHIFT | MASK_RIGHT_SHIFT)
 99  
100  if POSITIONAL:
101      keycodes = [
102          K.BACKSPACE, K.ENTER, K.BACKSLASH, K.F8, K.F1, K.F2, K.F3, K_AT,
103          K.THREE, K.W, K.A, K.FOUR, K.Z, K.S, K.E, K.LEFT_SHIFT,
104          K.FIVE, K.R, K.D, K.SIX, K.C, K.F, K.T, K.X,
105          K.SEVEN, K.Y, K.G, K.EIGHT, K.B, K.H, K.U, K.V,
106          K.NINE, K.I, K.J, K.ZERO, K.M, K.K, K.O, K.N,
107          K.DOWN_ARROW, K.P, K.L, K.UP_ARROW, K.PERIOD, K_COLON, K.MINUS, K.COMMA,
108          K.LEFT_ARROW, K_ASTERISK, K.SEMICOLON, K.RIGHT_ARROW, K.ESCAPE, K.EQUALS, K_PLUS,
109          K.FORWARD_SLASH, K.ONE, K_FN, K.LEFT_CONTROL, K.TWO, K.SPACE, K.ALT, K.Q, K.GRAVE_ACCENT,
110      ]
111  
112      shifted = {
113              K.TWO: (K.SHIFT, K.QUOTE),  # double quote
114              K.SIX: (K.SHIFT, K.SEVEN),  # ampersand
115              K.SEVEN: (K.QUOTE,),        # single quote
116              K.EIGHT: (K.SHIFT, K.NINE), # left paren
117              K.NINE: (K.SHIFT, K.ZERO),  # right paren
118              K.ZERO: (K.SHIFT, K.SIX),  # caret
119              K_AT: (K.SHIFT, K.LEFT_BRACKET),
120              K_PLUS: (K.SHIFT, K.RIGHT_BRACKET),
121              K_COLON: (K.LEFT_BRACKET,),
122              K.SEMICOLON: (K.RIGHT_BRACKET,),
123              K.EQUALS: (K.TAB,),
124      }
125  else:
126      # TODO clear/home, up/down positional arrows
127      keycodes = [
128          K.BACKSPACE, K.ENTER, K.LEFT_ARROW, K.F8, K.F1, K.F2, K.F3, K.LEFT_BRACKET,
129          K.THREE, K.W, K.A, K.FOUR, K.Z, K.S, K.E, K.LEFT_SHIFT,
130          K.FIVE, K.R, K.D, K.SIX, K.C, K.F, K.T, K.X,
131          K.SEVEN, K.Y, K.G, K.EIGHT, K.B, K.H, K.U, K.V,
132          K.NINE, K.I, K.J, K.ZERO, K.M, K.K, K.O, K.N,
133          K.DOWN_ARROW, K.P, K.L, K.UP_ARROW, K.PERIOD, K.SEMICOLON, K.QUOTE, K.COMMA,
134          K.BACKSLASH, K_ASTERISK, K.SEMICOLON, K.EQUALS, K.ESCAPE, K.RIGHT_ARROW, K.RIGHT_BRACKET,
135          K.FORWARD_SLASH, K.ONE, K.HOME, K.LEFT_CONTROL, K.TWO, K.SPACE, K.ALT, K.Q, K.GRAVE_ACCENT,
136      ]
137  
138      shifted = {
139      }
140  class AsyncEventQueue:
141      def __init__(self, events):
142          self._events = events
143  
144      async def __await__(self):
145          yield asyncio.core._io_queue.queue_read(self._events)
146          return self._events.get()
147  
148      def __enter__(self):
149          return self
150  
151      def __exit__(self, exc_type, exc_value, traceback):
152          pass
153  
154  class XKROFilter:
155      """Perform an X-key rollover algorithm, blocking ghosts if more than X keys are pressed at once
156  
157  A key matrix without diodes can support 2-key rollover.
158      """
159      def __init__(self, rollover=2):
160          self._count = 0
161          self._rollover = rollover
162          self._real = [0] * 64
163          self._ghost = [0] * 64
164  
165      def __call__(self, event):
166          self._ghost[event.key_number] = event.pressed
167          if event.pressed:
168              if self._count < self._rollover:
169                  self._real[event.key_number] = True
170                  yield event
171              self._count += 1
172          else:
173              self._real[event.key_number] = False
174              yield event
175              self._count -= 1
176  
177  twokey_filter = XKROFilter(2)
178  
179  async def key_task():
180      # Initialize Keyboard
181      kbd = Keyboard(usb_hid.devices)
182  
183      with keypad.KeyMatrix(rows, cols) as keys, AsyncEventQueue(keys.events) as q:
184          while True:
185              ev = await q
186              for ev in twokey_filter(ev):
187                  keycode = keycodes[ev.key_number]
188                  if callable(keycode):
189                      keycode = keycode(ev)
190                  keycode = fn_state.fn_modify(keycode)
191                  if keycode is None:
192                      continue
193                  old_report_modifier = kbd.report_modifier[0]
194                  shift_pressed = old_report_modifier & MASK_ANY_SHIFT
195                  if shift_pressed:
196                      keycode = shifted.get(keycode, keycode)
197                  if isinstance(keycode, tuple):
198                      if ev.pressed:
199                          kbd.report_modifier[0] = old_report_modifier & ~MASK_ANY_SHIFT
200                          kbd.press(*keycode)
201                          kbd.release_all()
202                          kbd.report_modifier[0] = old_report_modifier
203                  elif ev.pressed:
204                      kbd.press(keycode)
205                  else:
206                      kbd.release(keycode)
207  
208  
209  async def forever_task():
210      while True:
211          await asyncio.sleep(.1)
212  
213  async def main():
214      forever = asyncio.create_task(forever_task())
215      key = asyncio.create_task(key_task())
216      await asyncio.gather(  # Don't forget the await!
217          forever,
218          key,
219      )
220  
221  asyncio.run(main())