kmk_firmware/kmk/modules/encoder.py

260 lines
9.6 KiB
Python

import digitalio
from supervisor import ticks_ms
from typing import Any, ClassVar, Dict, List, Optional, Tuple, Union
from kmk.keys import Key
from kmk.kmk_keyboard import KMKKeyboard
from kmk.kmktime import ticks_ms
from kmk.modules import Module
EncoderMap = Tuple[
List[Tuple[Key, Key, int]],
List[Tuple[Key, Key, int]],
List[Tuple[None, None, int]],
]
class EncoderPadState:
OFF: bool = False
ON: bool = True
class EndcoderDirection:
Left: bool = False
Right: bool = True
class Encoder:
def __init__(
self,
pad_a: Any,
pad_b: Any,
button_pin: Optional[Any] = None,
) -> None:
self.pad_a: Union[digitalio.DigitalInOut, None] = self.PreparePin(
pad_a
) # board pin for enc pin a
self.pad_a_state: bool = False
self.pad_b: Union[digitalio.DigitalInOut, None] = self.PreparePin(
pad_b
) # board pin for enc pin b
self.pad_b_state: bool = False
self.button_pin: Union[digitalio.DigitalInOut, None] = self.PreparePin(
button_pin
) # board pin for enc btn
self.button_state = None # state of pushbutton on encoder if enabled
self.encoder_value: int = 0 # clarify what this value is
self.encoder_state: Tuple[bool, bool] = (
self.pad_a_state,
self.pad_b_state,
) # quaderature encoder state
self.encoder_direction: Optional[
bool
] = None # arbitrary, tells direction of knob
self.last_encoder_state: Optional[Tuple[bool, bool]] = None # not used yet
self.resolution: int = 2 # number of keys sent per position change
self.revolution_count: int = 20 # position changes per revolution
self.has_button: bool = False # enable/disable button functionality
self.encoder_data: Optional[Tuple] = None # 6tuple containing all encoder data
self.position_change: Optional[
int
] = None # revolution count, inc/dec as knob turns
self.last_encoder_value: int = 0 # not used
self.is_inverted: bool = False # switch to invert knob direction
self.vel_mode: bool = False # enable the velocity output
self.vel_ts: Optional[float] = None # velocity timestamp
self.last_vel_ts: float = 0 # last velocity timestamp
self.encoder_speed: Optional[float] = None # ms per position change(4 states)
self.encoder_map: Optional[EncoderMap] = None
self.eps: EncoderPadState = EncoderPadState()
self.encoder_pad_lookup: Dict[bool, bool] = {
False: self.eps.OFF,
True: self.eps.ON,
}
self.edr: EndcoderDirection = (
EndcoderDirection()
) # lookup for current encoder direction
self.encoder_dir_lookup: Dict[bool, bool] = {
False: self.edr.Left,
True: self.edr.Right,
}
def __repr__(self, idx: int) -> str:
return 'ENCODER_{}({})'.format(idx, self._to_dict())
def _to_dict(self) -> Dict[str, Any]:
return {
'Encoder_State': self.encoder_state,
'Direction': self.encoder_direction,
'Value': self.encoder_value,
'Position_Change': self.position_change,
'Speed': self.encoder_speed,
'Button_State': self.button_state,
}
# adapted for CircuitPython from raspi
def PreparePin(self, num: Union[Any, None]) -> Union[digitalio.DigitalInOut, None]:
if num is not None:
pad = digitalio.DigitalInOut(num)
pad.direction = digitalio.Direction.INPUT
pad.pull = digitalio.Pull.UP
return pad
else:
return None
# checks encoder pins, reports encoder data
def report(self) -> Union[int, None]:
new_encoder_state = (
self.encoder_pad_lookup[int(self.pad_a.value)],
self.encoder_pad_lookup[int(self.pad_b.value)],
)
if self.encoder_state == (self.eps.ON, self.eps.ON): # Resting position
if new_encoder_state == (self.eps.ON, self.eps.OFF): # Turned right 1
self.encoder_direction = self.edr.Right
elif new_encoder_state == (self.eps.OFF, self.eps.ON): # Turned left 1
self.encoder_direction = self.edr.Left
elif self.encoder_state == (self.eps.ON, self.eps.OFF): # R1 or L3 position
if new_encoder_state == (self.eps.OFF, self.eps.OFF): # Turned right 1
self.encoder_direction = self.edr.Right
elif new_encoder_state == (self.eps.ON, self.eps.ON): # Turned left 1
if self.encoder_direction == self.edr.Left:
self.encoder_value = self.encoder_value - 1
elif self.encoder_state == (self.eps.OFF, self.eps.ON): # R3 or L1
if new_encoder_state == (self.eps.OFF, self.eps.OFF): # Turned left 1
self.encoder_direction = self.edr.Left
elif new_encoder_state == (self.eps.ON, self.eps.ON): # Turned right 1
if self.encoder_direction == self.edr.Right:
self.encoder_value = self.encoder_value + 1
else: # self.encoder_state == '11'
if new_encoder_state == (self.eps.ON, self.eps.OFF): # Turned left 1
self.encoder_direction = self.edr.Left
elif new_encoder_state == (self.eps.OFF, self.eps.ON): # Turned right 1
self.encoder_direction = self.edr.Right # 'R'
elif new_encoder_state == (
self.eps.ON,
self.eps.ON,
): # Skipped intermediate 01 or 10 state, however turn completed
if self.encoder_direction == self.edr.Left:
self.encoder_value = self.encoder_value - 1
elif self.encoder_direction == self.edr.Right:
self.encoder_value = self.encoder_value + 1
self.encoder_state = new_encoder_state
if self.vel_mode:
self.vel_ts = ticks_ms()
if self.encoder_state != self.last_encoder_state:
self.position_change = self.invert_rotation(
self.encoder_value, self.last_encoder_value
)
self.last_encoder_state = self.encoder_state
self.last_encoder_value = self.encoder_value
if self.position_change > 0:
self._to_dict()
# return self.increment_key
return 0
elif self.position_change < 0:
self._to_dict()
# return self.decrement_key
return 1
else:
return None
# invert knob direction if encoder pins are soldered backwards
def invert_rotation(self, new: int, old: int) -> int:
if self.is_inverted:
return -(new - old)
else:
return new - old
# returns knob velocity as milliseconds between position changes(detents)
def vel_report(self) -> float:
self.encoder_speed = self.vel_ts - self.last_vel_ts
self.last_vel_ts = self.vel_ts
return self.encoder_speed
class EncoderHandler(Module):
encoders: ClassVar[List[Encoder]] = []
debug_enabled: ClassVar[
bool
] = False # not working as inttended, do not use for now
def __init__(
self, pad_a: List[Any], pad_b: List[Any], encoder_map: EncoderMap
) -> None:
self.pad_a: List[Any] = pad_a
self.pad_b: List[Any] = pad_b
self.encoder_count: int = len(self.pad_a)
self.encoder_map: EncoderMap = encoder_map
self.make_encoders()
def on_runtime_enable(self, keyboard: KMKKeyboard) -> None:
return
def on_runtime_disable(self, keyboard: KMKKeyboard) -> None:
return
def during_bootup(self, keyboard: KMKKeyboard) -> None:
return
def before_matrix_scan(self, keyboard: KMKKeyboard) -> Union[KMKKeyboard, None]:
'''
Return value will be injected as an extra matrix update
'''
return self.get_reports(keyboard)
def after_matrix_scan(self, keyboard: KMKKeyboard) -> None:
'''
Return value will be replace matrix update if supplied
'''
return
def before_hid_send(self, keyboard: KMKKeyboard) -> None:
return
def after_hid_send(self, keyboard: KMKKeyboard) -> None:
return
def on_powersave_enable(self, keyboard: KMKKeyboard) -> None:
return
def on_powersave_disable(self, keyboard: KMKKeyboard) -> None:
return
def make_encoders(self) -> None:
for i in range(self.encoder_count):
self.encoders.append(
Encoder(
self.pad_a[i], # encoder pin a
self.pad_b[i], # encoder pin b
)
)
def send_encoder_keys(
self, keyboard: KMKKeyboard, encoder_key: int, encoder_idx: int
) -> KMKKeyboard:
# position in the encoder map tuple
encoder_resolution = 2
for _ in range(
self.encoder_map[keyboard.active_layers[0]][encoder_idx][encoder_resolution]
):
keyboard.tap_key(
self.encoder_map[keyboard.active_layers[0]][encoder_idx][encoder_key]
)
return keyboard
def get_reports(self, keyboard: KMKKeyboard) -> Union[KMKKeyboard, None]:
for idx in range(self.encoder_count):
if self.debug_enabled: # not working as inttended, do not use for now
print(self.encoders[idx].__repr__(idx))
encoder_key = self.encoders[idx].report()
if encoder_key is not None:
return self.send_encoder_keys(keyboard, encoder_key, idx)