diff --git a/docs/en/analogin.md b/docs/en/analogin.md new file mode 100644 index 000000000..9440518d2 --- /dev/null +++ b/docs/en/analogin.md @@ -0,0 +1,155 @@ +# AnalogIn + +Make use of input sources that implement CircuitPython's `analogio` interface. + +## Usage + +### AnalogInputs + +The module that reads and maps "analog" inputs to events/actions. + +```python +from kmk.modules.analogin import AnalogInputs, AnalogInput + +analog = AnalogInputs( + inputs: list(AnalogInput), + evtmap=[[]], +) +``` + +#### inputs + +A list of `AnalogInput` objects, see below. + +#### evtmap + +The event map is `AnalogIn`s version of `keyboard.keymap`, but for analog events +instead of keys. +It supports KMK's layer mechanism and `KC.TRNS` and `KC.NO`. +Any other keys have to be wrapped in `AnalogKey`, see below. + +### AnalogInput + +A light wrapper around objects that implement CircuitPython's analogio +interface, i.e. objects that have a `value` property that contains the current +value in the domain [0, 65535]. + +```python +from kmk.modules.analogin import AnalogInput +a = AnalogInput( + input: AnalogIn, + filter: Optional(Callable[AnalogIn, int]) = lambda input:input.value>>8, +) + +a.value +a.delta + +``` + +#### input + +An `AnalogIn` like object. + +#### filter + +A customizable function that reads and transforms `input.value`. +The default transformation maps uint16 ([0-65535]) to uint8 ([0-255]) resolution. + +#### value + +Holds the transformed value of the `AnalogIn` input. +To be used in handler functions. + +#### delta + +Holds the amount of change of transformed value of the `AnalogIn` input. +To be used in handler functions. + + +### AnalogEvent + +The analog version of [`Key` objects](keys.md). + +```python +from analogin import AnalogEvent + +AE = AnalogEvent( + on_change: Callable[self, AnalogInput, Keyboard, None] = pass, + on_stop: Callable[self, AnalogInput, Keyboard, None] = pass, +) +``` + +### AnalogKey + +A "convenience" implementation of `AnalogEvent` that emits `Key` objects. + +```python +from analogio import AnalogKey + +AK = AnalogKey( + key: Key, + threshold: Optional[int] = 127, +) +``` + +## Examples + +### Analogio with AnalogKeys + +```python +import board +from analogio import AnalogIn +from kmk.modules.analogin import AnalogIn + +analog = AnalogIn( + [ + AnalogInput(AnalogIn(board.A0)), + AnalogInput(AnalogIn(board.A1)), + AnalogInput(AnalogIn(board.A2)), + ], + [ + [AnalogKey(KC.X), AnalogKey(KC.Y), AnalogKey(KC.Z)], + [KC.TRNS, KC.NO, AnalogKey(KC.W, threshold=96)], + ], +) + +keyboard.modules.append(analog) +``` + +### External DAC with AnalogEvent + +Use an external ADC to adjust holdtap taptime at runtime between 20 and 2000 ms. +If no new readings occur: change rgb hue. +But carefull: if changed by more than 100 units at a time, the board will reboot. + +```python +# setup of holdtap and rgb omitted for brevity +# holdtap = ... +# rgb = ... + +import board +import busio +import adafruit_mcp4725 + +from kmk.modules.analogin import AnalogEvent, AnalogInput + +i2c = busio.I2C(board.SCL, board.SDA) +dac = adafruit_mcp4725.MCP4725(i2c) + +def adj_ht_taptime(self, event, keyboard): + holdtap.tap_time = event.value + if abs(event.change) > 100: + import microcontroller + microcontroller.reset() + +HTT = AnalogEvent( + on_press=adj_ht_taptime, + on_hold=lambda self, event, keyboard: rgb.increase_hue(16), +) + +a0 = AnalogInput(dac, lambda _: int(_.value / 0xFFFF * 1980) + 20) + +analog = AnalogIn( + [a0], + [[HTT]], +``` diff --git a/kmk/modules/analogin.py b/kmk/modules/analogin.py new file mode 100644 index 000000000..2d0969bbc --- /dev/null +++ b/kmk/modules/analogin.py @@ -0,0 +1,132 @@ +from kmk.keys import KC +from kmk.modules import Module +from kmk.utils import Debug + +debug = Debug(__name__) + + +def noop(*args): + pass + + +class AnalogEvent: + def __init__(self, on_change=noop, on_stop=noop): + self._on_change = on_change + self._on_stop = on_stop + + def on_change(self, event, keyboard): + self._on_change(self, event, keyboard) + + def on_stop(self, event, keyboard): + self._on_stop(self, event, keyboard) + + +class AnalogKey(AnalogEvent): + def __init__(self, key, threshold=127): + self.key = key + self.threshold = threshold + self.pressed = False + + def on_change(self, event, keyboard): + debug(event.value) + if event.value >= self.threshold and not self.pressed: + self.pressed = True + keyboard.pre_process_key(self.key, True) + + elif event.value < self.threshold and self.pressed: + self.pressed = False + keyboard.pre_process_key(self.key, False) + + def on_stop(self, event, keyboard): + pass + + +class AnalogInput: + def __init__(self, input, filter=lambda input: input.value >> 8): + self.input = input + self.value = 0 + self.delta = 0 + self.filter = filter + + def update(self): + ''' + Read a new value from an analogio compatible input, apply + transformation, then return either the new value if it changed or `None` + otherwise. + ''' + value = self.filter(self.input) + self.delta = value - self.value + if self.delta != 0: + self.value = value + return value + + +class AnalogInputs(Module): + def __init__(self, inputs, evtmap): + self._active = {} + self.inputs = inputs + self.evtmap = evtmap + + def on_runtime_enable(self, keyboard): + return + + def on_runtime_disable(self, keyboard): + return + + def during_bootup(self, keyboard): + return + + def before_matrix_scan(self, keyboard): + for idx, input in enumerate(self.inputs): + value = input.update() + + # No change in value: stop or pass + if value is None: + if input in self._active: + if debug.enabled: + debug('on_stop', input, self._active[idx]) + self._active[idx].on_stop(input, keyboard) + del self._active[idx] + continue + + # Resolve event handler + if input in self._active: + key = self._active[idx] + else: + key = None + for layer in keyboard.active_layers: + try: + key = self.evtmap[layer][idx] + except IndexError: + if debug.enabled: + debug('evtmap IndexError: idx=', idx, ' layer=', layer) + if key and key != KC.TRNS: + break + + if key == KC.NO: + continue + + # Forward change to event handler + try: + self._active[idx] = key + if debug.enabled: + debug('on_change', input, key, value) + key.on_change(input, keyboard) + except Exception as e: + if debug.enabled: + debug(type(e), ': ', e, ' in ', key.on_change) + + def after_matrix_scan(self, keyboard): + return + + def before_hid_send(self, keyboard): + return + + def after_hid_send(self, keyboard): + return + + def on_powersave_enable(self, keyboard): + return + + def on_powersave_disable(self, keyboard): + return