started to refactor key pin and layer functionality

This commit is contained in:
2022-01-15 22:03:10 +01:00
parent b4f61cf8e7
commit 09b8206742
8 changed files with 374 additions and 49 deletions

View File

@@ -1,4 +1,16 @@
from micropython import const
from nmlkpy import keymap_delta
from nmlkpy.keymap_delta import KeymapDelta, LayerDelta
from nmlkpy.pinstate_manager import PinState
class KeyDelta:
layer_delta: LayerDelta
keymap_delta: KeymapDelta
def __init__(self) -> None:
self.layer_delta = LayerDelta()
self.keymap_delta = KeymapDelta()
class KeyEvent:
@@ -42,8 +54,8 @@ class SoftRelease:
def __init__(self):
self._state = self._NOT_QUEUED
def is_not_queued(self) -> bool:
return self._state == self._NOT_QUEUED
def needs_attention(self) -> bool:
return self._state != self._NOT_QUEUED
def is_queued(self) -> bool:
return self._state == self._QUEUED
@@ -93,6 +105,9 @@ class KeyBase:
self.allowed_events = allowed_events
self.events = KeyEvents()
def get_delta(self, pin: PinState) -> KeyDelta:
raise NotImplementedError()
def _validate_next_event(self, next_event: int):
if next_event is None or next_event not in self.allowed_events:
raise Exception(
@@ -105,11 +120,11 @@ class KeyBase:
if next_event is KeyEvent.NO_EVENT:
return False
print(
f"[{index}] next: {_event_to_string(next_event)}, current: {_event_to_string(self.events.current)}, previous: {_event_to_string(self.events.previous)}")
f"[{index}] next: {_event_to_string(next_event)}, current: {_event_to_string(self.events.current)}, previous: {_event_to_string(self.events.previous)}, soft_release: {self.events.soft_release}")
self.events.shift(next_event)
return True
def handle(self, keyboard):
def handle(self, keyboard, is_active: bool = False):
raise NotImplementedError()
def self_test(self, keymap: list[list[object]]):

View File

@@ -1,5 +1,7 @@
from .base import KeyBase, KeyEvent
from .base import KeyBase, KeyDelta, KeyEvent
from ..pin import PinEvent
from ..pinstate_manager import PinState
from ..keymap_delta import KeymapDelta
_VALID_KEYCODES = range(0, 255)
_VALID_MODIFIERS = [0x0100, 0x0200, 0x0400, 0x0800, 0x1000, 0x2000, 0x4000]
@@ -8,6 +10,7 @@ _VALID_MODIFIERS = [0x0100, 0x0200, 0x0400, 0x0800, 0x1000, 0x2000, 0x4000]
class KeycodeBase(KeyBase):
"""Basic press and release functionality that appends a keycode to the report when active"""
keycode: int
currently_pressed: bool
def __init__(self, keycode: int):
allowed_events = [
@@ -17,12 +20,32 @@ class KeycodeBase(KeyBase):
]
super().__init__(allowed_events)
self.keycode = keycode
self.currently_pressed = False
def self_test(self, keymap):
raise NotImplementedError
def soft_release(self, keyboard):
self.events.soft_release.queue()
if self.currently_pressed:
self.events.soft_release.queue()
def _press(self, keyboard):
self.currently_pressed = True
keyboard.add_keycode_to_report(self.keycode)
def _release(self, keyboard):
self.currently_pressed = False
keyboard.remove_keycode_from_report(self.keycode)
def _handle_soft_release(self, keyboard):
if self.events.soft_release.is_queued():
if self.currently_pressed:
self._release(keyboard)
self._set_event(KeyEvent.RELEASED)
self.events.soft_release.reset()
return
raise NotImplementedError()
def _consume_next_event(self, pin) -> bool:
"""Returns: a boolean representing if the event property was updated with a new event or not"""
@@ -47,21 +70,30 @@ class KeycodeBase(KeyBase):
if self.events.soft_release.is_handled():
pass
if current == KeyEvent.PRESSED_:
keyboard.add_keycode_to_report(self.keycode)
self._press(keyboard)
return
if current == KeyEvent.RELEASED:
keyboard.remove_keycode_from_report(self.keycode)
return
if current == KeyEvent.SOFT_RELEASE_HANDLED:
keyboard.remove_keycode_from_report(self.keycode)
self._release(keyboard)
return
raise NotImplementedError()
def handle(self, keyboard, pin):
if not self._consume_next_event(pin):
def handle(self, keyboard, pin, is_active: bool = False):
new_event_available = self._consume_next_event(pin)
if self.events.soft_release.needs_attention():
self._handle_soft_release(keyboard)
return
self._handle_event(keyboard)
if new_event_available and is_active:
self._handle_event(keyboard)
def get_delta(self, pin: PinState) -> KeyDelta:
assert pin is not None
changes = KeyDelta()
if pin.is_pressed():
changes.keymap_delta.press(self.keycode)
else:
changes.keymap_delta.unpress(self.keycode)
return changes
class Keycode(KeycodeBase):

