|
| 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()) |
0 commit comments