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