View File

@@ -1,4 +1,6 @@
from .base import KeyEvent, KeyBase
from ..keymap_delta import KeymapDelta, LayerChange
from ..pinstate_manager import PinState
from .base import KeyDelta, KeyEvent, KeyBase
from ..pin import PinEvent, Pin
@@ -39,9 +41,28 @@ class Toggle(LayerKeyBase):
]
super().__init__(layer, allowed_events)
def _press(self, keyboard):
keyboard.layer.toggle(self.layer_to_switch_to)
self._release_all_other_keys(keyboard)
def _release(self, keyboard):
keyboard.layer.release_toggled()
self._release_all_other_keys(keyboard)
def soft_release(self, keyboard):
self.events.soft_release.queue()
def _handle_soft_release(self, keyboard):
if self.events.soft_release.is_queued():
# leads to recursion
self._release(keyboard)
self.events.soft_release.mark_as_handled()
return
if self.events.soft_release.is_handled():
self.events.soft_release.reset()
return
raise NotImplementedError()
def _consume_next_event(self, pin: Pin) -> bool:
"""Returns: a boolean representing if the event property was updated with a new event or not"""
next_pin_event = pin.read_event()
@@ -88,13 +109,17 @@ class Toggle(LayerKeyBase):
raise NotImplementedError()
def handle(self, keyboard, pin: Pin):
if not self._consume_next_event(pin):
def handle(self, keyboard, pin: Pin, is_active: bool = False):
if self.events.soft_release.needs_attention():
self._handle_soft_release(keyboard)
return
self._handle_event(keyboard, pin)
if self._consume_next_event(pin):
self._handle_event(keyboard, pin)
class Hold(LayerKeyBase):
currently_pressed: bool
def __init__(self, layer: int):
allowed_events = [
KeyEvent.NO_EVENT,
@@ -102,14 +127,28 @@ class Hold(LayerKeyBase):
KeyEvent.RELEASED
]
super().__init__(layer, allowed_events)
self.currently_pressed = False
def soft_release(self, keyboard):
self.events.soft_release.queue()
if self.currently_pressed:
self.events.soft_release.queue()
def handle(self, keyboard, pin):
if not self._consume_next_event(pin):
def _handle_soft_release(self, keyboard):
if self.events.soft_release.is_queued():
if self.currently_pressed:
self._release(keyboard)
self._set_event(KeyEvent.RELEASED)
self.events.soft_release.reset()
return
self._handle_event(keyboard)
raise NotImplementedError()
def handle(self, keyboard, pin, is_active: bool = False):
new_event_available = self._consume_next_event(pin)
if self.events.soft_release.needs_attention():
self._handle_soft_release(keyboard)
return
if new_event_available and is_active:
self._handle_event(keyboard)
def _consume_next_event(self, pin: Pin) -> bool:
next_pin_event = pin.read_event()
@@ -129,12 +168,30 @@ class Hold(LayerKeyBase):
current = self.events.current
if current == KeyEvent.PRESSED_:
keyboard.layer.hold(self.layer_to_switch_to)
self._release_all_other_non_toggle_keys(keyboard)
self._press(keyboard)
return
if current == KeyEvent.RELEASED:
keyboard.layer.release_held()
self._release_all_other_non_toggle_keys(keyboard)
self._release(keyboard)
return
raise NotImplementedError()
def _press(self, keyboard):
self.currently_pressed = True
keyboard.layer.hold(self.layer_to_switch_to)
self._release_all_other_non_toggle_keys(keyboard)
def _release(self, keyboard):
self.currently_pressed = False
keyboard.layer.release_held()
self._release_all_other_non_toggle_keys(keyboard)
def get_delta(self, pin: PinState) -> KeyDelta:
changes = KeyDelta()
if pin.is_pressed():
changes.layer_delta.apply_layer(
LayerChange(self.layer_to_switch_to))
else:
changes.layer_delta.remove_layer(
LayerChange(self.layer_to_switch_to))
return changes

