From 6779aa6c8f08721a38c6ba09ddb701b4a1ef4d1b Mon Sep 17 00:00:00 2001 From: xs5871 Date: Tue, 10 Dec 2024 07:45:02 +0000 Subject: [PATCH 1/3] Implement generalized analog input module --- docs/en/analogin.md | 155 ++++++++++++++++++++++++++++++++++++++++ kmk/modules/analogin.py | 132 ++++++++++++++++++++++++++++++++++ 2 files changed, 287 insertions(+) create mode 100644 docs/en/analogin.md create mode 100644 kmk/modules/analogin.py 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 From 3a76f2bfabacad2e7cd82caf1e542b8689b961b8 Mon Sep 17 00:00:00 2001 From: xs5871 Date: Tue, 10 Dec 2024 07:58:17 +0000 Subject: [PATCH 2/3] Fix spellchecking of analogin.md --- docs/en/analogin.md | 18 +++++++++--------- util/aspell.en.pws | 12 +++++++++++- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/docs/en/analogin.md b/docs/en/analogin.md index 9440518d2..06032daca 100644 --- a/docs/en/analogin.md +++ b/docs/en/analogin.md @@ -17,11 +17,11 @@ analog = AnalogInputs( ) ``` -#### inputs +#### `inputs` A list of `AnalogInput` objects, see below. -#### evtmap +#### `evtmap` The event map is `AnalogIn`s version of `keyboard.keymap`, but for analog events instead of keys. @@ -46,21 +46,21 @@ a.delta ``` -#### input +#### `input` An `AnalogIn` like object. -#### filter +#### `filter` A customizable function that reads and transforms `input.value`. The default transformation maps uint16 ([0-65535]) to uint8 ([0-255]) resolution. -#### value +#### `value` Holds the transformed value of the `AnalogIn` input. To be used in handler functions. -#### delta +#### `delta` Holds the amount of change of transformed value of the `AnalogIn` input. To be used in handler functions. @@ -118,9 +118,9 @@ 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. +Use an external ADC to adjust `HoldTap.tap_time` at runtime between 20 and 2000 ms. +If no new readings occur: change RGB hue. +But careful: if changed by more than 100 units at a time, the board will reboot. ```python # setup of holdtap and rgb omitted for brevity diff --git a/util/aspell.en.pws b/util/aspell.en.pws index 86e222c03..5ddb553c7 100644 --- a/util/aspell.en.pws +++ b/util/aspell.en.pws @@ -1,4 +1,4 @@ -personal_ws-1.1 en 363 +personal_ws-1.1 en 368 ADNS AMS ANAVI @@ -7,6 +7,13 @@ AVR Adafruit Adafruit's Affero +AnalogEvent +AnalogIn +AnalogInput +AnalogInputs +AnalogKey +AnalogKeys +Analogio BT BYO Batreus @@ -33,6 +40,7 @@ Crkbd Crowboard Ctrl Cygwin +DAC DFU DISCOVERABLE DIY @@ -218,6 +226,7 @@ adafruit addon adns amongst +analogio argumented assignees automounter @@ -350,6 +359,7 @@ synched th tl txt +uint uncomment underglow underlighting From 40b6c85f10898ba485af031afe32f6b92c53ae88 Mon Sep 17 00:00:00 2001 From: xs5871 Date: Mon, 16 Dec 2024 09:47:42 +0000 Subject: [PATCH 3/3] Fix examples in analogin docs --- docs/en/analogin.md | 19 +++++++++++-------- kmk/modules/analogin.py | 1 - 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/docs/en/analogin.md b/docs/en/analogin.md index 06032daca..fee84bc9a 100644 --- a/docs/en/analogin.md +++ b/docs/en/analogin.md @@ -37,8 +37,8 @@ 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, + input: AnalogInput, + filter: Optional(Callable[AnalogInput, int]) = lambda input:input.value>>8, ) a.value @@ -71,7 +71,7 @@ To be used in handler functions. The analog version of [`Key` objects](keys.md). ```python -from analogin import AnalogEvent +from kmk.modules.analogin import AnalogEvent AE = AnalogEvent( on_change: Callable[self, AnalogInput, Keyboard, None] = pass, @@ -99,9 +99,9 @@ AK = AnalogKey( ```python import board from analogio import AnalogIn -from kmk.modules.analogin import AnalogIn +from kmk.modules.analogin import AnalogInput, AnalogInputs -analog = AnalogIn( +analog = AnalogInputs( [ AnalogInput(AnalogIn(board.A0)), AnalogInput(AnalogIn(board.A1)), @@ -131,7 +131,7 @@ import board import busio import adafruit_mcp4725 -from kmk.modules.analogin import AnalogEvent, AnalogInput +from kmk.modules.analogin import AnalogEvent, AnalogInput, AnalogInputs i2c = busio.I2C(board.SCL, board.SDA) dac = adafruit_mcp4725.MCP4725(i2c) @@ -143,13 +143,16 @@ def adj_ht_taptime(self, event, keyboard): microcontroller.reset() HTT = AnalogEvent( - on_press=adj_ht_taptime, + on_change=adj_ht_taptime, on_hold=lambda self, event, keyboard: rgb.increase_hue(16), ) a0 = AnalogInput(dac, lambda _: int(_.value / 0xFFFF * 1980) + 20) -analog = AnalogIn( +analog = AnalogInputs( [a0], [[HTT]], +) + +keyboard.modules.append(analog) ``` diff --git a/kmk/modules/analogin.py b/kmk/modules/analogin.py index 2d0969bbc..66463e115 100644 --- a/kmk/modules/analogin.py +++ b/kmk/modules/analogin.py @@ -28,7 +28,6 @@ def __init__(self, key, threshold=127): 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)