Merge pull request #57 from KMKfw/topic-rotary-encoders-lol-reddit

Turn down for WHAT?! Rotary encoder support
This commit is contained in:
Josh Klar 2018-10-11 13:57:05 -07:00 committed by GitHub
commit 3c5c95cfd7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 279 additions and 99 deletions

View File

@ -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')

View File

@ -1,3 +1,7 @@
CIRCUITPYTHON = 'CircuitPython'
MICROPYTHON = 'MicroPython'
class HIDReportTypes:
KEYBOARD = 1
MOUSE = 2

View File

@ -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

View File

@ -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)

View File

@ -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()

View File

@ -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

View File

@ -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,
)

View File

@ -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,
)

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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'🍺',