as on device
This commit is contained in:
parent
9da2b09f78
commit
223313d0f9
1 changed files with 220 additions and 0 deletions
220
CircuitPython_Commodore_16_KB2040/code.py
Normal file
220
CircuitPython_Commodore_16_KB2040/code.py
Normal file
|
|
@ -0,0 +1,220 @@
|
|||
# SPDX-FileCopyrightText: 2022 Jeff Epler for Adafruit Industries
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
# Commodore 16 to USB HID adapter with Adafruit KB2040
|
||||
#
|
||||
# Note that:
|
||||
# * This matrix is different than the (more common) Commodore 64 matrix
|
||||
# * There are no diodes, not even on modifiers, so there's only 2-key rollover.
|
||||
|
||||
from board import *
|
||||
import keypad
|
||||
import asyncio.core
|
||||
from adafruit_hid.keycode import Keycode as K
|
||||
from adafruit_hid.keyboard import Keyboard
|
||||
import usb_hid
|
||||
|
||||
# True to use a more POSITIONAL mapping, False to use a more PC-style mapping
|
||||
POSITIONAL = True
|
||||
|
||||
# Keyboard schematic https://archive.org/details/SAMS_Computerfacts_Commodore_C16_1984-12_Howard_W_Sams_Co_CC8/page/n9/mode/2up
|
||||
# 1 3 6 7 8 9 10 11 12 13 14 15 16 17 18 19 # connector pins
|
||||
# R5 C7 R7 C4 R1 C5 C6 R3 R2 R4 C2 C1 R6 C3 C0 R0 # row/column in schematic
|
||||
# D2 D3 D4 D5 D6 D7 D8 D9 D10 MOSI MISO SCK A0 A1 A2 A3 # conencted to kb2040 at
|
||||
rows = [A3, D6, D10, D9, MOSI, D2, A0, D4] # give the following ...
|
||||
cols = [A2, SCK, MISO, A1, D5, D7, D8, D3]
|
||||
|
||||
# ROM listing of key values from ed7.src in
|
||||
# http://www.zimmers.net/anonftp/pub/cbm/src/plus4/ted_kernal_basic_src.tar.gz
|
||||
# shows key matrix arrangement (it's nuts)
|
||||
# del return £ f8 f1 f2 f3 @
|
||||
# 3 w a 4 z s e shift
|
||||
# 5 r d 6 c f t x
|
||||
# 7 y g 8 b h u v
|
||||
# 9 i j 0 m k o n
|
||||
# down p l up . : - ,
|
||||
# left * ; right escape = + /
|
||||
# 1 home control 2 space c=key q stop
|
||||
|
||||
# Implement an FN-key for some keys not present on the default keyboard
|
||||
class FnState:
|
||||
def __init__(self):
|
||||
self.state = False
|
||||
|
||||
def fn_event(self, event):
|
||||
self.state = event.pressed
|
||||
|
||||
def fn_modify(self, keycode):
|
||||
if self.state:
|
||||
return self.mods.get(keycode, keycode)
|
||||
return keycode
|
||||
|
||||
mods = {
|
||||
K.ONE: K.F1,
|
||||
K.TWO: K.F2,
|
||||
K.THREE: K.F3,
|
||||
K.FOUR: K.F4,
|
||||
K.FIVE: K.F5,
|
||||
K.SIX: K.F6,
|
||||
K.SEVEN: K.F7,
|
||||
K.EIGHT: K.F8,
|
||||
K.NINE: K.F9,
|
||||
K.ZERO: K.F10,
|
||||
K.F1: K.F11,
|
||||
K.F2: K.F12,
|
||||
K.UP_ARROW: K.PAGE_UP,
|
||||
K.DOWN_ARROW: K.PAGE_DOWN,
|
||||
K.LEFT_ARROW: K.HOME,
|
||||
K.RIGHT_ARROW: K.END,
|
||||
K.BACKSPACE: K.DELETE,
|
||||
K.F3: K.INSERT,
|
||||
}
|
||||
fn_state = FnState()
|
||||
|
||||
K_FN = fn_state.fn_event
|
||||
|
||||
# A tuple is special, it:
|
||||
# * Clears shift modifiers & pressed keys
|
||||
# * Presses the given sequence
|
||||
# * Releases all pressed keys
|
||||
# * Restores the original modifiers
|
||||
# It's mostly used to send a key that requires a shift keypress on a standard
|
||||
# keyboard (or which is mapped to a shifted key but requires that shift NOT
|
||||
# be pressed)
|
||||
#
|
||||
# A consequence of this is that the key will not repeat, even if it is held
|
||||
# down. So for example in the positional mapping, shift-1 will repeat "!"
|
||||
# but shift-7 will not repeat "'" and shift-0 will not repeat "^".
|
||||
K_AT = (K.SHIFT, K.TWO)
|
||||
K_PLUS = (K.SHIFT, K.EQUALS)
|
||||
K_ASTERISK = (K.SHIFT, K.EIGHT)
|
||||
K_COLON = (K.SHIFT, K.SEMICOLON)
|
||||
|
||||
# We need these mask values for the reasons discussed above
|
||||
MASK_LEFT_SHIFT = K.modifier_bit(K.LEFT_SHIFT)
|
||||
MASK_RIGHT_SHIFT = K.modifier_bit(K.RIGHT_SHIFT)
|
||||
MASK_ANY_SHIFT = (MASK_LEFT_SHIFT | MASK_RIGHT_SHIFT)
|
||||
|
||||
if POSITIONAL:
|
||||
keycodes = [
|
||||
K.BACKSPACE, K.ENTER, K.BACKSLASH, K.F8, K.F1, K.F2, K.F3, K_AT,
|
||||
K.THREE, K.W, K.A, K.FOUR, K.Z, K.S, K.E, K.LEFT_SHIFT,
|
||||
K.FIVE, K.R, K.D, K.SIX, K.C, K.F, K.T, K.X,
|
||||
K.SEVEN, K.Y, K.G, K.EIGHT, K.B, K.H, K.U, K.V,
|
||||
K.NINE, K.I, K.J, K.ZERO, K.M, K.K, K.O, K.N,
|
||||
K.DOWN_ARROW, K.P, K.L, K.UP_ARROW, K.PERIOD, K_COLON, K.MINUS, K.COMMA,
|
||||
K.LEFT_ARROW, K_ASTERISK, K.SEMICOLON, K.RIGHT_ARROW, K.ESCAPE, K.EQUALS, K_PLUS,
|
||||
K.FORWARD_SLASH, K.ONE, K_FN, K.LEFT_CONTROL, K.TWO, K.SPACE, K.ALT, K.Q, K.GRAVE_ACCENT,
|
||||
]
|
||||
|
||||
shifted = {
|
||||
K.TWO: (K.SHIFT, K.QUOTE), # double quote
|
||||
K.SIX: (K.SHIFT, K.SEVEN), # ampersand
|
||||
K.SEVEN: (K.QUOTE,), # single quote
|
||||
K.EIGHT: (K.SHIFT, K.NINE), # left paren
|
||||
K.NINE: (K.SHIFT, K.ZERO), # right paren
|
||||
K.ZERO: (K.SHIFT, K.SIX), # caret
|
||||
K_AT: (K.SHIFT, K.LEFT_BRACKET),
|
||||
K_PLUS: (K.SHIFT, K.RIGHT_BRACKET),
|
||||
K_COLON: (K.LEFT_BRACKET,),
|
||||
K.SEMICOLON: (K.RIGHT_BRACKET,),
|
||||
K.EQUALS: (K.TAB,),
|
||||
}
|
||||
else:
|
||||
# TODO clear/home, up/down positional arrows
|
||||
keycodes = [
|
||||
K.BACKSPACE, K.ENTER, K.LEFT_ARROW, K.F8, K.F1, K.F2, K.F3, K.LEFT_BRACKET,
|
||||
K.THREE, K.W, K.A, K.FOUR, K.Z, K.S, K.E, K.LEFT_SHIFT,
|
||||
K.FIVE, K.R, K.D, K.SIX, K.C, K.F, K.T, K.X,
|
||||
K.SEVEN, K.Y, K.G, K.EIGHT, K.B, K.H, K.U, K.V,
|
||||
K.NINE, K.I, K.J, K.ZERO, K.M, K.K, K.O, K.N,
|
||||
K.DOWN_ARROW, K.P, K.L, K.UP_ARROW, K.PERIOD, K.SEMICOLON, K.QUOTE, K.COMMA,
|
||||
K.BACKSLASH, K_ASTERISK, K.SEMICOLON, K.EQUALS, K.ESCAPE, K.RIGHT_ARROW, K.RIGHT_BRACKET,
|
||||
K.FORWARD_SLASH, K.ONE, K.HOME, K.LEFT_CONTROL, K.TWO, K.SPACE, K.ALT, K.Q, K.GRAVE_ACCENT,
|
||||
]
|
||||
|
||||
shifted = {
|
||||
}
|
||||
class AsyncEventQueue:
|
||||
def __init__(self, events):
|
||||
self._events = events
|
||||
|
||||
async def __await__(self):
|
||||
yield asyncio.core._io_queue.queue_read(self._events)
|
||||
return self._events.get()
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
pass
|
||||
|
||||
class XKROFilter:
|
||||
"""Perform an X-key rollover algorithm, blocking ghosts if more than X keys are pressed at once
|
||||
|
||||
A key matrix without diodes can support 2-key rollover.
|
||||
"""
|
||||
def __init__(self, rollover=2):
|
||||
self._count = 0
|
||||
self._rollover = rollover
|
||||
self._real = [0] * 64
|
||||
self._ghost = [0] * 64
|
||||
|
||||
def __call__(self, event):
|
||||
old_count = self._count
|
||||
self._ghost[event.key_number] = event.pressed
|
||||
if event.pressed:
|
||||
if self._count < self._rollover:
|
||||
self._real[event.key_number] = True
|
||||
yield event
|
||||
self._count += 1
|
||||
else:
|
||||
self._real[event.key_number] = False
|
||||
yield event
|
||||
self._count -= 1
|
||||
|
||||
twokey_filter = XKROFilter(2)
|
||||
|
||||
async def key_task():
|
||||
# Initialize Keyboard
|
||||
kbd = Keyboard(usb_hid.devices)
|
||||
|
||||
with keypad.KeyMatrix(rows, cols) as keys, AsyncEventQueue(keys.events) as q:
|
||||
while True:
|
||||
ev = await q
|
||||
for ev in twokey_filter(ev):
|
||||
keycode = keycodes[ev.key_number]
|
||||
if callable(keycode):
|
||||
keycode = keycode(ev)
|
||||
keycode = fn_state.fn_modify(keycode)
|
||||
if keycode is None:
|
||||
continue
|
||||
old_report_modifier = kbd.report_modifier[0]
|
||||
shift_pressed = old_report_modifier & MASK_ANY_SHIFT
|
||||
if shift_pressed:
|
||||
keycode = shifted.get(keycode, keycode)
|
||||
if isinstance(keycode, tuple):
|
||||
if ev.pressed:
|
||||
kbd.report_modifier[0] = old_report_modifier & ~MASK_ANY_SHIFT
|
||||
kbd.press(*keycode)
|
||||
kbd.release_all()
|
||||
kbd.report_modifier[0] = old_report_modifier
|
||||
elif ev.pressed:
|
||||
kbd.press(keycode)
|
||||
else:
|
||||
kbd.release(keycode)
|
||||
|
||||
|
||||
async def forever_task():
|
||||
while True:
|
||||
await asyncio.sleep(.1)
|
||||
|
||||
async def main():
|
||||
forever = asyncio.create_task(forever_task())
|
||||
key = asyncio.create_task(key_task())
|
||||
await asyncio.gather( # Don't forget the await!
|
||||
forever,
|
||||
key,
|
||||
)
|
||||
|
||||
asyncio.run(main())
|
||||
Loading…
Reference in a new issue