Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement generalized analog input module #1054

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
158 changes: 158 additions & 0 deletions docs/en/analogin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
# 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: AnalogInput,
filter: Optional(Callable[AnalogInput, 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 kmk.modules.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 AnalogInput, AnalogInputs

analog = AnalogInputs(
[
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.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
# holdtap = ...
# rgb = ...

import board
import busio
import adafruit_mcp4725

from kmk.modules.analogin import AnalogEvent, AnalogInput, AnalogInputs

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_change=adj_ht_taptime,
on_hold=lambda self, event, keyboard: rgb.increase_hue(16),
)

a0 = AnalogInput(dac, lambda _: int(_.value / 0xFFFF * 1980) + 20)

analog = AnalogInputs(
[a0],
[[HTT]],
)

keyboard.modules.append(analog)
```
131 changes: 131 additions & 0 deletions kmk/modules/analogin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
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):
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
12 changes: 11 additions & 1 deletion util/aspell.en.pws
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
personal_ws-1.1 en 363
personal_ws-1.1 en 368
ADNS
AMS
ANAVI
Expand All @@ -7,6 +7,13 @@ AVR
Adafruit
Adafruit's
Affero
AnalogEvent
AnalogIn
AnalogInput
AnalogInputs
AnalogKey
AnalogKeys
Analogio
BT
BYO
Batreus
Expand All @@ -33,6 +40,7 @@ Crkbd
Crowboard
Ctrl
Cygwin
DAC
DFU
DISCOVERABLE
DIY
Expand Down Expand Up @@ -218,6 +226,7 @@ adafruit
addon
adns
amongst
analogio
argumented
assignees
automounter
Expand Down Expand Up @@ -350,6 +359,7 @@ synched
th
tl
txt
uint
uncomment
underglow
underlighting
Expand Down
Loading