from micropython import const from adafruit_mcp230xx.mcp23017 import MCP23017 from nmlkpy.keymap_manager import KeymapManager from .tests import test_keymap from .key_types.base import KeyBase from .pin import Pin import digitalio import busio import usb_hid import pwmio import time __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]) class LayerState: _NO_TOGGLED_LAYER = 0 _NO_HELD_LAYER = None toggled: int held: int def __init__(self): self.toggled = self._NO_TOGGLED_LAYER self.held = self._NO_HELD_LAYER def get(self) -> int: return self.held if self.held != self._NO_HELD_LAYER else self.toggled def toggle(self, toggled: int): self.toggled = toggled self.held = self._NO_HELD_LAYER def release_toggled(self): self.toggled = self._NO_TOGGLED_LAYER def hold(self, held): self.held = held def release_held(self): self.held = self._NO_HELD_LAYER def is_held(self): return self.held is not self._NO_HELD_LAYER class Keyboard: debug_repl: bool layer: LayerState layer_colors: list[tuple[int, int, int]] pressed_keys_last_cycle: set[int] pressed_keys: set[int] pins: list[Pin] keymap: list[list[KeyBase]] 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[KeyBase]], 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) self.keymap = self._initialize_keymap(keymap) self.layer_colors = layer_colors self.layer = LayerState() self.led = RGBLED(rgb_pins) if debug_repl: print("Done initializing keyboard") def _initialize_keymap(self, keymap: list[list[KeyBase]]) -> list[list[KeyBase]]: # keys cannot be enriched like that since they share state # for layer_index in range(len(keymap)): # layer = keymap[layer_index] # for key_index in range(len(layer)): # key = layer[key_index] # key.enrich(key_index, layer_index) test_keymap(keymap, True) return keymap 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[Pin]: pins = [] for pinout in pinouts: io_extender_index, pin_number = pinout io_extender = io_extenders[io_extender_index] pin_index = pinouts.index(pinout) pin = io_extender.get_pin(pin_number) pin.direction = digitalio.Direction.INPUT pin.pull = digitalio.Pull.UP pins.append(Pin(pin_index, pin)) return pins def _initialize_session_values(self): self.toggled_layer = 0 self.held_layer = None self.pressed_keys = [] self.pressed_keys_last_cycle = [] def get_active_key(self, layer_index, key_index) -> KeyBase: return self.keymap[layer_index][key_index] def get_inactive_keys(self, active_layer_index, key_index) -> list[KeyBase]: layers_excluding_active = list( range(len(self.keymap))) layers_excluding_active.remove(active_layer_index) return [self.keymap[layer][key_index] for layer in layers_excluding_active] def add_keycode_to_report(self, keycode: int): self.pressed_keys.append(keycode) def remove_keycode_from_report(self, keycode: int): try: self.pressed_keys.remove(keycode) except ValueError: # silently pass errors when same keycode has been issued twice pass def _update_layer_led(self, layer: int) -> None: if len(self.layer_colors) >= (layer + 1): self.led.set(self.layer_colors[layer]) else: self.led.off() # 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) def start(self): if self.debug_repl: print("Starting keyboard") self.led.indicate_boot() try: self._initialize_session_values() if self.debug_repl: print("Keyboard started") m = KeymapManager(self.keymap, self.pins) while True: keymap_report = m.step() if keymap_report.keymap_changes.contains_keycode_changes(): for keycode in keymap_report.keymap_changes.get_to_press(): self.add_keycode_to_report(keycode) for keycode in keymap_report.keymap_changes.get_to_unpress(): self.remove_keycode_from_report(keycode) self.send_nkro_report() self._update_layer_led(keymap_report.current_layer) except Exception as e: if self.debug_repl: raise e print(f"Exception thrown: {e}, restarting..") self.led.indicate_exception() time.sleep(1) self.start()