diff --git a/kmk/common/abstract/matrix_scanner.py b/kmk/common/abstract/matrix_scanner.py deleted file mode 100644 index 6f324b7..0000000 --- a/kmk/common/abstract/matrix_scanner.py +++ /dev/null @@ -1,9 +0,0 @@ -from kmk.common.consts import DiodeOrientation - - -class AbstractMatrixScanner(): - def __init__(self, cols, rows, active_layers, diode_orientation=DiodeOrientation.COLUMNS): - raise NotImplementedError('Abstract implementation') - - def scan_for_pressed(self): - raise NotImplementedError('Abstract implementation') diff --git a/kmk/common/consts.py b/kmk/common/consts.py index 426d488..080379a 100644 --- a/kmk/common/consts.py +++ b/kmk/common/consts.py @@ -1,3 +1,7 @@ +CIRCUITPYTHON = 'CircuitPython' +MICROPYTHON = 'MicroPython' + + class HIDReportTypes: KEYBOARD = 1 MOUSE = 2 diff --git a/kmk/common/macros/rotary_encoder.py b/kmk/common/macros/rotary_encoder.py new file mode 100644 index 0000000..c3bb65d --- /dev/null +++ b/kmk/common/macros/rotary_encoder.py @@ -0,0 +1,76 @@ +import math + +from kmk.common.event_defs import (hid_report_event, keycode_down_event, + keycode_up_event) +from kmk.common.keycodes import Media +from kmk.common.rotary_encoder import RotaryEncoder + +VAL_FALSE = False + 1 +VAL_NONE = True + 2 +VAL_TRUE = True + 1 +VOL_UP_PRESS = keycode_down_event(Media.KC_AUDIO_VOL_UP) +VOL_UP_RELEASE = keycode_up_event(Media.KC_AUDIO_VOL_UP) +VOL_DOWN_PRESS = keycode_down_event(Media.KC_AUDIO_VOL_DOWN) +VOL_DOWN_RELEASE = keycode_up_event(Media.KC_AUDIO_VOL_DOWN) + + +class RotaryEncoderMacro: + def __init__(self, pos_pin, neg_pin, slop_history=1, slop_threshold=1): + self.encoder = RotaryEncoder(pos_pin, neg_pin) + self.max_history = slop_history + self.history = bytearray(slop_history) + self.history_idx = 0 + self.history_threshold = math.floor(slop_threshold * slop_history) + + def scan(self): + # Anti-slop logic + self.history[self.history_idx] = 0 + + reading = self.encoder.direction() + self.history[self.history_idx] = VAL_NONE if reading is None else reading + 1 + + self.history_idx += 1 + + if self.history_idx >= self.max_history: + self.history_idx = 0 + + nones = 0 + trues = 0 + falses = 0 + + for val in self.history: + if val == VAL_NONE: + nones += 1 + elif val == VAL_TRUE: + trues += 1 + elif val == VAL_FALSE: + falses += 1 + + if nones >= self.history_threshold: + return None + + if trues >= self.history_threshold: + return self.on_increase() + + if falses >= self.history_threshold: + return self.on_decrease() + + def on_decrease(self): + pass + + def on_increase(self): + pass + + +class VolumeRotaryEncoder(RotaryEncoderMacro): + def on_decrease(self): + yield VOL_DOWN_PRESS + yield hid_report_event + yield VOL_DOWN_RELEASE + yield hid_report_event + + def on_increase(self): + yield VOL_UP_PRESS + yield hid_report_event + yield VOL_UP_RELEASE + yield hid_report_event diff --git a/kmk/circuitpython/matrix.py b/kmk/common/matrix.py similarity index 84% rename from kmk/circuitpython/matrix.py rename to kmk/common/matrix.py index 3c1b458..297002b 100644 --- a/kmk/circuitpython/matrix.py +++ b/kmk/common/matrix.py @@ -1,11 +1,10 @@ import digitalio -from kmk.common.abstract.matrix_scanner import AbstractMatrixScanner from kmk.common.consts import DiodeOrientation from kmk.common.event_defs import matrix_changed -class MatrixScanner(AbstractMatrixScanner): +class MatrixScanner: def __init__(self, cols, rows, diode_orientation=DiodeOrientation.COLUMNS): # A pin cannot be both a row and column, detect this by combining the # two tuples into a set and validating that the length did not drop @@ -15,8 +14,8 @@ class MatrixScanner(AbstractMatrixScanner): if len(unique_pins) != len(cols) + len(rows): raise ValueError('Cannot use a pin as both a column and row') - self.cols = [digitalio.DigitalInOut(pin) for pin in cols] - self.rows = [digitalio.DigitalInOut(pin) for pin in rows] + self.cols = cols + self.rows = rows self.diode_orientation = diode_orientation self.last_pressed_len = 0 @@ -41,15 +40,15 @@ class MatrixScanner(AbstractMatrixScanner): pressed = [] for oidx, opin in enumerate(self.outputs): - opin.value = True + opin.value(True) for iidx, ipin in enumerate(self.inputs): - if ipin.value: + if ipin.value(): pressed.append( (oidx, iidx) if self.diode_orientation == DiodeOrientation.ROWS else (iidx, oidx) # noqa ) - opin.value = False + opin.value(False) if len(pressed) != self.last_pressed_len: self.last_pressed_len = len(pressed) diff --git a/kmk/common/pins.py b/kmk/common/pins.py index b469a5a..18974b1 100644 --- a/kmk/common/pins.py +++ b/kmk/common/pins.py @@ -1,18 +1,22 @@ +from micropython import const + +from kmk.common.consts import CIRCUITPYTHON, MICROPYTHON + +PULL_UP = const(1) +PULL_DOWN = const(2) + + try: import board + import digitalio - PLATFORM = 'CircuitPython' + PLATFORM = CIRCUITPYTHON PIN_SOURCE = board except ImportError: import machine - PLATFORM = 'MicroPython' + PLATFORM = MICROPYTHON PIN_SOURCE = machine.Pin.board -except ImportError: - from kmk.common.types import Passthrough - - PLATFORM = 'Unit Testing' - PIN_SOURCE = Passthrough() def get_pin(pin): @@ -32,9 +36,64 @@ def get_pin(pin): return getattr(PIN_SOURCE, pin) +class AbstractedDigitalPin: + def __init__(self, pin): + self.raw_pin = pin + + if PLATFORM == CIRCUITPYTHON: + self.pin = digitalio.DigitalInOut(pin) + elif PLATFORM == MICROPYTHON: + self.pin = machine.Pin(pin) + else: + self.pin = pin + + self.call_value = callable(self.pin.value) + + def __repr__(self): + return 'AbstractedPin({})'.format(repr(self.raw_pin)) + + def switch_to_input(self, pull=None): + if PLATFORM == CIRCUITPYTHON: + if pull == PULL_UP: + return self.pin.switch_to_input(pull=digitalio.Pull.UP) + elif pull == PULL_DOWN: + return self.pin.switch_to_input(pull=digitalio.Pull.DOWN) + + return self.pin.switch_to_input(pull=pull) + + elif PLATFORM == MICROPYTHON: + if pull == PULL_UP: + return self.pin.init(machine.Pin.IN, machine.Pin.PULL_UP) + elif pull == PULL_DOWN: + return self.pin.init(machine.Pin.IN, machine.Pin.PULL_DOWN) + + raise ValueError('only PULL_UP and PULL_DOWN supported on MicroPython') + + raise NotImplementedError('switch_to_input not supported on platform') + + def switch_to_output(self): + if PLATFORM == CIRCUITPYTHON: + return self.pin.switch_to_output() + elif PLATFORM == MICROPYTHON: + return self.pin.init(machine.Pin.OUT) + + raise NotImplementedError('switch_to_output not supported on platform') + + def value(self, value=None): + if value is None: + if self.call_value: + return self.pin.value() + return self.pin.value + + if self.call_value: + return self.pin.value(value) + self.pin.value = value + return value + + class PinLookup: def __getattr__(self, attr): - return get_pin(attr) + return AbstractedDigitalPin(get_pin(attr)) Pin = PinLookup() diff --git a/kmk/common/rotary_encoder.py b/kmk/common/rotary_encoder.py new file mode 100644 index 0000000..96a71ab --- /dev/null +++ b/kmk/common/rotary_encoder.py @@ -0,0 +1,57 @@ +from kmk.common.pins import PULL_UP + + +class RotaryEncoder: + # Please don't ask. I don't know. All I know is bit_value + # works as expected. Here be dragons, etc. etc. + MIN_VALUE = False + 1 << 1 | True + 1 + MAX_VALUE = True + 1 << 1 | True + 1 + + def __init__(self, pos_pin, neg_pin): + self.pos_pin = pos_pin + self.neg_pin = neg_pin + + self.pos_pin.switch_to_input(pull=PULL_UP) + self.neg_pin.switch_to_input(pull=PULL_UP) + + self.prev_bit_value = self.bit_value() + + def value(self): + return (self.pos_pin.value(), self.neg_pin.value()) + + def bit_value(self): + ''' + Returns 2, 3, 5, or 6 based on the state of the rotary encoder's two + bits. This is a total hack but it does what we need pretty efficiently. + Shrug. + ''' + return self.pos_pin.value() + 1 << 1 | self.neg_pin.value() + 1 + + def direction(self): + ''' + Compares the current rotary position against the last seen position. + + Returns True if we're rotating "positively", False if we're rotating "negatively", + and None if no change could safely be detected for any reason (usually this + means the encoder itself did not change) + ''' + new_value = self.bit_value() + rolling_under = self.prev_bit_value == self.MIN_VALUE and new_value == self.MAX_VALUE + rolling_over = self.prev_bit_value == self.MAX_VALUE and new_value == self.MIN_VALUE + increasing = new_value > self.prev_bit_value + decreasing = new_value < self.prev_bit_value + self.prev_bit_value = new_value + + if rolling_over: + return True + elif rolling_under: + return False + + if increasing: + return True + if decreasing: + return False + + # Either no change, or not a type of change we can safely detect, + # so safely do nothing + return None diff --git a/kmk/entrypoints/handwire/feather_m4_express.py b/kmk/entrypoints/handwire/feather_m4_express.py index b695541..64773ab 100644 --- a/kmk/entrypoints/handwire/feather_m4_express.py +++ b/kmk/entrypoints/handwire/feather_m4_express.py @@ -1,14 +1,24 @@ import sys -from logging import WARNING from kmk.circuitpython.hid import HIDHelper -from kmk.circuitpython.matrix import MatrixScanner from kmk.common.consts import UnicodeModes +from kmk.common.matrix import MatrixScanner from kmk.firmware import Firmware def main(): - from kmk_keyboard_user import cols, diode_orientation, keymap, rows + import kmk_keyboard_user + cols = getattr(kmk_keyboard_user, 'cols') + diode_orientation = getattr(kmk_keyboard_user, 'diode_orientation') + keymap = getattr(kmk_keyboard_user, 'keymap') + rows = getattr(kmk_keyboard_user, 'rows') + + DEBUG_ENABLE = getattr(kmk_keyboard_user, 'DEBUG_ENABLE', False) + + if DEBUG_ENABLE: + from logging import DEBUG as log_level + else: + from logging import ERROR as log_level try: from kmk_keyboard_user import unicode_mode @@ -22,7 +32,7 @@ def main(): col_pins=cols, diode_orientation=diode_orientation, unicode_mode=unicode_mode, - log_level=WARNING, + log_level=log_level, matrix_scanner=MatrixScanner, hid=HIDHelper, ) diff --git a/kmk/entrypoints/handwire/itsybitsy_m4_express.py b/kmk/entrypoints/handwire/itsybitsy_m4_express.py index b695541..64773ab 100644 --- a/kmk/entrypoints/handwire/itsybitsy_m4_express.py +++ b/kmk/entrypoints/handwire/itsybitsy_m4_express.py @@ -1,14 +1,24 @@ import sys -from logging import WARNING from kmk.circuitpython.hid import HIDHelper -from kmk.circuitpython.matrix import MatrixScanner from kmk.common.consts import UnicodeModes +from kmk.common.matrix import MatrixScanner from kmk.firmware import Firmware def main(): - from kmk_keyboard_user import cols, diode_orientation, keymap, rows + import kmk_keyboard_user + cols = getattr(kmk_keyboard_user, 'cols') + diode_orientation = getattr(kmk_keyboard_user, 'diode_orientation') + keymap = getattr(kmk_keyboard_user, 'keymap') + rows = getattr(kmk_keyboard_user, 'rows') + + DEBUG_ENABLE = getattr(kmk_keyboard_user, 'DEBUG_ENABLE', False) + + if DEBUG_ENABLE: + from logging import DEBUG as log_level + else: + from logging import ERROR as log_level try: from kmk_keyboard_user import unicode_mode @@ -22,7 +32,7 @@ def main(): col_pins=cols, diode_orientation=diode_orientation, unicode_mode=unicode_mode, - log_level=WARNING, + log_level=log_level, matrix_scanner=MatrixScanner, hid=HIDHelper, ) diff --git a/kmk/entrypoints/handwire/pyboard.py b/kmk/entrypoints/handwire/pyboard.py index 97816c0..8749002 100644 --- a/kmk/entrypoints/handwire/pyboard.py +++ b/kmk/entrypoints/handwire/pyboard.py @@ -2,8 +2,8 @@ import sys import gc +from kmk.common.matrix import MatrixScanner from kmk.firmware import Firmware -from kmk.micropython.matrix import MatrixScanner from kmk.micropython.pyb_hid import HIDHelper @@ -17,9 +17,9 @@ def main(): DEBUG_ENABLE = getattr(kmk_keyboard_user, 'DEBUG_ENABLE', False) if DEBUG_ENABLE: - from logging import DEBUG + from logging import DEBUG as log_level else: - from logging import ERROR as DEBUG + from logging import ERROR as log_level # This will run out of ram at this point unless you manually GC gc.collect() @@ -31,7 +31,7 @@ def main(): col_pins=cols, diode_orientation=diode_orientation, hid=HIDHelper, - log_level=DEBUG, + log_level=log_level, matrix_scanner=MatrixScanner, ) # This will run out of ram at this point unless you manually GC diff --git a/kmk/firmware.py b/kmk/firmware.py index 706b05c..7aaddb6 100644 --- a/kmk/firmware.py +++ b/kmk/firmware.py @@ -19,6 +19,9 @@ class Firmware: logger = logging.getLogger(__name__) logger.setLevel(log_level) + import kmk_keyboard_user + self.encoders = getattr(kmk_keyboard_user, 'encoders', []) + self.hydrated = False self.store = Store(kmk_reducer, log_level=log_level) @@ -58,3 +61,10 @@ class Firmware: if update: self.store.dispatch(update) + + for encoder in self.encoders: + eupdate = encoder.scan() + + if eupdate: + for event in eupdate: + self.store.dispatch(event) diff --git a/kmk/micropython/matrix.py b/kmk/micropython/matrix.py deleted file mode 100644 index a89f6dd..0000000 --- a/kmk/micropython/matrix.py +++ /dev/null @@ -1,63 +0,0 @@ -import machine - -from kmk.common.abstract.matrix_scanner import AbstractMatrixScanner -from kmk.common.consts import DiodeOrientation -from kmk.common.event_defs import matrix_changed - - -class MatrixScanner(AbstractMatrixScanner): - def __init__(self, cols, rows, active_layers, diode_orientation=DiodeOrientation.COLUMNS): - # A pin cannot be both a row and column, detect this by combining the - # two tuples into a set and validating that the length did not drop - # - # repr() hackery is because MicroPython Pin objects are not hashable. - # Technically we support passing either a string (hashable) or the - # Pin object directly here, so the hackaround is necessary. - unique_pins = {repr(c) for c in cols} | {repr(r) for r in rows} - if len(unique_pins) != len(cols) + len(rows): - raise ValueError('Cannot use a pin as both a column and row') - - self.cols = [machine.Pin(pin) for pin in cols] - self.rows = [machine.Pin(pin) for pin in rows] - self.diode_orientation = diode_orientation - self.active_layers = active_layers - self.last_pressed_len = 0 - - if self.diode_orientation == DiodeOrientation.COLUMNS: - self.outputs = self.cols - self.inputs = self.rows - elif self.diode_orientation == DiodeOrientation.ROWS: - self.outputs = self.rows - self.inputs = self.cols - else: - raise ValueError('Invalid DiodeOrientation: {}'.format( - self.diode_orientation, - )) - - for pin in self.outputs: - pin.init(machine.Pin.OUT) - pin.off() - - for pin in self.inputs: - pin.init(machine.Pin.IN, machine.Pin.PULL_DOWN) - pin.off() - - def scan_for_pressed(self): - pressed = [] - - for oidx, opin in enumerate(self.outputs): - opin.value(1) - - for iidx, ipin in enumerate(self.inputs): - if ipin.value(): - pressed.append( - (oidx, iidx) if self.diode_orientation == DiodeOrientation.ROWS else (iidx, oidx) # noqa - ) - - opin.value(0) - - if len(pressed) != self.last_pressed_len: - self.last_pressed_len = len(pressed) - return matrix_changed(pressed) - - return None # The default, but for explicitness diff --git a/upy-unix-stubs/machine/__init__.py b/upy-unix-stubs/machine/__init__.py index ecf74c1..6199bc0 100644 --- a/upy-unix-stubs/machine/__init__.py +++ b/upy-unix-stubs/machine/__init__.py @@ -5,9 +5,19 @@ class Anything: def __init__(self, name): self.name = name + def __call__(self, *args, **kwargs): + return self + def __repr__(self): return 'Anything<{}>'.format(self.name) + def init(self, *args, **kwargs): + pass + + @property + def value(self): + return False + class Passthrough: def __getattr__(self, attr): @@ -16,3 +26,13 @@ class Passthrough: class Pin: board = Passthrough() + IN = 'IN' + OUT = 'OUT' + PULL_DOWN = 'PULL_DOWN' + PULL_UP = 'PULL_UP' + + def __call__(self, *args, **kwargs): + return self.board + + def __getattr__(self, attr): + return getattr(self.board, attr) diff --git a/user_keymaps/klardotsh/itsybitsy_m4_express/threethree.py b/user_keymaps/klardotsh/itsybitsy_m4_express/threethree.py index e8243aa..3166ff9 100644 --- a/user_keymaps/klardotsh/itsybitsy_m4_express/threethree.py +++ b/user_keymaps/klardotsh/itsybitsy_m4_express/threethree.py @@ -1,5 +1,6 @@ from kmk.common.consts import DiodeOrientation, UnicodeModes from kmk.common.keycodes import KC +from kmk.common.macros.rotary_encoder import VolumeRotaryEncoder from kmk.common.macros.simple import send_string, simple_key_sequence from kmk.common.macros.unicode import unicode_string_sequence from kmk.common.pins import Pin as P @@ -7,12 +8,18 @@ from kmk.common.types import AttrDict from kmk.entrypoints.handwire.itsybitsy_m4_express import main from kmk.firmware import Firmware +DEBUG_ENABLE = True + cols = (P.A4, P.A5, P.D7) rows = (P.D12, P.D11, P.D10) diode_orientation = DiodeOrientation.COLUMNS unicode_mode = UnicodeModes.LINUX +encoders = [ + VolumeRotaryEncoder(P.A3, P.A2, 6, 0.6), +] + emoticons = AttrDict({ # Emojis 'BEER': r'🍺',