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
|
||||
Add twist control to your keyboard! Volume, zoom, anything you want.
|
||||
|
||||
## 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,
|
||||
and an encoder_map. 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
|
||||
layers that you don't require any action. The encoder supports a velocity mode
|
||||
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.
|
||||
|
||||
## Configuration
|
||||
|
||||
There is a complete example in the Atreus62 [main.py](/boards/atreus62/main.py)
|
||||
|
||||
Create the encoder_map.
|
||||
|
||||
> Anatomy of an encoder_map tuple: (increment_key, decrement_key, keys presses per encoder click)
|
||||
> `encoder_map` is a Nested List with a Tuple as the list element (`List[List[Tuple(Key,Key,Int)]]`)
|
||||
|
||||
```python
|
||||
from kmk.modules.encoder import EncoderHandler # import the Encoder module
|
||||
|
||||
Zoom_in = KC.LCTRL(KC.EQUAL)
|
||||
Zoom_out = KC.LCTRL(KC.MINUS)
|
||||
|
||||
# create the encoder map, modeled after the keymap
|
||||
encoder_map = [
|
||||
[
|
||||
# Only 1 encoder is being used, so only one tuple per layer is required
|
||||
# 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),
|
||||
],
|
||||
[
|
||||
# only one key press sent per encoder click
|
||||
(Zoom_in, Zoom_out,1),
|
||||
],
|
||||
[
|
||||
# No action keys sent here, the resolution is a dummy number, to be
|
||||
# removed in the future.
|
||||
(_______,_______,1),#
|
||||
]
|
||||
]
|
||||
|
||||
# create the encoder instance, and pass in a list of pad_a pins, a list of pad_b
|
||||
# pins, and the encoder map created above
|
||||
encoder_ext = EncoderHandler([board.D40],[board.D41], encoder_map)
|
||||
|
||||
# if desired, you can flip the incrfement/decrement direction of the knob by
|
||||
# setting the is_inerted flag to True. If you turn the knob to the right and
|
||||
# the volume goes down, setting this flag will make it go up. It's default
|
||||
# setting is False
|
||||
encoder_ext.encoders[0].is_inverted = True
|
||||
|
||||
# Make sure to add the encoder_ext to the modules list
|
||||
keyboard.modules = [encoder_ext]
|
||||
```
|
||||
# Encoder module
|
||||
Add twist control to your keyboard! Volume, zoom, anything you want
|
||||
|
||||
## Enabling the extension
|
||||
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
|
||||
The encoder_map is modeled after the keymap and works the
|
||||
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.
|
||||
The encoder supports a velocity mode if you desire to make something for video or sound editing.
|
||||
|
||||
|
||||
|
||||
## How to use
|
||||
How to use this module in your main / code file
|
||||
|
||||
1. load the module
|
||||
```python
|
||||
from kmk.modules.encoder import EncoderHandler
|
||||
encoder_handler = EncoderHandler()
|
||||
keyboard.modules = [layers, modtap, encoder_handler]
|
||||
```
|
||||
|
||||
2. Define the pins for each encoder (pin_a, pin_b, pin_button, True for an inversed encoder)
|
||||
```python
|
||||
encoder_handler.pins = ((board.GP17, board.GP15, board.GP14, False), (encoder 2 definition), etc. )
|
||||
```
|
||||
|
||||
3. Define the mapping of keys to be called (1 / layer)
|
||||
```python
|
||||
# You can optionally predefine combo keys as for your layout
|
||||
Zoom_in = KC.LCTRL(KC.EQUAL)
|
||||
Zoom_out = KC.LCTRL(KC.MINUS)
|
||||
|
||||
|
||||
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
|
||||
((KC.NO, KC.NO, KC.NO),(encoder 2 definition), etc. ), # Layer 4
|
||||
]
|
||||
```
|
||||
|
||||
|
||||
|
||||
4. Encoder methods on_move_do and on_button_do can be overwritten for complex use cases
|
||||
|
||||
## Full example (with 1 encoder)
|
||||
|
||||
```python
|
||||
import board
|
||||
|
||||
from kmk.kmk_keyboard import KMKKeyboard
|
||||
from kmk.consts import UnicodeMode
|
||||
from kmk.keys import KC
|
||||
from kmk.matrix import DiodeOrientation
|
||||
from kmk.modules.layers import Layers
|
||||
from kmk.modules.encoder import EncoderHandler
|
||||
|
||||
|
||||
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
|
||||
from supervisor import ticks_ms
|
||||
# See docs/encoder.md for how to use
|
||||
|
||||
import digitalio
|
||||
|
||||
from supervisor import ticks_ms
|
||||
from kmk.modules import Module
|
||||
|
||||
|
||||
class EncoderPadState:
|
||||
OFF = False
|
||||
ON = True
|
||||
|
||||
|
||||
class EndcoderDirection:
|
||||
Left = False
|
||||
Right = True
|
||||
# NB : not using rotaryio as it requires the pins to be consecutive
|
||||
|
||||
|
||||
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):
|
||||
return 'ENCODER_{}({})'.format(idx, self._to_dict())
|
||||
VELOCITY_MODE = True
|
||||
|
||||
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 {
|
||||
'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,
|
||||
'direction': self.is_inverted and -self._direction or self._direction,
|
||||
'position': self.is_inverted and -self._pos or self._pos,
|
||||
'is_pressed': not self._button_state,
|
||||
'velocity': self._velocity,
|
||||
}
|
||||
|
||||
# adapted for CircuitPython from raspi
|
||||
def PreparePin(self, num):
|
||||
if num is not None:
|
||||
pad = digitalio.DigitalInOut(num)
|
||||
pad.direction = digitalio.Direction.INPUT
|
||||
pad.pull = digitalio.Pull.UP
|
||||
return pad
|
||||
else:
|
||||
return None
|
||||
# Called in a loop to refresh encoder state
|
||||
def update_state(self):
|
||||
# Rotation events
|
||||
new_state = (self.pin_a.get_value(), self.pin_b.get_value())
|
||||
|
||||
# checks encoder pins, reports encoder data
|
||||
def report(self):
|
||||
new_encoder_state = (
|
||||
self.encoder_pad_lookup[int(self.pad_a.value)],
|
||||
self.encoder_pad_lookup[int(self.pad_b.value)],
|
||||
)
|
||||
if new_state != self._state:
|
||||
# it moves !
|
||||
self._movement += 1
|
||||
# false / false and true / true are common half steps
|
||||
# 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
|
||||
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
|
||||
# when the encoder settles on a position (every 2 steps)
|
||||
if new_state == (True, True):
|
||||
if self._movement > 2:
|
||||
# 1 full step is 4 movements, however, when rotated quickly,
|
||||
# some steps may be missed. This makes it behaves more
|
||||
# naturally
|
||||
real_movement = round(self._movement / 4)
|
||||
self._pos += self._direction * real_movement
|
||||
if self.on_move_do is not None:
|
||||
for i in range(real_movement):
|
||||
self.on_move_do(self.get_state())
|
||||
# Reinit to properly identify new movement
|
||||
self._movement = 0
|
||||
self._direction = 0
|
||||
|
||||
self.encoder_state = new_encoder_state
|
||||
self._state = new_state
|
||||
|
||||
if self.vel_mode:
|
||||
self.vel_ts = ticks_ms()
|
||||
# Velocity
|
||||
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:
|
||||
self.position_change = self.invert_rotation(
|
||||
self.encoder_value, self.last_encoder_value
|
||||
)
|
||||
# Button events
|
||||
new_button_state = self.pin_button.get_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
|
||||
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, old):
|
||||
if self.is_inverted:
|
||||
return -(new - old)
|
||||
else:
|
||||
return new - old
|
||||
|
||||
# returns knob velocity as milliseconds between position changes(detents)
|
||||
# returnd knob velocity as milliseconds between position changes (detents)
|
||||
# for backwards compatibility
|
||||
def vel_report(self):
|
||||
self.encoder_speed = self.vel_ts - self.last_vel_ts
|
||||
self.last_vel_ts = self.vel_ts
|
||||
return self.encoder_speed
|
||||
print(self._velocity)
|
||||
return self._velocity
|
||||
|
||||
|
||||
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):
|
||||
|
||||
encoders = []
|
||||
debug_enabled = False # not working as inttended, do not use for now
|
||||
|
||||
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 __init__(self):
|
||||
self.encoders = []
|
||||
self.pins = None
|
||||
self.map = None
|
||||
|
||||
def on_runtime_enable(self, keyboard):
|
||||
return
|
||||
@ -175,13 +124,41 @@ class EncoderHandler(Module):
|
||||
return
|
||||
|
||||
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
|
||||
|
||||
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):
|
||||
'''
|
||||
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):
|
||||
'''
|
||||
@ -200,31 +177,3 @@ class EncoderHandler(Module):
|
||||
|
||||
def on_powersave_disable(self, keyboard):
|
||||
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
|
||||
|
||||
from kmk.kmktime import tick_ms
|
||||
from kmk.modules import Module
|
||||
|
||||
# NB : not using rotaryio as it requires the pins to be consecutive
|
||||
@ -26,9 +27,7 @@ from kmk.modules import Module
|
||||
|
||||
class Encoder:
|
||||
|
||||
_debug = False
|
||||
_debug_counter = 0
|
||||
|
||||
VELOCITY_MODE = True
|
||||
STATES = {
|
||||
# old_pos_a, old_pos_b, new_pos_a, new_pos_b
|
||||
# -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.is_inverted = is_inverted
|
||||
|
||||
self._actual_state = (self.pin_a.get_value(), self.pin_b.get_value())
|
||||
self._actual_direction = None
|
||||
self._actual_pos = 0
|
||||
self._actual_button_state = True
|
||||
self._movement_counter = 0
|
||||
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 = tick_ms()
|
||||
|
||||
# callback functions on events. Need to be defined externally
|
||||
self.on_move_do = None
|
||||
@ -65,45 +67,59 @@ class Encoder:
|
||||
def get_state(self):
|
||||
return {
|
||||
'direction': self.is_inverted
|
||||
and -self._actual_direction
|
||||
or self._actual_direction,
|
||||
'position': self.is_inverted and -self._actual_pos or self._actual_pos,
|
||||
'is_pressed': not self._actual_button_state,
|
||||
and -self._direction
|
||||
or self._direction,
|
||||
'position': self.is_inverted and -self._pos or self._pos,
|
||||
'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):
|
||||
# Rotation events
|
||||
new_state = (self.pin_a.get_value(), self.pin_b.get_value())
|
||||
if new_state != self._actual_state:
|
||||
if self._debug:
|
||||
print(' ', new_state)
|
||||
self._movement_counter += 1
|
||||
new_direction = self.STATES[(self._actual_state, new_state)]
|
||||
if new_state != self._state:
|
||||
self._movement += 1
|
||||
new_direction = self.STATES[(self._state, new_state)]
|
||||
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 (
|
||||
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
|
||||
self._movement_counter = 0
|
||||
self._actual_pos += self._actual_direction
|
||||
if self._debug:
|
||||
self._debug_counter += 1
|
||||
print(self._debug_counter, self.get_state())
|
||||
self._movement = 0
|
||||
self._pos += self._direction
|
||||
if self.on_move_do is not None:
|
||||
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
|
||||
new_button_state = self.pin_button.get_value()
|
||||
if new_button_state != self._actual_button_state:
|
||||
self._actual_button_state = new_button_state
|
||||
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())
|
||||
|
||||
# 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:
|
||||
def __init__(self, pin, button_type=False):
|
||||
@ -140,7 +156,7 @@ class EncoderHandler(Module):
|
||||
for idx, pins in enumerate(self.pins):
|
||||
gpio_pins = pins[:3]
|
||||
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_button_do = lambda x: self.on_button_do(keyboard, idx, x)
|
||||
self.encoders.append(new_encoder)
|
||||
|
Loading…
x
Reference in New Issue
Block a user