View File

@@ -1,6 +1,7 @@
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
@@ -204,14 +205,20 @@ class Keyboard:
def _initialize_session_values(self):
self.toggled_layer = 0
self.held_layer = None
self.pressed_keys = set()
self.pressed_keys_last_cycle = set()
self.pressed_keys = []
self.pressed_keys_last_cycle = []
def get_key(self, layer_index, key_index) -> KeyBase:
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.add(keycode)
self.pressed_keys.append(keycode)
def remove_keycode_from_report(self, keycode: int):
try:
@@ -251,15 +258,17 @@ class Keyboard:
self._initialize_session_values()
if self.debug_repl:
print("Keyboard started")
m = KeymapManager(self.keymap, self.pins)
while True:
for pin in self.pins:
key = self.get_key(self.layer.get(), pin.index)
key.handle(self, pin)
self._update_layer_led()
if self.pressed_keys != self.pressed_keys_last_cycle:
keymap_delta = m.step()
if keymap_delta.contains_keycode_changes():
for keycode in keymap_delta.get_to_press():
self.add_keycode_to_report(keycode)
for keycode in keymap_delta.get_to_unpress():
self.remove_keycode_from_report(keycode)
self.send_nkro_report()
self.pressed_keys_last_cycle = set(self.pressed_keys)
self._update_layer_led()
except Exception as e:
if self.debug_repl:
raise e

86
nmlkpy/keymap_delta.py Normal file
View File

@@ -0,0 +1,86 @@
class KeyIdentifier:
_layer_index: int
_key_index: int
def __init__(self, layer_index: int, key_index: int) -> None:
assert layer_index is not None
assert key_index is not None
self._layer_index = layer_index
self._key_index = key_index
class LayerChange:
_issued_by: KeyIdentifier
_to_layer: int
_is_toggle: bool
def __init__(self, to_layer: int, is_toggle: bool = False) -> None:
assert to_layer is not None
self._issued_by = None
self._to_layer = to_layer
self._is_toggle = is_toggle
def get_layer(self) -> int:
return self._to_layer
class LayerDelta:
_layers_to_apply: list[LayerChange]
_layers_to_remove: list[LayerChange]
def __init__(self) -> None:
self._layers_to_remove = []
self._layers_to_apply = []
def contains_layer_change(self) -> bool:
return len(self._layers_to_apply) + len(self._layers_to_remove) > 0
def get_layers_to_apply(self) -> list[LayerChange]:
return self._layers_to_apply
def get_layers_to_remove(self) -> list[LayerChange]:
return self._layers_to_remove
def apply_layer(self, layer_change: LayerChange) -> None:
assert layer_change is not None
self._layers_to_apply.append(layer_change)
def remove_layer(self, layer_change: LayerChange) -> None:
assert layer_change is not None
self._layers_to_remove.append(layer_change)
def merge_deltas(self, other_delta) -> None:
for layer in other_delta.get_layers_to_apply():
self.apply_layer(layer)
for layer in other_delta.get_layers_to_remove():
self.remove_layer(layer)
class KeymapDelta:
_keycodes_to_press: list[int]
_keycodes_to_unpress: list[int]
def __init__(self) -> None:
self._keycodes_to_press = []
self._keycodes_to_unpress = []
def get_to_press(self) -> list[int]:
return self._keycodes_to_press
def get_to_unpress(self) -> list[int]:
return self._keycodes_to_unpress
def contains_keycode_changes(self) -> bool:
return len(self._keycodes_to_unpress) + len(self._keycodes_to_press) > 0
def press(self, keycode: int) -> None:
self._keycodes_to_press.append(keycode)
def unpress(self, keycode: int) -> None:
self._keycodes_to_unpress.append(keycode)
def merge_deltas(self, other_delta) -> None:
for keycode in other_delta.get_to_press():
self.press(keycode)
for keycode in other_delta.get_to_unpress():
self.unpress(keycode)

85
nmlkpy/keymap_manager.py Normal file
View File

