-
Notifications
You must be signed in to change notification settings - Fork 488
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement generalized analog input module
- Loading branch information
Showing
2 changed files
with
287 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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]], | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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: | ||
self._active[idx].on_stop(input, keyboard) | ||
del self._active[idx] | ||
if debug.enabled: | ||
debug('on_stop', input, key) | ||
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 |