Encoder class rebuild
implement BaseEncoder
This commit is contained in:
parent
0e029ebf72
commit
8e4ab1d733
@ -1,10 +1,15 @@
|
||||
# Encoder module
|
||||
Add twist control to your keyboard! Volume, zoom, anything you want
|
||||
|
||||
I2C encoder type has been tested with the Adafruit I2C QT Rotary Encoder with NeoPixel
|
||||
|
||||
## 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 constructor(`EncoderHandler` class) takes a list of encoder, each one defined as either:
|
||||
|
||||
* a list of pad_a pin, pad_b pin, button_pin and optionnally a flag set to True is you want it to be reversed
|
||||
* a `busio.I2C`, address and optionally a flag set to True if you want 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.
|
||||
|
||||
|
||||
@ -21,9 +26,24 @@ 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
|
||||
#GPIO Encoder
|
||||
encoder_handler.pins = ((board.GP17, board.GP15, board.GP14, False), (encoder 2 definition), etc. )
|
||||
```
|
||||
|
||||
OR
|
||||
|
||||
```python
|
||||
# I2C Encoder
|
||||
|
||||
# Setup i2c
|
||||
SDA = board.GP0
|
||||
SCL = board.GP1
|
||||
i2c = busio.I2C(SCL, SDA)
|
||||
|
||||
encoder_handler.pins = ((i2c, 0x36, False),)
|
||||
```
|
||||
|
||||
|
||||
3. Define the mapping of keys to be called (1 / layer)
|
||||
```python
|
||||
# You can optionally predefine combo keys as for your layout
|
||||
@ -68,6 +88,14 @@ keyboard.col_pins = (
|
||||
)
|
||||
keyboard.row_pins = (board.GP28, board.GP27, board.GP22, board.GP26, board.GP21)
|
||||
keyboard.diode_orientation = DiodeOrientation.COLUMNS
|
||||
|
||||
# I2C example
|
||||
#import busio
|
||||
#SDA = board.GP0
|
||||
#SCL = board.GP1
|
||||
#i2c = busio.I2C(SCL, SDA)
|
||||
#encoder_handler.i2c = ((i2c, 0x36, False),)
|
||||
|
||||
encoder_handler.pins = ((board.GP17, board.GP15, board.GP14, False),)
|
||||
|
||||
keyboard.tap_time = 250
|
||||
|
@ -1,126 +0,0 @@
|
||||
# Encoder module
|
||||
Add twist control to your keyboard! Volume, zoom, anything you want
|
||||
|
||||
This module was mostly built to support basoc functionality of the Adafruit I2C QT Rotary Encoder with NeoPixel
|
||||
|
||||
## Enabling the extension
|
||||
The constructor(`i2cEncoderHandler` class) takes a list of encoders, each one defined as a `busio.I2C`, address and optionally a flag set to True if you want 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. **only dented encoder tested**
|
||||
|
||||
## How to use
|
||||
How to use this module in your main / code file
|
||||
|
||||
you would need to install `adafruit_seesaw` package
|
||||
|
||||
1. load the module
|
||||
```python
|
||||
from kmk.modules.i2c_encoder import i2cEncoderHandler
|
||||
encoder_handler = i2cEncoderHandler()
|
||||
keyboard.modules = [layers, modtap, encoder_handler]
|
||||
```
|
||||
|
||||
2. Define the i2c for each encoder (i2c, address, True for an inversed encoder)
|
||||
```python
|
||||
encoder_handler.i2c = ((i2c, 0x36, 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
|
||||
from kb import KMKKeyboard
|
||||
from kmk.keys import KC
|
||||
from kmk.modules.layers import Layers
|
||||
from kmk.modules.i2c_encoder import i2cEncoderHandler
|
||||
from kmk.extensions.media_keys import MediaKeys
|
||||
|
||||
import board
|
||||
import busio
|
||||
|
||||
|
||||
# Addons -------------------------------------------
|
||||
|
||||
# Setup i2c
|
||||
SDA = board.GP0
|
||||
SCL = board.GP1
|
||||
i2c = busio.I2C(SCL, SDA)
|
||||
|
||||
#if i2c.try_lock():
|
||||
# print("i2c.scan(): " + str(i2c.scan()))
|
||||
#i2c.unlock()
|
||||
|
||||
# Keyboard
|
||||
|
||||
# Setup Keyboard
|
||||
keyboard = KMKKeyboard()
|
||||
layers = Layers()
|
||||
encoder_handler = i2cEncoderHandler()
|
||||
keyboard.modules = [
|
||||
layers,
|
||||
encoder_handler
|
||||
]
|
||||
keyboard.extensions = [
|
||||
MediaKeys()
|
||||
]
|
||||
|
||||
# keyboard.unicode_mode = UnicodeMode.LINUX
|
||||
keyboard.tap_time = 250
|
||||
keyboard.debug_enabled = False
|
||||
|
||||
# Encoder
|
||||
# Setup Encoder @ address 0x36
|
||||
encoder_handler.i2c = ((i2c, 0x36, False),)
|
||||
|
||||
# Rotary Encoder (1 encoder / 1 definition per layer)
|
||||
encoder_handler.map = [
|
||||
((KC.VOLD, KC.VOLU, KC.MPLY),), # Default
|
||||
((KC.PGDN, KC.PGDN, KC.END),), # Function
|
||||
]
|
||||
|
||||
# Trackball
|
||||
# Setup trackball @ address 0xA
|
||||
|
||||
|
||||
# Keymap
|
||||
_______ = KC.TRNS
|
||||
XXXXXXX = KC.NO
|
||||
|
||||
keyboard.keymap = [
|
||||
[ # Default
|
||||
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, XXXXXXX,
|
||||
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, XXXXXXX, KC.PGUP,
|
||||
KC.CAPS, KC.A, KC.S, KC.D, KC.F, KC.G, KC.H, KC.J, KC.K, KC.L, KC.SCLN, KC.QUOT, KC.NUHS, KC.ENTER, KC.PGDN,
|
||||
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.RSFT, XXXXXXX, KC.DEL,
|
||||
KC.LCTL, KC.LGUI, KC.LALT, XXXXXXX, KC.MO(1), XXXXXXX, KC.SPC, XXXXXXX, KC.MO(1), KC.RALT, KC.RCTL, KC.LEFT, KC.DOWN, KC.UP, KC.RIGHT,
|
||||
],
|
||||
[ # Function
|
||||
KC.GRV, 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.PSCR,
|
||||
_______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, KC.PAUSE,
|
||||
_______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, KC.INSERT,
|
||||
_______, _______, _______, XXXXXXX, _______, XXXXXXX, _______, XXXXXXX, _______, _______, _______, KC.HOME, KC.PGDN, KC.PGUP, KC.END,
|
||||
]
|
||||
]
|
||||
|
||||
# ------------------------------------------------------------- Main logic -------------------------------------------
|
||||
|
||||
if __name__ == "__main__":
|
||||
keyboard.go()
|
@ -1,5 +1,6 @@
|
||||
# See docs/encoder.md for how to use
|
||||
|
||||
import busio
|
||||
import digitalio
|
||||
from supervisor import ticks_ms
|
||||
|
||||
@ -8,22 +9,19 @@ from kmk.modules import Module
|
||||
# NB : not using rotaryio as it requires the pins to be consecutive
|
||||
|
||||
|
||||
class Encoder:
|
||||
class BaseEncoder:
|
||||
|
||||
VELOCITY_MODE = True
|
||||
|
||||
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) if pin_button is not None else None
|
||||
)
|
||||
def __init__(self, is_inverted=False):
|
||||
|
||||
self.is_inverted = is_inverted
|
||||
|
||||
self._state = (self.pin_a.get_value(), self.pin_b.get_value())
|
||||
self._state = None
|
||||
self._direction = None
|
||||
self._pos = 0
|
||||
self._button_state = True
|
||||
self._button_held = None
|
||||
self._velocity = 0
|
||||
|
||||
self._movement = 0
|
||||
@ -41,7 +39,8 @@ class Encoder:
|
||||
'velocity': self._velocity,
|
||||
}
|
||||
|
||||
# Called in a loop to refresh encoder state
|
||||
# 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())
|
||||
@ -76,26 +75,42 @@ class Encoder:
|
||||
self._state = new_state
|
||||
|
||||
# Velocity
|
||||
self.velocity_event()
|
||||
|
||||
# Button event
|
||||
self.button_event()
|
||||
|
||||
def velocity_event(self):
|
||||
if self.VELOCITY_MODE:
|
||||
new_timestamp = ticks_ms()
|
||||
self._velocity = new_timestamp - self._timestamp
|
||||
self._timestamp = new_timestamp
|
||||
|
||||
# Button events
|
||||
if self.pin_button:
|
||||
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())
|
||||
def button_event(self):
|
||||
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())
|
||||
|
||||
# returnd knob velocity as milliseconds between position changes (detents)
|
||||
# return knob velocity as milliseconds between position changes (detents)
|
||||
# for backwards compatibility
|
||||
def vel_report(self):
|
||||
print(self._velocity)
|
||||
# print(self._velocity)
|
||||
return self._velocity
|
||||
|
||||
|
||||
class GPIOEncoder(BaseEncoder):
|
||||
def __init__(self, pin_a, pin_b, pin_button=None, is_inverted=False):
|
||||
super().__init__(is_inverted)
|
||||
|
||||
self.pin_a = EncoderPin(pin_a)
|
||||
self.pin_b = EncoderPin(pin_b)
|
||||
self.pin_button = EncoderPin(pin_button, button_type=True)
|
||||
|
||||
self._state = (self.pin_a.get_value(), self.pin_b.get_value())
|
||||
|
||||
|
||||
class EncoderPin:
|
||||
def __init__(self, pin, button_type=False):
|
||||
self.pin = pin
|
||||
@ -114,6 +129,76 @@ class EncoderPin:
|
||||
return self.io.value
|
||||
|
||||
|
||||
class I2CEncoder(BaseEncoder):
|
||||
def __init__(self, i2c, address, is_inverted=False):
|
||||
|
||||
try:
|
||||
from adafruit_seesaw import digitalio, neopixel, rotaryio, seesaw
|
||||
except ImportError:
|
||||
print('seesaw missing')
|
||||
return
|
||||
|
||||
super().__init__(is_inverted)
|
||||
|
||||
self.seesaw = seesaw.Seesaw(i2c, address)
|
||||
|
||||
# Check for correct product
|
||||
|
||||
seesaw_product = (self.seesaw.get_version() >> 16) & 0xFFFF
|
||||
if seesaw_product != 4991:
|
||||
print('Wrong firmware loaded? Expected 4991')
|
||||
|
||||
self.encoder = rotaryio.IncrementalEncoder(self.seesaw)
|
||||
self.seesaw.pin_mode(24, self.seesaw.INPUT_PULLUP)
|
||||
self.switch = digitalio.DigitalIO(self.seesaw, 24)
|
||||
self.pixel = neopixel.NeoPixel(self.seesaw, 6, 1)
|
||||
|
||||
self._state = self.encoder.position
|
||||
|
||||
def update_state(self):
|
||||
|
||||
# Rotation events
|
||||
new_state = self.encoder.position
|
||||
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 self.encoder.position > self._state:
|
||||
self._direction = 1
|
||||
else:
|
||||
self._direction = -1
|
||||
self._state = new_state
|
||||
self.on_move_do(self.get_state())
|
||||
|
||||
# Velocity
|
||||
self.velocity_event()
|
||||
|
||||
# Button events
|
||||
self.button_event()
|
||||
|
||||
def button_event(self):
|
||||
if not self.switch.value and not self._button_held:
|
||||
# Pressed
|
||||
self._button_held = True
|
||||
if self.on_button_do is not None:
|
||||
self.on_button_do(self.get_state())
|
||||
|
||||
if self.switch.value and self._button_held:
|
||||
# Released
|
||||
self._button_held = False
|
||||
|
||||
def get_state(self):
|
||||
return {
|
||||
'direction': self.is_inverted and -self._direction or self._direction,
|
||||
'position': self._state,
|
||||
'is_pressed': not self.switch.value,
|
||||
'is_held': self._button_held,
|
||||
'velocity': self._velocity,
|
||||
}
|
||||
|
||||
|
||||
class EncoderHandler(Module):
|
||||
def __init__(self):
|
||||
self.encoders = []
|
||||
@ -129,16 +214,24 @@ class EncoderHandler(Module):
|
||||
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, bound_idx=idx: self.on_move_do(
|
||||
keyboard, bound_idx, x
|
||||
)
|
||||
new_encoder.on_button_do = lambda x, bound_idx=idx: self.on_button_do(
|
||||
keyboard, bound_idx, x
|
||||
)
|
||||
self.encoders.append(new_encoder)
|
||||
try:
|
||||
# Check for busio.I2C
|
||||
if isinstance(pins[0], busio.I2C):
|
||||
new_encoder = I2CEncoder(*pins)
|
||||
|
||||
# Else fall back to GPIO
|
||||
else:
|
||||
gpio_pins = pins[:3]
|
||||
new_encoder = GPIOEncoder(*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)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
return
|
||||
|
||||
def on_move_do(self, keyboard, encoder_id, state):
|
||||
|
@ -1,165 +0,0 @@
|
||||
# See docs/i2c_encoder.md for how to use
|
||||
# Build to support Adafruit I2C QT Rotary Encoder with NeoPixel
|
||||
# https://www.adafruit.com/product/4991
|
||||
|
||||
from supervisor import ticks_ms
|
||||
|
||||
from adafruit_seesaw import digitalio, neopixel, rotaryio, seesaw
|
||||
|
||||
from kmk.modules import Module
|
||||
|
||||
|
||||
class Encoder:
|
||||
|
||||
VELOCITY_MODE = False # not really for detents
|
||||
|
||||
def __init__(self, i2c, address, is_inverted=False):
|
||||
self.seesaw = seesaw.Seesaw(i2c, address)
|
||||
|
||||
# Check for correct product
|
||||
seesaw_product = (self.seesaw.get_version() >> 16) & 0xFFFF
|
||||
if seesaw_product != 4991:
|
||||
print('Wrong firmware loaded? Expected 4991')
|
||||
|
||||
self.encoder = rotaryio.IncrementalEncoder(self.seesaw)
|
||||
|
||||
self.seesaw.pin_mode(24, self.seesaw.INPUT_PULLUP)
|
||||
self.switch = digitalio.DigitalIO(self.seesaw, 24)
|
||||
self.pixel = neopixel.NeoPixel(self.seesaw, 6, 1)
|
||||
|
||||
self.is_inverted = is_inverted
|
||||
|
||||
self._state = self.encoder.position
|
||||
self._direction = None
|
||||
self._pos = 0
|
||||
self._button_state = True
|
||||
self._button_held = False
|
||||
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 {
|
||||
'direction': self.is_inverted and -self._direction or self._direction,
|
||||
'position': self._state,
|
||||
'is_pressed': not self.switch.value,
|
||||
'is_held': self._button_held,
|
||||
# 'velocity': self._velocity,
|
||||
}
|
||||
|
||||
# Called in a loop to refresh encoder state
|
||||
def update_state(self):
|
||||
|
||||
# Rotation events
|
||||
new_state = self.encoder.position
|
||||
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 self.encoder.position > self._state:
|
||||
self._direction = 1
|
||||
else:
|
||||
self._direction = -1
|
||||
self._state = new_state
|
||||
self.on_move_do(self.get_state())
|
||||
|
||||
# Velocity
|
||||
if self.VELOCITY_MODE:
|
||||
new_timestamp = ticks_ms()
|
||||
self._velocity = new_timestamp - self._timestamp
|
||||
self._timestamp = new_timestamp
|
||||
|
||||
# Button events
|
||||
if not self.switch.value and not self._button_held:
|
||||
# Pressed
|
||||
self._button_held = True
|
||||
if self.on_button_do is not None:
|
||||
self.on_button_do(self.get_state())
|
||||
|
||||
if self.switch.value and self._button_held:
|
||||
self._button_held = False
|
||||
# Released
|
||||
|
||||
# returnd knob velocity as milliseconds between position changes (detents)
|
||||
# for backwards compatibility
|
||||
def vel_report(self):
|
||||
return self._velocity
|
||||
|
||||
|
||||
class EncoderHandler(Module):
|
||||
def __init__(self):
|
||||
self.encoders = []
|
||||
self.i2c = None
|
||||
self.map = None
|
||||
|
||||
def on_runtime_enable(self, keyboard):
|
||||
return
|
||||
|
||||
def on_runtime_disable(self, keyboard):
|
||||
return
|
||||
|
||||
def during_bootup(self, keyboard):
|
||||
if self.i2c and self.map:
|
||||
for idx, definition in enumerate(self.i2c):
|
||||
new_encoder = Encoder(*definition)
|
||||
# In our case, we need to define keybord and encoder_id for callbacks
|
||||
new_encoder.on_move_do = lambda x, bound_idx=idx: self.on_move_do(
|
||||
keyboard, bound_idx, x
|
||||
)
|
||||
new_encoder.on_button_do = lambda x, bound_idx=idx: self.on_button_do(
|
||||
keyboard, bound_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
|
||||
'''
|
||||
for encoder in self.encoders:
|
||||
encoder.update_state()
|
||||
|
||||
return keyboard
|
||||
|
||||
def after_matrix_scan(self, keyboard):
|
||||
'''
|
||||
Return value will be replace matrix update if supplied
|
||||
'''
|
||||
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
|
Loading…
x
Reference in New Issue
Block a user