Skip to content

Commit 525df06

Browse files
authored
Implement generalized analog input module (#1054)
* Implement generalized analog input module * Fix spellchecking of analogin.md * Fix examples in analogin docs
1 parent e1f3642 commit 525df06

File tree

3 files changed

+300
-1
lines changed

3 files changed

+300
-1
lines changed

docs/en/analogin.md

+158
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
# AnalogIn
2+
3+
Make use of input sources that implement CircuitPython's `analogio` interface.
4+
5+
## Usage
6+
7+
### AnalogInputs
8+
9+
The module that reads and maps "analog" inputs to events/actions.
10+
11+
```python
12+
from kmk.modules.analogin import AnalogInputs, AnalogInput
13+
14+
analog = AnalogInputs(
15+
inputs: list(AnalogInput),
16+
evtmap=[[]],
17+
)
18+
```
19+
20+
#### `inputs`
21+
22+
A list of `AnalogInput` objects, see below.
23+
24+
#### `evtmap`
25+
26+
The event map is `AnalogIn`s version of `keyboard.keymap`, but for analog events
27+
instead of keys.
28+
It supports KMK's layer mechanism and `KC.TRNS` and `KC.NO`.
29+
Any other keys have to be wrapped in `AnalogKey`, see below.
30+
31+
### AnalogInput
32+
33+
A light wrapper around objects that implement CircuitPython's analogio
34+
interface, i.e. objects that have a `value` property that contains the current
35+
value in the domain [0, 65535].
36+
37+
```python
38+
from kmk.modules.analogin import AnalogInput
39+
a = AnalogInput(
40+
input: AnalogInput,
41+
filter: Optional(Callable[AnalogInput, int]) = lambda input:input.value>>8,
42+
)
43+
44+
a.value
45+
a.delta
46+
47+
```
48+
49+
#### `input`
50+
51+
An `AnalogIn` like object.
52+
53+
#### `filter`
54+
55+
A customizable function that reads and transforms `input.value`.
56+
The default transformation maps uint16 ([0-65535]) to uint8 ([0-255]) resolution.
57+
58+
#### `value`
59+
60+
Holds the transformed value of the `AnalogIn` input.
61+
To be used in handler functions.
62+
63+
#### `delta`
64+
65+
Holds the amount of change of transformed value of the `AnalogIn` input.
66+
To be used in handler functions.
67+
68+
69+
### AnalogEvent
70+
71+
The analog version of [`Key` objects](keys.md).
72+
73+
```python
74+
from kmk.modules.analogin import AnalogEvent
75+
76+
AE = AnalogEvent(
77+
on_change: Callable[self, AnalogInput, Keyboard, None] = pass,
78+
on_stop: Callable[self, AnalogInput, Keyboard, None] = pass,
79+
)
80+
```
81+
82+
### AnalogKey
83+
84+
A "convenience" implementation of `AnalogEvent` that emits `Key` objects.
85+
86+
```python
87+
from analogio import AnalogKey
88+
89+
AK = AnalogKey(
90+
key: Key,
91+
threshold: Optional[int] = 127,
92+
)
93+
```
94+
95+
## Examples
96+
97+
### Analogio with AnalogKeys
98+
99+
```python
100+
import board
101+
from analogio import AnalogIn
102+
from kmk.modules.analogin import AnalogInput, AnalogInputs
103+
104+
analog = AnalogInputs(
105+
[
106+
AnalogInput(AnalogIn(board.A0)),
107+
AnalogInput(AnalogIn(board.A1)),
108+
AnalogInput(AnalogIn(board.A2)),
109+
],
110+
[
111+
[AnalogKey(KC.X), AnalogKey(KC.Y), AnalogKey(KC.Z)],
112+
[KC.TRNS, KC.NO, AnalogKey(KC.W, threshold=96)],
113+
],
114+
)
115+
116+
keyboard.modules.append(analog)
117+
```
118+
119+
### External DAC with AnalogEvent
120+
121+
Use an external ADC to adjust `HoldTap.tap_time` at runtime between 20 and 2000 ms.
122+
If no new readings occur: change RGB hue.
123+
But careful: if changed by more than 100 units at a time, the board will reboot.
124+
125+
```python
126+
# setup of holdtap and rgb omitted for brevity
127+
# holdtap = ...
128+
# rgb = ...
129+
130+
import board
131+
import busio
132+
import adafruit_mcp4725
133+
134+
from kmk.modules.analogin import AnalogEvent, AnalogInput, AnalogInputs
135+
136+
i2c = busio.I2C(board.SCL, board.SDA)
137+
dac = adafruit_mcp4725.MCP4725(i2c)
138+
139+
def adj_ht_taptime(self, event, keyboard):
140+
holdtap.tap_time = event.value
141+
if abs(event.change) > 100:
142+
import microcontroller
143+
microcontroller.reset()
144+
145+
HTT = AnalogEvent(
146+
on_change=adj_ht_taptime,
147+
on_hold=lambda self, event, keyboard: rgb.increase_hue(16),
148+
)
149+
150+
a0 = AnalogInput(dac, lambda _: int(_.value / 0xFFFF * 1980) + 20)
151+
152+
analog = AnalogInputs(
153+
[a0],
154+
[[HTT]],
155+
)
156+
157+
keyboard.modules.append(analog)
158+
```

kmk/modules/analogin.py

+131
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
from kmk.keys import KC
2+
from kmk.modules import Module
3+
from kmk.utils import Debug
4+
5+
debug = Debug(__name__)
6+
7+
8+
def noop(*args):
9+
pass
10+
11+
12+
class AnalogEvent:
13+
def __init__(self, on_change=noop, on_stop=noop):
14+
self._on_change = on_change
15+
self._on_stop = on_stop
16+
17+
def on_change(self, event, keyboard):
18+
self._on_change(self, event, keyboard)
19+
20+
def on_stop(self, event, keyboard):
21+
self._on_stop(self, event, keyboard)
22+
23+
24+
class AnalogKey(AnalogEvent):
25+
def __init__(self, key, threshold=127):
26+
self.key = key
27+
self.threshold = threshold
28+
self.pressed = False
29+
30+
def on_change(self, event, keyboard):
31+
if event.value >= self.threshold and not self.pressed:
32+
self.pressed = True
33+
keyboard.pre_process_key(self.key, True)
34+
35+
elif event.value < self.threshold and self.pressed:
36+
self.pressed = False
37+
keyboard.pre_process_key(self.key, False)
38+
39+
def on_stop(self, event, keyboard):
40+
pass
41+
42+
43+
class AnalogInput:
44+
def __init__(self, input, filter=lambda input: input.value >> 8):
45+
self.input = input
46+
self.value = 0
47+
self.delta = 0
48+
self.filter = filter
49+
50+
def update(self):
51+
'''
52+
Read a new value from an analogio compatible input, apply
53+
transformation, then return either the new value if it changed or `None`
54+
otherwise.
55+
'''
56+
value = self.filter(self.input)
57+
self.delta = value - self.value
58+
if self.delta != 0:
59+
self.value = value
60+
return value
61+
62+
63+
class AnalogInputs(Module):
64+
def __init__(self, inputs, evtmap):
65+
self._active = {}
66+
self.inputs = inputs
67+
self.evtmap = evtmap
68+
69+
def on_runtime_enable(self, keyboard):
70+
return
71+
72+
def on_runtime_disable(self, keyboard):
73+
return
74+
75+
def during_bootup(self, keyboard):
76+
return
77+
78+
def before_matrix_scan(self, keyboard):
79+
for idx, input in enumerate(self.inputs):
80+
value = input.update()
81+
82+
# No change in value: stop or pass
83+
if value is None:
84+
if input in self._active:
85+
if debug.enabled:
86+
debug('on_stop', input, self._active[idx])
87+
self._active[idx].on_stop(input, keyboard)
88+
del self._active[idx]
89+
continue
90+
91+
# Resolve event handler
92+
if input in self._active:
93+
key = self._active[idx]
94+
else:
95+
key = None
96+
for layer in keyboard.active_layers:
97+
try:
98+
key = self.evtmap[layer][idx]
99+
except IndexError:
100+
if debug.enabled:
101+
debug('evtmap IndexError: idx=', idx, ' layer=', layer)
102+
if key and key != KC.TRNS:
103+
break
104+
105+
if key == KC.NO:
106+
continue
107+
108+
# Forward change to event handler
109+
try:
110+
self._active[idx] = key
111+
if debug.enabled:
112+
debug('on_change', input, key, value)
113+
key.on_change(input, keyboard)
114+
except Exception as e:
115+
if debug.enabled:
116+
debug(type(e), ': ', e, ' in ', key.on_change)
117+
118+
def after_matrix_scan(self, keyboard):
119+
return
120+
121+
def before_hid_send(self, keyboard):
122+
return
123+
124+
def after_hid_send(self, keyboard):
125+
return
126+
127+
def on_powersave_enable(self, keyboard):
128+
return
129+
130+
def on_powersave_disable(self, keyboard):
131+
return

util/aspell.en.pws

+11-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
personal_ws-1.1 en 364
1+
personal_ws-1.1 en 368
22
ADNS
33
AMS
44
ANAVI
@@ -7,6 +7,13 @@ AVR
77
Adafruit
88
Adafruit's
99
Affero
10+
AnalogEvent
11+
AnalogIn
12+
AnalogInput
13+
AnalogInputs
14+
AnalogKey
15+
AnalogKeys
16+
Analogio
1017
BT
1118
BYO
1219
Batreus
@@ -34,6 +41,7 @@ Crkbd
3441
Crowboard
3542
Ctrl
3643
Cygwin
44+
DAC
3745
DFU
3846
DISCOVERABLE
3947
DIY
@@ -219,6 +227,7 @@ adafruit
219227
addon
220228
adns
221229
amongst
230+
analogio
222231
argumented
223232
assignees
224233
automounter
@@ -351,6 +360,7 @@ synched
351360
th
352361
tl
353362
txt
363+
uint
354364
uncomment
355365
underglow
356366
underlighting

0 commit comments

Comments
 (0)