@@ -0,0 +1,85 @@
from nmlkpy.key_types.layer import Hold
from .keymap_delta import KeymapDelta, LayerChange, LayerDelta
from .pinstate_manager import PinState, PinManager
from .key_types.base import KeyBase
from .key_types.keycode import KeycodeBase
from .pin import Pin
from micropython import const
from nmlkpy import keymap_delta
# Needs to be built in a way where every "key type" only has to know how to modify it's own state
# it cannot be passed a pin or keyboard instance
# this is to help keep order in what can access what
# layer manager should be able to figure out what needs to happen to the state after a loop is run.
#
# Keys
# Keys have one way communication from KeymapManager to key and are allowed to return a result
# all keys changes together create a delta that is handled by the manager
# the manager defines an interface for changes that can happen to the state
# - Pressed
# - unpressed
# - Layer was changed
# When a delta is applied the manager can figure out what to do for that state index
# state
# Keys state is stored and managed in the manager
# One single layer of state removes the issue of when layers are changing
# each state is a class that can be consumed from different aspects, keycode, toggle, hold etc
# Where the state presents itself in the way that is asked
# Pins
# Can be asked for their current event
# Pins state is a class that holds current and last, can give you the delta to figure out what to change
_LAYERS_NONE = 0
class LayerManager:
number_of_layers: int = None
current_layer: int = None
def __init__(self, number_of_layers: int) -> None:
assert number_of_layers is not None
assert number_of_layers > 0
self.number_of_layers = number_of_layers
self.current_layer = _LAYERS_NONE
def get_current_layer(self) -> int:
return self.current_layer
def handle_layer_changes(self, layer):
pass
class KeymapManager:
keymap: list[list[KeyBase]] = None
pin_manager: PinManager = None
layer_manager: LayerManager = None
def __init__(self, keymap: list[list[KeyBase]], pins: list[Pin]):
self.keymap = keymap
self.pin_manager = PinManager(pins)
self.layer_manager = LayerManager(len(keymap))
def step(self) -> KeymapDelta:
self.pin_manager.step()
pin_changes = self.pin_manager.get_state_delta()
keymap_changes = KeymapDelta()
layer_changes = LayerDelta()
layer_index = self.layer_manager.get_current_layer()
for pin_index in range(len(pin_changes)):
pin_state = pin_changes[pin_index]
if pin_state is None:
continue
key = self.keymap[layer_index][pin_index]
if isinstance(key, KeycodeBase) or isinstance(key, Hold):
key_changes = key.get_delta(pin_state)
keymap_changes.merge_deltas(key_changes.keymap_delta)
layer_changes.merge_deltas(key_changes.layer_delta)
return keymap_changes

View File

@@ -25,7 +25,6 @@ class PinStates:
class Pin:
index: int
hw_pin: digitalio.DigitalInOut
previous_state: int
def __init__(self, index, hw_pin: digitalio.DigitalInOut):
self.index = index
@@ -38,15 +37,7 @@ class Pin:
return states
def read_event(self) -> int:
states = self._consume_next_state()
if states.previous == PinState.RELEASED:
if states.current == PinState.RELEASED:
return PinEvent.NO_EVENT
if states.current == PinState.PRESSED:
return PinEvent.PRESSED
if states.previous == PinState.PRESSED:
if states.current == PinState.PRESSED:
return PinEvent.NO_EVENT
if states.current == PinState.RELEASED:
return PinEvent.RELEASED
raise NotImplementedError()
def get_value(self) -> int:
return self.hw_pin.value

View File

@@ -0,0 +1,50 @@
from micropython import const
from .pin import Pin
_PINSTATE_PRESSED = const(0)
_PINSTATE_UNPRESSED = const(1)
class PinState:
_state: int
def __init__(self, pin_value: int) -> None:
self._state = _PINSTATE_PRESSED if pin_value == _PINSTATE_PRESSED else _PINSTATE_UNPRESSED
def __eq__(self, obj):
return isinstance(obj, PinState) and self._state == obj._state
def __ne__(self, obj):
return not isinstance(obj, PinState) or self._state != obj._state
def is_pressed(self) -> bool:
return self._state == _PINSTATE_PRESSED
class PinManager:
_pins: list[Pin] = []
_current_state: list[PinState] = []
_previous_state: list[PinState] = []
def __init__(self, pins: list[Pin]) -> None:
self._pins = pins
self.step()
self.step()
def _get_next_state(self) -> list[PinState]:
return [PinState(pin.get_value()) for pin in self._pins]
def step(self) -> None:
self._previous_state = self._current_state
self._current_state = self._get_next_state()
def get_state_delta(self) -> list[PinState]:
"""Compares the current state with the previous and returns a list of either PinState or None depending on if the state changed or not"""
delta = []
for i in range(len(self._pins)):
previous_state = self._previous_state[i]
current_state = self._current_state[i]
delta.append(current_state if current_state !=
previous_state else None)
return delta