Skip to content

Commit

Permalink
Implement generalized analog input module
Browse files Browse the repository at this point in the history
  • Loading branch information
xs5871 committed Dec 10, 2024
1 parent f634693 commit 29f4a7a
Show file tree
Hide file tree
Showing 2 changed files with 287 additions and 0 deletions.
155 changes: 155 additions & 0 deletions docs/en/analogin.md
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]],
```
132 changes: 132 additions & 0 deletions kmk/modules/analogin.py
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

0 comments on commit 29f4a7a

Please sign in to comment.