Files
macropad/keyboard.py

277 lines
11 KiB
Python

from adafruit_mcp230xx.digital_inout import DigitalInOut
from adafruit_mcp230xx.mcp23017 import MCP23017
import digitalio
import busio
import usb_hid
import pwmio
import time
from micropython import const
class LayerKey:
layer: int
def __init__(self, layer:int):
self.layer = layer
class Toggle(LayerKey):
def __init__(self, layer:int):
super().__init__(layer)
class Hold(LayerKey):
def __init__(self, layer:int):
super().__init__(layer)
__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)
class Keyboard:
active_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[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[int], rgb_pins: tuple[int, int, int], layer_colors: list[tuple[int, int, int]]):
"""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.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 = keymap
self.layer_colors = layer_colors
self.led = RGBLED(rgb_pins)
self.led.indicate_boot()
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):
try:
self.active_layer = 0
self.previous_layers = []
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)
while True:
for pin in self.pins:
pin_index = self.pins.index(pin)
keycode = self.keymap[pin_index]
previously = self.pin_states_last_cycle[pin_index]
value = pin.value
currentlyPressed = value == __PRESSED
previouslyPressed = previously == __PRESSED
previouslyToggled = previously == __TOGGLED_PRESSED
previouslyToggledReleased = previously == __TOGGLED_RELEASED
previouslyUntoggledPressed = previously == __UNTOGGLED_PRESSED
if not isinstance(keycode, LayerKey):
if currentlyPressed:
if not previouslyPressed:
self.pressed_keys.add(keycode)
else:
if previouslyPressed:
try:
self.pressed_keys.remove(keycode)
# Catch silenly if same keycode is pressed twice then released
except KeyError:
pass
self.pin_states_last_cycle[pin_index] = value
continue
# TODO: Always release old key when entering new layer
if type(keycode) is Hold:
if currentlyPressed and not previouslyPressed:
self.previous_layers.append(self.active_layer)
self.active_layer = keycode.layer
print(f"hold: {self.previous_layers}")
self.pin_states_last_cycle[pin_index] = value
continue
if previouslyPressed and not currentlyPressed:
self.active_layer = self.previous_layers.pop()
self.pin_states_last_cycle[pin_index] = value
print(f"release: {self.previous_layers}")
continue
if type(keycode) is Toggle:
if currentlyPressed and not previouslyToggled and not previouslyToggledReleased and not previouslyUntoggledPressed:
self.previous_layers.append(self.active_layer)
self.active_layer = keycode.layer
print(f"Toggled: {self.previous_layers}")
self.pin_states_last_cycle[pin_index] = __TOGGLED_PRESSED
continue
if not currentlyPressed and previouslyToggled:
print(f"Toggled Released: {self.previous_layers}")
self.pin_states_last_cycle[pin_index] = __TOGGLED_RELEASED
continue
if currentlyPressed and previouslyToggledReleased:
print(f"Untoggled: {self.previous_layers}")
self.active_layer = self.previous_layers.pop()
self.pin_states_last_cycle[pin_index] = __UNTOGGLED_PRESSED
continue
if not currentlyPressed and previouslyUntoggledPressed:
print(f"Untoggled unpressed: {self.previous_layers}")
self.pin_states_last_cycle[pin_index] = __UNPRESSED
continue
if len(self.layer_colors) >= (self.active_layer + 1):
self.led.set(self.layer_colors[self.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:
print(f"Exception thrown: {e}, restarting..")
self.led.indicate_exception()
time.sleep(1)
self.led.indicate_boot()
self.start()
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
#