Merge Encoder and NewEncoder. Enhance missteps handling
This commit is contained in:
parent
d9fb351448
commit
ed68ddb79d
196
docs/encoder.md
196
docs/encoder.md
@ -1,59 +1,137 @@
|
|||||||
# Encoder
|
# Encoder module
|
||||||
Add twist control to your keyboard! Volume, zoom, anything you want.
|
Add twist control to your keyboard! Volume, zoom, anything you want
|
||||||
|
|
||||||
## Enabling the extension
|
## Enabling the extension
|
||||||
The constructor(`EncoderHandler` class) takes a minimum of 3 arguments: a list of pad_a pins, a list of pad_b pins,
|
The constructor(`EncoderHandler` class) takes a list of encoders, each one defined as a list of pad_a pin, pad_b pin, button_pin and optionnally a flag set to True is youwant it to be reversed
|
||||||
and an encoder_map. The encoder_map is modeled after the keymap and works the
|
The encoder_map is modeled after the keymap and works the
|
||||||
same way. It should have as many layers as your keymap, and use KC.NO keys for
|
same way. It should have as many layers (key pressed on "turned left", key pressed on "turned right", key pressed on "knob pressed") as your keymap, and use KC.NO keys for layers that you don't require any action.
|
||||||
layers that you don't require any action. The encoder supports a velocity mode
|
The encoder supports a velocity mode if you desire to make something for video or sound editing.
|
||||||
if you desire to make something for video or sound editing. The direction of
|
|
||||||
increment/decrement can be changed to make sense for the direction the knob is
|
|
||||||
turning by setting the is_inverted flag.
|
|
||||||
|
## How to use
|
||||||
## Configuration
|
How to use this module in your main / code file
|
||||||
|
|
||||||
There is a complete example in the Atreus62 [main.py](/boards/atreus62/main.py)
|
1. load the module
|
||||||
|
```python
|
||||||
Create the encoder_map.
|
from kmk.modules.encoder import EncoderHandler
|
||||||
|
encoder_handler = EncoderHandler()
|
||||||
> Anatomy of an encoder_map tuple: (increment_key, decrement_key, keys presses per encoder click)
|
keyboard.modules = [layers, modtap, encoder_handler]
|
||||||
> `encoder_map` is a Nested List with a Tuple as the list element (`List[List[Tuple(Key,Key,Int)]]`)
|
```
|
||||||
|
|
||||||
```python
|
2. Define the pins for each encoder (pin_a, pin_b, pin_button, True for an inversed encoder)
|
||||||
from kmk.modules.encoder import EncoderHandler # import the Encoder module
|
```python
|
||||||
|
encoder_handler.pins = ((board.GP17, board.GP15, board.GP14, False), (encoder 2 definition), etc. )
|
||||||
Zoom_in = KC.LCTRL(KC.EQUAL)
|
```
|
||||||
Zoom_out = KC.LCTRL(KC.MINUS)
|
|
||||||
|
3. Define the mapping of keys to be called (1 / layer)
|
||||||
# create the encoder map, modeled after the keymap
|
```python
|
||||||
encoder_map = [
|
# You can optionally predefine combo keys as for your layout
|
||||||
[
|
Zoom_in = KC.LCTRL(KC.EQUAL)
|
||||||
# Only 1 encoder is being used, so only one tuple per layer is required
|
Zoom_out = KC.LCTRL(KC.MINUS)
|
||||||
# Increment key is volume up, decrement key is volume down, and sends 2
|
|
||||||
# key presses for every "click" felt while turning the encoder.
|
|
||||||
(KC.VOLU,KC.VOLD,2),
|
encoder_handler.map = [(( KC.VOLD, KC.VOLU, KC.MUTE),(encoder 2 definition), etc. ), # Layer 1
|
||||||
],
|
((KC.Zoom_out, KC.Zoom_in, KC.NO),(encoder 2 definition), etc. ), # Layer 2
|
||||||
[
|
((KC.A, KC.Z, KC.N1),(encoder 2 definition), etc. ), # Layer 3
|
||||||
# only one key press sent per encoder click
|
((KC.NO, KC.NO, KC.NO),(encoder 2 definition), etc. ), # Layer 4
|
||||||
(Zoom_in, Zoom_out,1),
|
]
|
||||||
],
|
```
|
||||||
[
|
|
||||||
# No action keys sent here, the resolution is a dummy number, to be
|
|
||||||
# removed in the future.
|
|
||||||
(_______,_______,1),#
|
4. Encoder methods on_move_do and on_button_do can be overwritten for complex use cases
|
||||||
]
|
|
||||||
]
|
## Full example (with 1 encoder)
|
||||||
|
|
||||||
# create the encoder instance, and pass in a list of pad_a pins, a list of pad_b
|
```python
|
||||||
# pins, and the encoder map created above
|
import board
|
||||||
encoder_ext = EncoderHandler([board.D40],[board.D41], encoder_map)
|
|
||||||
|
from kmk.kmk_keyboard import KMKKeyboard
|
||||||
# if desired, you can flip the incrfement/decrement direction of the knob by
|
from kmk.consts import UnicodeMode
|
||||||
# setting the is_inerted flag to True. If you turn the knob to the right and
|
from kmk.keys import KC
|
||||||
# the volume goes down, setting this flag will make it go up. It's default
|
from kmk.matrix import DiodeOrientation
|
||||||
# setting is False
|
from kmk.modules.layers import Layers
|
||||||
encoder_ext.encoders[0].is_inverted = True
|
from kmk.modules.encoder import EncoderHandler
|
||||||
|
|
||||||
# Make sure to add the encoder_ext to the modules list
|
|
||||||
keyboard.modules = [encoder_ext]
|
keyboard = KMKKeyboard()
|
||||||
```
|
layers = Layers()
|
||||||
|
encoder_handler = EncoderHandler()
|
||||||
|
keyboard.modules = [layers, encoder_handler]
|
||||||
|
|
||||||
|
|
||||||
|
keyboard.col_pins = (
|
||||||
|
board.GP0, board.GP1, board.GP2, board.GP3, board.GP4, board.GP5,
|
||||||
|
board.GP6, board.GP7, board.GP8, board.GP9, board.GP10, board.GP11,
|
||||||
|
board.GP12, board.GP13,
|
||||||
|
)
|
||||||
|
keyboard.row_pins = (board.GP28, board.GP27, board.GP22, board.GP26, board.GP21)
|
||||||
|
keyboard.diode_orientation = DiodeOrientation.COLUMNS
|
||||||
|
encoder_handler.pins = ((board.GP17, board.GP15, board.GP14, False),)
|
||||||
|
|
||||||
|
keyboard.tap_time = 250
|
||||||
|
keyboard.debug_enabled = False
|
||||||
|
|
||||||
|
|
||||||
|
# Filler keys
|
||||||
|
_______ = KC.TRNS
|
||||||
|
xxxxxxx = KC.NO
|
||||||
|
tbdtbd = KC.A
|
||||||
|
|
||||||
|
|
||||||
|
# Layers
|
||||||
|
LYR_STD, LYR_EXT, LYR_NUM, LYR_GAME = 0, 1, 2, 3
|
||||||
|
|
||||||
|
TO_STD = KC.DF(LYR_STD)
|
||||||
|
MT_EXT = KC.MO(LYR_EXT)
|
||||||
|
TO_NUM = KC.MO(LYR_NUM)
|
||||||
|
TO_GAME = KC.DF(LYR_GAME)
|
||||||
|
|
||||||
|
|
||||||
|
# Keymap
|
||||||
|
|
||||||
|
keyboard.keymap = [
|
||||||
|
# Standard (ISO) Layer
|
||||||
|
[
|
||||||
|
KC.ESC , KC.N1 , KC.N2 , KC.N3 , KC.N4 , KC.N5 , KC.N6 , KC.N7 , KC.N8 , KC.N9 , KC.N0 , KC.MINS, KC.EQL , KC.BSPC,
|
||||||
|
KC.TAB , KC.Q , KC.W , KC.E , KC.R , KC.T , KC.Y , KC.U , KC.I , KC.O , KC.P , KC.LBRC, KC.RBRC, KC.DEL ,
|
||||||
|
xxxxxxx, KC.A , KC.S , KC.D , KC.F , KC.G , KC.H , KC.J , KC.K , KC.L , KC.SCLN, KC.QUOT, KC.NUHS, xxxxxxx,
|
||||||
|
KC.LSFT, KC.NUBS, KC.Z , KC.X , KC.C , KC.V , KC.B , KC.N , KC.M , KC.COMM, KC.DOT , KC.SLSH, KC.UP , KC.ENT ,
|
||||||
|
KC.LCTL, KC.LGUI, xxxxxxx, KC.LALT, MT_EXT , xxxxxxx, KC.SPC , xxxxxxx, KC.RALT, TO_NUM , KC.RSFT, KC.LEFT, KC.DOWN, KC.RGHT,
|
||||||
|
],
|
||||||
|
# Extra Keys Layer
|
||||||
|
[
|
||||||
|
TO_STD , KC.F1 , KC.F2 , KC.F3 , KC.F4 , KC.F5 , KC.F6 , KC.F7 , KC.F8 , KC.F9 , KC.F10 , KC.F11 , KC.F12 , KC.RESET,
|
||||||
|
_______, KC.N1 , KC.N2 , KC.N3 , KC.N4 , KC.N5 , KC.N6 , KC.N7 , KC.N8 , KC.N9 , KC.N0 , KC.MINS, KC.EQL , _______,
|
||||||
|
xxxxxxx, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, xxxxxxx,
|
||||||
|
KC.LSFT, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, KC.PGUP, _______,
|
||||||
|
KC.LCTL, KC.LGUI, xxxxxxx, KC.LALT, MT_EXT , xxxxxxx, _______, xxxxxxx, _______, TO_NUM , _______, KC.HOME, KC.PGDN, KC.END ,
|
||||||
|
],
|
||||||
|
# NumPad Layer
|
||||||
|
[
|
||||||
|
TO_STD , xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, KC.P7 , KC.P8 , KC.P9 , KC.PSLS, xxxxxxx, xxxxxxx, KC.BSPC,
|
||||||
|
xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, KC.P4 , KC.P5 , KC.P6 , KC.PAST, xxxxxxx, xxxxxxx, KC.DEL ,
|
||||||
|
xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, KC.LPRN, KC.P1 , KC.P2 , KC.P3 , KC.PPLS, xxxxxxx, xxxxxxx, xxxxxxx,
|
||||||
|
xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, KC.RPRN, KC.P0 , KC.PDOT, _______, KC.PMNS, xxxxxxx, xxxxxxx, KC.PENT,
|
||||||
|
xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, MT_EXT , xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, TO_NUM , xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx,
|
||||||
|
],
|
||||||
|
# Gaming Layer
|
||||||
|
[
|
||||||
|
TO_STD , xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx,
|
||||||
|
xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx,
|
||||||
|
xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx,
|
||||||
|
xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx,
|
||||||
|
xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, MT_EXT , xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, TO_NUM , xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx,
|
||||||
|
],
|
||||||
|
]
|
||||||
|
|
||||||
|
# Rotary Encoder (1 encoder / 1 definition per layer)
|
||||||
|
encoder_handler.map = ( ((KC.UP, KC.DOWN, KC.MUTE),), # Standard
|
||||||
|
((KC.VOLD, KC.VOLU, KC.MUTE),), # Extra
|
||||||
|
((KC.A, KC.Z, KC.N1),), # NumPad not yet properly configured
|
||||||
|
((KC.A, KC.Z, KC.N1),), # Gaming not yet properly configured
|
||||||
|
)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
keyboard.go()
|
||||||
|
@ -1,172 +1,121 @@
|
|||||||
import digitalio
|
# See docs/encoder.md for how to use
|
||||||
from supervisor import ticks_ms
|
|
||||||
|
|
||||||
|
import digitalio
|
||||||
|
|
||||||
|
from supervisor import ticks_ms
|
||||||
from kmk.modules import Module
|
from kmk.modules import Module
|
||||||
|
|
||||||
|
# NB : not using rotaryio as it requires the pins to be consecutive
|
||||||
class EncoderPadState:
|
|
||||||
OFF = False
|
|
||||||
ON = True
|
|
||||||
|
|
||||||
|
|
||||||
class EndcoderDirection:
|
|
||||||
Left = False
|
|
||||||
Right = True
|
|
||||||
|
|
||||||
|
|
||||||
class Encoder:
|
class Encoder:
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
pad_a,
|
|
||||||
pad_b,
|
|
||||||
button_pin=None,
|
|
||||||
):
|
|
||||||
self.pad_a = self.PreparePin(pad_a) # board pin for enc pin a
|
|
||||||
self.pad_a_state = False
|
|
||||||
self.pad_b = self.PreparePin(pad_b) # board pin for enc pin b
|
|
||||||
self.pad_b_state = False
|
|
||||||
self.button_pin = self.PreparePin(button_pin) # board pin for enc btn
|
|
||||||
self.button_state = None # state of pushbutton on encoder if enabled
|
|
||||||
self.encoder_value = 0 # clarify what this value is
|
|
||||||
self.encoder_state = (
|
|
||||||
self.pad_a_state,
|
|
||||||
self.pad_b_state,
|
|
||||||
) # quaderature encoder state
|
|
||||||
self.encoder_direction = None # arbitrary, tells direction of knob
|
|
||||||
self.last_encoder_state = None # not used yet
|
|
||||||
self.resolution = 2 # number of keys sent per position change
|
|
||||||
self.revolution_count = 20 # position changes per revolution
|
|
||||||
self.has_button = False # enable/disable button functionality
|
|
||||||
self.encoder_data = None # 6tuple containing all encoder data
|
|
||||||
self.position_change = None # revolution count, inc/dec as knob turns
|
|
||||||
self.last_encoder_value = 0 # not used
|
|
||||||
self.is_inverted = False # switch to invert knob direction
|
|
||||||
self.vel_mode = False # enable the velocity output
|
|
||||||
self.vel_ts = None # velocity timestamp
|
|
||||||
self.last_vel_ts = 0 # last velocity timestamp
|
|
||||||
self.encoder_speed = None # ms per position change(4 states)
|
|
||||||
self.encoder_map = None
|
|
||||||
self.eps = EncoderPadState()
|
|
||||||
self.encoder_pad_lookup = {
|
|
||||||
False: self.eps.OFF,
|
|
||||||
True: self.eps.ON,
|
|
||||||
}
|
|
||||||
self.edr = EndcoderDirection() # lookup for current encoder direction
|
|
||||||
self.encoder_dir_lookup = {
|
|
||||||
False: self.edr.Left,
|
|
||||||
True: self.edr.Right,
|
|
||||||
}
|
|
||||||
|
|
||||||
def __repr__(self, idx):
|
VELOCITY_MODE = True
|
||||||
return 'ENCODER_{}({})'.format(idx, self._to_dict())
|
|
||||||
|
|
||||||
def _to_dict(self):
|
def __init__(self, pin_a, pin_b, pin_button=None, is_inverted=False):
|
||||||
|
self.pin_a = EncoderPin(pin_a)
|
||||||
|
self.pin_b = EncoderPin(pin_b)
|
||||||
|
self.pin_button = EncoderPin(pin_button, button_type=True)
|
||||||
|
self.is_inverted = is_inverted
|
||||||
|
|
||||||
|
self._state = (self.pin_a.get_value(), self.pin_b.get_value())
|
||||||
|
self._direction = None
|
||||||
|
self._pos = 0
|
||||||
|
self._button_state = True
|
||||||
|
self._velocity = 0
|
||||||
|
|
||||||
|
self._movement = 0
|
||||||
|
self._timestamp = ticks_ms()
|
||||||
|
|
||||||
|
# callback functions on events. Need to be defined externally
|
||||||
|
self.on_move_do = None
|
||||||
|
self.on_button_do = None
|
||||||
|
|
||||||
|
def get_state(self):
|
||||||
return {
|
return {
|
||||||
'Encoder_State': self.encoder_state,
|
'direction': self.is_inverted and -self._direction or self._direction,
|
||||||
'Direction': self.encoder_direction,
|
'position': self.is_inverted and -self._pos or self._pos,
|
||||||
'Value': self.encoder_value,
|
'is_pressed': not self._button_state,
|
||||||
'Position_Change': self.position_change,
|
'velocity': self._velocity,
|
||||||
'Speed': self.encoder_speed,
|
|
||||||
'Button_State': self.button_state,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# adapted for CircuitPython from raspi
|
# Called in a loop to refresh encoder state
|
||||||
def PreparePin(self, num):
|
def update_state(self):
|
||||||
if num is not None:
|
# Rotation events
|
||||||
pad = digitalio.DigitalInOut(num)
|
new_state = (self.pin_a.get_value(), self.pin_b.get_value())
|
||||||
pad.direction = digitalio.Direction.INPUT
|
|
||||||
pad.pull = digitalio.Pull.UP
|
|
||||||
return pad
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
# checks encoder pins, reports encoder data
|
if new_state != self._state:
|
||||||
def report(self):
|
# it moves !
|
||||||
new_encoder_state = (
|
self._movement += 1
|
||||||
self.encoder_pad_lookup[int(self.pad_a.value)],
|
# false / false and true / true are common half steps
|
||||||
self.encoder_pad_lookup[int(self.pad_b.value)],
|
# looking on the step just before helps determining
|
||||||
)
|
# the direction
|
||||||
|
if new_state[0] == new_state[1] and self._state[0] != self._state[1]:
|
||||||
|
if new_state[1] == self._state[0]:
|
||||||
|
self._direction = 1
|
||||||
|
else:
|
||||||
|
self._direction = -1
|
||||||
|
|
||||||
if self.encoder_state == (self.eps.ON, self.eps.ON): # Resting position
|
# when the encoder settles on a position (every 2 steps)
|
||||||
if new_encoder_state == (self.eps.ON, self.eps.OFF): # Turned right 1
|
if new_state == (True, True):
|
||||||
self.encoder_direction = self.edr.Right
|
if self._movement > 2:
|
||||||
elif new_encoder_state == (self.eps.OFF, self.eps.ON): # Turned left 1
|
# 1 full step is 4 movements, however, when rotated quickly,
|
||||||
self.encoder_direction = self.edr.Left
|
# some steps may be missed. This makes it behaves more
|
||||||
elif self.encoder_state == (self.eps.ON, self.eps.OFF): # R1 or L3 position
|
# naturally
|
||||||
if new_encoder_state == (self.eps.OFF, self.eps.OFF): # Turned right 1
|
real_movement = round(self._movement / 4)
|
||||||
self.encoder_direction = self.edr.Right
|
self._pos += self._direction * real_movement
|
||||||
elif new_encoder_state == (self.eps.ON, self.eps.ON): # Turned left 1
|
if self.on_move_do is not None:
|
||||||
if self.encoder_direction == self.edr.Left:
|
for i in range(real_movement):
|
||||||
self.encoder_value = self.encoder_value - 1
|
self.on_move_do(self.get_state())
|
||||||
elif self.encoder_state == (self.eps.OFF, self.eps.ON): # R3 or L1
|
# Reinit to properly identify new movement
|
||||||
if new_encoder_state == (self.eps.OFF, self.eps.OFF): # Turned left 1
|
self._movement = 0
|
||||||
self.encoder_direction = self.edr.Left
|
self._direction = 0
|
||||||
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
|
self._state = new_state
|
||||||
|
|
||||||
if self.vel_mode:
|
# Velocity
|
||||||
self.vel_ts = ticks_ms()
|
if VELOCITY_MODE:
|
||||||
|
new_timestamp = ticks_ms()
|
||||||
|
self._velocity = new_timestamp - self._timestamp
|
||||||
|
self._timestamp = new_timestamp
|
||||||
|
|
||||||
if self.encoder_state != self.last_encoder_state:
|
# Button events
|
||||||
self.position_change = self.invert_rotation(
|
new_button_state = self.pin_button.get_value()
|
||||||
self.encoder_value, self.last_encoder_value
|
if new_button_state != self._button_state:
|
||||||
)
|
self._button_state = new_button_state
|
||||||
|
if self.on_button_do is not None:
|
||||||
|
self.on_button_do(self.get_state())
|
||||||
|
|
||||||
self.last_encoder_state = self.encoder_state
|
# returnd knob velocity as milliseconds between position changes (detents)
|
||||||
self.last_encoder_value = self.encoder_value
|
# for backwards compatibility
|
||||||
|
|
||||||
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, old):
|
|
||||||
if self.is_inverted:
|
|
||||||
return -(new - old)
|
|
||||||
else:
|
|
||||||
return new - old
|
|
||||||
|
|
||||||
# returns knob velocity as milliseconds between position changes(detents)
|
|
||||||
def vel_report(self):
|
def vel_report(self):
|
||||||
self.encoder_speed = self.vel_ts - self.last_vel_ts
|
print(self._velocity)
|
||||||
self.last_vel_ts = self.vel_ts
|
return self._velocity
|
||||||
return self.encoder_speed
|
|
||||||
|
|
||||||
|
class EncoderPin:
|
||||||
|
def __init__(self, pin, button_type=False):
|
||||||
|
self.pin = pin
|
||||||
|
self.button_type = button_type
|
||||||
|
self.prepare_pin()
|
||||||
|
|
||||||
|
def prepare_pin(self):
|
||||||
|
if self.pin is not None:
|
||||||
|
self.io = digitalio.DigitalInOut(self.pin)
|
||||||
|
self.io.direction = digitalio.Direction.INPUT
|
||||||
|
self.io.pull = digitalio.Pull.UP
|
||||||
|
else:
|
||||||
|
self.io = None
|
||||||
|
|
||||||
|
def get_value(self):
|
||||||
|
return self.io.value
|
||||||
|
|
||||||
|
|
||||||
class EncoderHandler(Module):
|
class EncoderHandler(Module):
|
||||||
|
def __init__(self):
|
||||||
encoders = []
|
self.encoders = []
|
||||||
debug_enabled = False # not working as inttended, do not use for now
|
self.pins = None
|
||||||
|
self.map = None
|
||||||
def __init__(self, pad_a, pad_b, encoder_map):
|
|
||||||
self.pad_a = pad_a
|
|
||||||
self.pad_b = pad_b
|
|
||||||
self.encoder_count = len(self.pad_a)
|
|
||||||
self.encoder_map = encoder_map
|
|
||||||
self.make_encoders()
|
|
||||||
|
|
||||||
def on_runtime_enable(self, keyboard):
|
def on_runtime_enable(self, keyboard):
|
||||||
return
|
return
|
||||||
@ -175,13 +124,41 @@ class EncoderHandler(Module):
|
|||||||
return
|
return
|
||||||
|
|
||||||
def during_bootup(self, keyboard):
|
def during_bootup(self, keyboard):
|
||||||
|
if self.pins and self.map:
|
||||||
|
for idx, pins in enumerate(self.pins):
|
||||||
|
gpio_pins = pins[:3]
|
||||||
|
new_encoder = Encoder(*gpio_pins)
|
||||||
|
# In our case, we need to define keybord and encoder_id for callbacks
|
||||||
|
new_encoder.on_move_do = lambda x: self.on_move_do(keyboard, idx, x)
|
||||||
|
new_encoder.on_button_do = lambda x: self.on_button_do(keyboard, idx, x)
|
||||||
|
self.encoders.append(new_encoder)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
def on_move_do(self, keyboard, encoder_id, state):
|
||||||
|
if self.map:
|
||||||
|
layer_id = keyboard.active_layers[0]
|
||||||
|
# if Left, key index 0 else key index 1
|
||||||
|
if state['direction'] == -1:
|
||||||
|
key_index = 0
|
||||||
|
else:
|
||||||
|
key_index = 1
|
||||||
|
key = self.map[layer_id][encoder_id][key_index]
|
||||||
|
keyboard.tap_key(key)
|
||||||
|
|
||||||
|
def on_button_do(self, keyboard, encoder_id, state):
|
||||||
|
if state['is_pressed'] is True:
|
||||||
|
layer_id = keyboard.active_layers[0]
|
||||||
|
key = self.map[layer_id][encoder_id][2]
|
||||||
|
keyboard.tap_key(key)
|
||||||
|
|
||||||
def before_matrix_scan(self, keyboard):
|
def before_matrix_scan(self, keyboard):
|
||||||
'''
|
'''
|
||||||
Return value will be injected as an extra matrix update
|
Return value will be injected as an extra matrix update
|
||||||
'''
|
'''
|
||||||
return self.get_reports(keyboard)
|
for encoder in self.encoders:
|
||||||
|
encoder.update_state()
|
||||||
|
|
||||||
|
return keyboard
|
||||||
|
|
||||||
def after_matrix_scan(self, keyboard):
|
def after_matrix_scan(self, keyboard):
|
||||||
'''
|
'''
|
||||||
@ -200,31 +177,3 @@ class EncoderHandler(Module):
|
|||||||
|
|
||||||
def on_powersave_disable(self, keyboard):
|
def on_powersave_disable(self, keyboard):
|
||||||
return
|
return
|
||||||
|
|
||||||
def make_encoders(self):
|
|
||||||
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, encoder_key, encoder_idx):
|
|
||||||
# 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):
|
|
||||||
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)
|
|
||||||
|
@ -19,6 +19,7 @@
|
|||||||
|
|
||||||
import digitalio
|
import digitalio
|
||||||
|
|
||||||
|
from kmk.kmktime import tick_ms
|
||||||
from kmk.modules import Module
|
from kmk.modules import Module
|
||||||
|
|
||||||
# NB : not using rotaryio as it requires the pins to be consecutive
|
# NB : not using rotaryio as it requires the pins to be consecutive
|
||||||
@ -26,9 +27,7 @@ from kmk.modules import Module
|
|||||||
|
|
||||||
class Encoder:
|
class Encoder:
|
||||||
|
|
||||||
_debug = False
|
VELOCITY_MODE = True
|
||||||
_debug_counter = 0
|
|
||||||
|
|
||||||
STATES = {
|
STATES = {
|
||||||
# old_pos_a, old_pos_b, new_pos_a, new_pos_b
|
# old_pos_a, old_pos_b, new_pos_a, new_pos_b
|
||||||
# -1 : Left ; 1 : Right ; 0 : we don't care
|
# -1 : Left ; 1 : Right ; 0 : we don't care
|
||||||
@ -52,11 +51,14 @@ class Encoder:
|
|||||||
self.pin_button = EncoderPin(pin_button, button_type=True)
|
self.pin_button = EncoderPin(pin_button, button_type=True)
|
||||||
self.is_inverted = is_inverted
|
self.is_inverted = is_inverted
|
||||||
|
|
||||||
self._actual_state = (self.pin_a.get_value(), self.pin_b.get_value())
|
self._state = (self.pin_a.get_value(), self.pin_b.get_value())
|
||||||
self._actual_direction = None
|
self._direction = None
|
||||||
self._actual_pos = 0
|
self._pos = 0
|
||||||
self._actual_button_state = True
|
self._button_state = True
|
||||||
self._movement_counter = 0
|
self._velocity = 0
|
||||||
|
|
||||||
|
self._movement = 0
|
||||||
|
self._timestamp = tick_ms()
|
||||||
|
|
||||||
# callback functions on events. Need to be defined externally
|
# callback functions on events. Need to be defined externally
|
||||||
self.on_move_do = None
|
self.on_move_do = None
|
||||||
@ -65,45 +67,59 @@ class Encoder:
|
|||||||
def get_state(self):
|
def get_state(self):
|
||||||
return {
|
return {
|
||||||
'direction': self.is_inverted
|
'direction': self.is_inverted
|
||||||
and -self._actual_direction
|
and -self._direction
|
||||||
or self._actual_direction,
|
or self._direction,
|
||||||
'position': self.is_inverted and -self._actual_pos or self._actual_pos,
|
'position': self.is_inverted and -self._pos or self._pos,
|
||||||
'is_pressed': not self._actual_button_state,
|
'is_pressed': not self._button_state,
|
||||||
|
'velocity': self.velocity
|
||||||
}
|
}
|
||||||
|
|
||||||
# to be called in a loop
|
# Called in a loop to refresh encoder state
|
||||||
def update_state(self):
|
def update_state(self):
|
||||||
# Rotation events
|
# Rotation events
|
||||||
new_state = (self.pin_a.get_value(), self.pin_b.get_value())
|
new_state = (self.pin_a.get_value(), self.pin_b.get_value())
|
||||||
if new_state != self._actual_state:
|
if new_state != self._state:
|
||||||
if self._debug:
|
self._movement += 1
|
||||||
print(' ', new_state)
|
new_direction = self.STATES[(self._state, new_state)]
|
||||||
self._movement_counter += 1
|
|
||||||
new_direction = self.STATES[(self._actual_state, new_state)]
|
|
||||||
if new_direction != 0:
|
if new_direction != 0:
|
||||||
self._actual_direction = new_direction
|
self._direction = new_direction
|
||||||
|
|
||||||
# when the encoder settles on a position
|
# when the encoder settles on a position (every 2 steps)
|
||||||
if (
|
if (
|
||||||
new_state == (True, True) and self._movement_counter > 2
|
new_state == (True, True) and self._movement > 2
|
||||||
): # if < 2 state changes, it is a misstep
|
): # if < 2 state changes, it is a misstep
|
||||||
self._movement_counter = 0
|
self._movement = 0
|
||||||
self._actual_pos += self._actual_direction
|
self._pos += self._direction
|
||||||
if self._debug:
|
|
||||||
self._debug_counter += 1
|
|
||||||
print(self._debug_counter, self.get_state())
|
|
||||||
if self.on_move_do is not None:
|
if self.on_move_do is not None:
|
||||||
self.on_move_do(self.get_state())
|
self.on_move_do(self.get_state())
|
||||||
|
|
||||||
self._actual_state = new_state
|
self._state = new_state
|
||||||
|
|
||||||
|
# Velocity
|
||||||
|
if VELOCITY_MODE:
|
||||||
|
new_timestamp = tick_ms()
|
||||||
|
self._velocity = new_timestamp - self._timestamp
|
||||||
|
self._timestamp = new_timestamp
|
||||||
|
|
||||||
# Button events
|
# Button events
|
||||||
new_button_state = self.pin_button.get_value()
|
new_button_state = self.pin_button.get_value()
|
||||||
if new_button_state != self._actual_button_state:
|
if new_button_state != self._button_state:
|
||||||
self._actual_button_state = new_button_state
|
self._button_state = new_button_state
|
||||||
if self.on_button_do is not None:
|
if self.on_button_do is not None:
|
||||||
self.on_button_do(self.get_state())
|
self.on_button_do(self.get_state())
|
||||||
|
|
||||||
|
# returnd knob velocity as milliseconds between position changes (detents)
|
||||||
|
def vel_report(self):
|
||||||
|
return self._velocity
|
||||||
|
|
||||||
|
# callback for actions on move (set up externally)
|
||||||
|
def on_move_do(self, :
|
||||||
|
return
|
||||||
|
|
||||||
|
# callback for actions on button press (set up externally)
|
||||||
|
def on_button_do:
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
class EncoderPin:
|
class EncoderPin:
|
||||||
def __init__(self, pin, button_type=False):
|
def __init__(self, pin, button_type=False):
|
||||||
@ -140,7 +156,7 @@ class EncoderHandler(Module):
|
|||||||
for idx, pins in enumerate(self.pins):
|
for idx, pins in enumerate(self.pins):
|
||||||
gpio_pins = pins[:3]
|
gpio_pins = pins[:3]
|
||||||
new_encoder = Encoder(*gpio_pins)
|
new_encoder = Encoder(*gpio_pins)
|
||||||
# In our case, we need to fix keybord and encoder_id for the callback
|
# In our case, we need to define keybord and encoder_id for callbacks
|
||||||
new_encoder.on_move_do = lambda x: self.on_move_do(keyboard, idx, x)
|
new_encoder.on_move_do = lambda x: self.on_move_do(keyboard, idx, x)
|
||||||
new_encoder.on_button_do = lambda x: self.on_button_do(keyboard, idx, x)
|
new_encoder.on_button_do = lambda x: self.on_button_do(keyboard, idx, x)
|
||||||
self.encoders.append(new_encoder)
|
self.encoders.append(new_encoder)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user