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 #