319 lines
13 KiB
Python
319 lines
13 KiB
Python
from adafruit_mcp230xx.digital_inout import DigitalInOut
|
|
from adafruit_mcp230xx.mcp23017 import MCP23017
|
|
import digitalio
|
|
import busio
|
|
from tests import test_keymap
|
|
import usb_hid
|
|
import pwmio
|
|
import time
|
|
from micropython import const
|
|
from keytypes import LayerKey, Toggle, Hold
|
|
|
|
|
|
__RED = (255, 0, 0)
|
|
__BLUE = (0, 0, 255)
|
|
__GREEN = (0, 255, 0)
|
|
__WHITE = (255, 255, 255)
|
|
__OFF = (0, 0, 0)
|
|
__BLINK = (0.05, 0, 0)
|
|
__INVERT_8_BIT_INTEGER_BITMASK = const(0xffff)
|
|
__DUTY_CYCLE_OFF = __INVERT_8_BIT_INTEGER_BITMASK
|
|
|
|
|
|
class RGBLED:
|
|
leds: list[pwmio.PWMOut, pwmio.PWMOut, pwmio.PWMOut]
|
|
|
|
def __init__(self, rgb_pins: tuple[int, int, int]):
|
|
self.leds = []
|
|
for i in range(len(rgb_pins)):
|
|
pin = rgb_pins[i]
|
|
led = pwmio.PWMOut(pin)
|
|
led.duty_cycle = __DUTY_CYCLE_OFF
|
|
self.leds.append(led)
|
|
|
|
def set(self, rgb_values: tuple[int, int, int]):
|
|
for i in range(3):
|
|
input = rgb_values[i]
|
|
assert 0 <= input <= 255
|
|
value = self.__to_inverse_8_bit_value(rgb_values[i])
|
|
self.leds[i].duty_cycle = value
|
|
|
|
def off(self):
|
|
self.set(__OFF)
|
|
|
|
def __to_inverse_8_bit_value(self, value: int) -> int:
|
|
return ~(value * 257) & __INVERT_8_BIT_INTEGER_BITMASK
|
|
|
|
def indicate_exception(self) -> None:
|
|
self.animate(
|
|
__RED,
|
|
__BLINK,
|
|
__OFF,
|
|
__BLINK,
|
|
__RED,
|
|
__BLINK,
|
|
__OFF,
|
|
__BLINK,
|
|
__RED,
|
|
__BLINK,
|
|
__OFF)
|
|
|
|
def indicate_boot(self) -> None:
|
|
self.animate(__WHITE, __BLINK, __OFF)
|
|
|
|
def animate(self, *color_sleep_cycles: tuple[int, int, int]) -> None:
|
|
"""Takes arguments of tuple with three int values.
|
|
Argument tuples are in the structure of 'color to display' and 'time to wait' after each other
|
|
example: animate((255,255,255),(0.05,0,0),(0,0,0)) - will blink the led white for 0.05 seconds
|
|
"""
|
|
for i in range(len(color_sleep_cycles)):
|
|
if not i % 2:
|
|
self.set(color_sleep_cycles[i])
|
|
else:
|
|
time.sleep(color_sleep_cycles[i][0])
|
|
|
|
|
|
__PRESSED = const(0)
|
|
__UNPRESSED = const(1)
|
|
__TOGGLED_PRESSED = const(2)
|
|
__TOGGLED_RELEASED = const(3)
|
|
__UNTOGGLED_PRESSED = const(4)
|
|
__DEBOUNCE = const(5)
|
|
|
|
|
|
class Keyboard:
|
|
debug_repl: bool
|
|
|
|
toggled_layer: int
|
|
held_layer: int
|
|
previous_layers: list[int]
|
|
layer_colors: list[tuple[int, int, int]]
|
|
|
|
pressed_keys_last_cycle: set[int]
|
|
pressed_keys: set[int]
|
|
|
|
pins: list[digitalio.DigitalInOut]
|
|
pin_states_last_cycle: list[int]
|
|
|
|
keymap: list[list[int]]
|
|
|
|
keyboard_device: usb_hid.Device
|
|
led: RGBLED
|
|
|
|
def __init__(
|
|
self,
|
|
io_extenders_pinout: list[tuple[int, int, int]],
|
|
pinout: list[tuple[int, int]],
|
|
keymap: list[list[int]],
|
|
rgb_pins: tuple[int, int, int],
|
|
layer_colors: list[tuple[int, int, int]],
|
|
debug_repl: bool = False):
|
|
"""Initialize new keyboard
|
|
|
|
'io_extenders_pinout': list of tuple containing the i2c address, clock pin and data pin of the device. The order of the extenders are decided by the order in the list.
|
|
'pinout': list of tuple containing the index of the io_extender provided in 'io_extenders_pinout' and the pin of that extender
|
|
'keymap': list of keycodes that will be mapped against the pinout provided
|
|
'rgb_pins': tuple of three values in the inclusive range of 0-255 deciding the brightness of the red, green and blue leds"""
|
|
|
|
self.debug_repl = debug_repl
|
|
if debug_repl:
|
|
print("Initializing keyboard firmware")
|
|
|
|
self.keyboard_device, self.media_device = self.initialize_hid()
|
|
io_extenders = self.initialize_io_extenders(io_extenders_pinout)
|
|
self.pins = self.initialize_pins(io_extenders, pinout)
|
|
|
|
test_keymap(keymap, debug_repl)
|
|
self.keymap = keymap
|
|
self.layer_colors = layer_colors
|
|
|
|
self.led = RGBLED(rgb_pins)
|
|
self.led.indicate_boot()
|
|
if debug_repl:
|
|
print("Done initializing keyboard")
|
|
|
|
def initialize_hid(self) -> tuple[usb_hid.Device, usb_hid.Device]:
|
|
"""Initializes keyboard and media device if availabe"""
|
|
for device in usb_hid.devices:
|
|
if device.usage == 0x06 and device.usage_page == 0x01:
|
|
keyboard_device = device
|
|
try:
|
|
device.send_report(b'\0' * 16)
|
|
except ValueError:
|
|
print(
|
|
"found keyboard, but it did not accept a 16-byte report. check that boot.py is installed properly")
|
|
if device.usage == 0x01 and device.usage_page == 0x0c:
|
|
media_device = device
|
|
if not keyboard_device:
|
|
raise RuntimeError("No keyboard device was found")
|
|
return (keyboard_device, media_device)
|
|
|
|
def initialize_io_extenders(self, io_extenders_pinout: list[tuple[int, int, int]]) -> list[MCP23017]:
|
|
io_extenders = []
|
|
for pinout in io_extenders_pinout:
|
|
address, clock_pin, data_pin = pinout
|
|
extender = MCP23017(busio.I2C(clock_pin, data_pin), address)
|
|
io_extenders.append(extender)
|
|
if not len(io_extenders):
|
|
raise ValueError("No io extenders were initialized")
|
|
return io_extenders
|
|
|
|
def initialize_pins(self, io_extenders: list[MCP23017], pinouts: list[tuple[int, int]]) -> list[DigitalInOut]:
|
|
pins = []
|
|
for pinout in pinouts:
|
|
io_extender_index, pin_number = pinout
|
|
io_extender = io_extenders[io_extender_index]
|
|
pin = io_extender.get_pin(pin_number)
|
|
pin.direction = digitalio.Direction.INPUT
|
|
pin.pull = digitalio.Pull.UP
|
|
pins.append(pin)
|
|
return pins
|
|
|
|
def start(self):
|
|
if self.debug_repl:
|
|
print("Starting keyboard")
|
|
try:
|
|
self.toggled_layer = 0
|
|
self.held_layer = None
|
|
self.pressed_keys = set()
|
|
self.pressed_keys_last_cycle = set()
|
|
self.pin_states_last_cycle = []
|
|
for pin in self.pins:
|
|
self.pin_states_last_cycle.append((__UNPRESSED,))
|
|
if self.debug_repl:
|
|
print("Keyboard started")
|
|
while True:
|
|
for pin in self.pins:
|
|
pin_index = self.pins.index(pin)
|
|
key = self.keymap[self.held_layer if self.held_layer !=
|
|
None else self.toggled_layer][pin_index]
|
|
previousValue = self.pin_states_last_cycle[pin_index][0]
|
|
value = pin.value
|
|
currentlyPressed = value == __PRESSED
|
|
previouslyPressed = previousValue == __PRESSED
|
|
previouslyToggled = previousValue == __TOGGLED_PRESSED
|
|
previouslyToggledReleased = previousValue == __TOGGLED_RELEASED
|
|
previouslyUntoggledPressed = previousValue == __UNTOGGLED_PRESSED
|
|
previouslyDebounced = previousValue == __DEBOUNCE
|
|
if not isinstance(key, LayerKey):
|
|
if currentlyPressed:
|
|
if not previouslyPressed:
|
|
self.pressed_keys.add(key.keycode)
|
|
else:
|
|
if previouslyPressed:
|
|
try:
|
|
self.pressed_keys.remove(key.keycode)
|
|
# Catch silenly if same keycode is pressed twice then released
|
|
except KeyError:
|
|
pass
|
|
self.pin_states_last_cycle[pin_index] = (value,)
|
|
continue
|
|
# todo: Release all keys not the same as the old layer
|
|
if type(key) is Hold:
|
|
if not currentlyPressed and previouslyDebounced:
|
|
self.pin_states_last_cycle[pin_index] = (
|
|
__UNPRESSED,)
|
|
|
|
if currentlyPressed and not previouslyPressed and not previouslyDebounced:
|
|
self.held_layer = key.layer
|
|
self.pin_states_last_cycle[pin_index] = (
|
|
__PRESSED, key)
|
|
continue
|
|
if previouslyPressed and not currentlyPressed:
|
|
self.held_layer = None
|
|
self.pin_states_last_cycle[pin_index] = (
|
|
__UNPRESSED,)
|
|
continue
|
|
# todo: Release all keys not the same as the old layer
|
|
# todo: Debounce old toggle when pressing new toggle
|
|
if type(key) is Toggle:
|
|
if currentlyPressed and not previouslyToggled and not previouslyToggledReleased and not previouslyUntoggledPressed:
|
|
self.toggled_layer = key.layer
|
|
self.held_layer = None
|
|
self.pin_states_last_cycle[pin_index] = (
|
|
__TOGGLED_PRESSED, key)
|
|
for i in range(len(self.pin_states_last_cycle)):
|
|
if i == pin_index or len(self.pin_states_last_cycle[i]) == 1:
|
|
continue
|
|
value, last_keycode = self.pin_states_last_cycle[i]
|
|
if type(last_keycode) is Toggle:
|
|
self.pin_states_last_cycle[i] = (
|
|
__UNPRESSED,)
|
|
if type(last_keycode) is Hold:
|
|
self.held_layer = None
|
|
self.pin_states_last_cycle[i] = (
|
|
__DEBOUNCE,)
|
|
continue
|
|
if not currentlyPressed and previouslyToggled:
|
|
self.pin_states_last_cycle[pin_index] = (
|
|
__TOGGLED_RELEASED, key)
|
|
continue
|
|
if currentlyPressed and previouslyToggledReleased:
|
|
self.toggled_layer = 0
|
|
self.pin_states_last_cycle[pin_index] = (
|
|
__UNTOGGLED_PRESSED, key)
|
|
continue
|
|
if not currentlyPressed and previouslyUntoggledPressed:
|
|
self.pin_states_last_cycle[pin_index] = (
|
|
__UNPRESSED,)
|
|
continue
|
|
active_layer = self.held_layer if self.held_layer != None else self.toggled_layer
|
|
if len(self.layer_colors) >= (active_layer + 1):
|
|
self.led.set(self.layer_colors[active_layer])
|
|
else:
|
|
self.led.off()
|
|
if self.pressed_keys != self.pressed_keys_last_cycle:
|
|
self.send_nkro_report()
|
|
self.pressed_keys_last_cycle = set(self.pressed_keys)
|
|
except Exception as e:
|
|
if self.debug_repl:
|
|
raise e
|
|
print(f"Exception thrown: {e}, restarting..")
|
|
self.led.indicate_exception()
|
|
time.sleep(1)
|
|
self.led.indicate_boot()
|
|
self.start()
|
|
|
|
# todo: add boot mode
|
|
def send_nkro_report(self):
|
|
"""Sends the USB HID NKRO keyboard report."""
|
|
|
|
report = bytearray(16)
|
|
report_mod_keys = memoryview(report)[0:1]
|
|
report_bitmap = memoryview(report)[1:]
|
|
for code in self.pressed_keys:
|
|
if code == 0:
|
|
continue
|
|
if code & 0xff00:
|
|
report_mod_keys[0] |= (code & 0xff00) >> 8
|
|
if code & 0x00ff:
|
|
report_bitmap[code >> 3] |= 1 << (code & 0x7)
|
|
self.keyboard_device.send_report(report)
|
|
|
|
# Create interface for key of different type
|
|
# interface has method to handle keypress and release and gets access to key keyboards state and keyboard instance
|
|
# keyboard state should be increased with layer
|
|
# layer keys take a type, a set layer, and a color combination
|
|
|
|
# class LayerKeyType(Enum):
|
|
# hold = 1
|
|
# toggle = 2
|
|
|
|
# def handle_key(raw_keymap:list[list]]):
|
|
# for row in raw_keymap:
|
|
# for key in row:
|
|
# if key in Keycode:
|
|
# # handle as keycode
|
|
# pass
|
|
# if key.
|
|
|
|
|
|
# class LayerKey:
|
|
# type: LayerKeyType
|
|
|
|
# def __init__(self, type: LayerKeyType, layer: int):
|
|
# class KeycodeKey:
|
|
# def __init__(self):
|
|
# pass
|
|
#
|