Encoder class rebuild

implement BaseEncoder
This commit is contained in:
Rene Giovanni Borella 2022-02-10 21:55:19 +01:00 committed by Kyle Brown
parent 0e029ebf72
commit 8e4ab1d733
4 changed files with 152 additions and 322 deletions

View File

@ -1,10 +1,15 @@
# Encoder module # Encoder module
Add twist control to your keyboard! Volume, zoom, anything you want 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 ## 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 constructor(`EncoderHandler` class) takes a list of encoder, each one defined as either:
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. * 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. 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) 2. Define the pins for each encoder (pin_a, pin_b, pin_button, True for an inversed encoder)
```python ```python
#GPIO Encoder
encoder_handler.pins = ((board.GP17, board.GP15, board.GP14, False), (encoder 2 definition), etc. ) 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) 3. Define the mapping of keys to be called (1 / layer)
```python ```python
# You can optionally predefine combo keys as for your layout # 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.row_pins = (board.GP28, board.GP27, board.GP22, board.GP26, board.GP21)
keyboard.diode_orientation = DiodeOrientation.COLUMNS 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),) encoder_handler.pins = ((board.GP17, board.GP15, board.GP14, False),)
keyboard.tap_time = 250 keyboard.tap_time = 250

View File

@ -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()

View File

@ -1,5 +1,6 @@
# See docs/encoder.md for how to use # See docs/encoder.md for how to use
import busio
import digitalio import digitalio
from supervisor import ticks_ms 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 # NB : not using rotaryio as it requires the pins to be consecutive
class Encoder: class BaseEncoder:
VELOCITY_MODE = True VELOCITY_MODE = True
def __init__(self, pin_a, pin_b, pin_button=None, is_inverted=False): def __init__(self, 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
)
self.is_inverted = is_inverted self.is_inverted = is_inverted
self._state = (self.pin_a.get_value(), self.pin_b.get_value()) self._state = None
self._direction = None self._direction = None
self._pos = 0 self._pos = 0
self._button_state = True self._button_state = True
self._button_held = None
self._velocity = 0 self._velocity = 0
self._movement = 0 self._movement = 0
@ -41,7 +39,8 @@ class Encoder:
'velocity': self._velocity, 'velocity': self._velocity,
} }
# Called in a loop to refresh encoder state # 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())
@ -76,26 +75,42 @@ class Encoder:
self._state = new_state self._state = new_state
# Velocity # Velocity
self.velocity_event()
# Button event
self.button_event()
def velocity_event(self):
if self.VELOCITY_MODE: if self.VELOCITY_MODE:
new_timestamp = ticks_ms() new_timestamp = ticks_ms()
self._velocity = new_timestamp - self._timestamp self._velocity = new_timestamp - self._timestamp
self._timestamp = new_timestamp self._timestamp = new_timestamp
# Button events def button_event(self):
if self.pin_button: new_button_state = self.pin_button.get_value()
new_button_state = self.pin_button.get_value() if new_button_state != self._button_state:
if new_button_state != self._button_state: self._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) # return knob velocity as milliseconds between position changes (detents)
# for backwards compatibility # for backwards compatibility
def vel_report(self): def vel_report(self):
print(self._velocity) # print(self._velocity)
return 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: class EncoderPin:
def __init__(self, pin, button_type=False): def __init__(self, pin, button_type=False):
self.pin = pin self.pin = pin
@ -114,6 +129,76 @@ class EncoderPin:
return self.io.value 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): class EncoderHandler(Module):
def __init__(self): def __init__(self):
self.encoders = [] self.encoders = []
@ -129,16 +214,24 @@ class EncoderHandler(Module):
def during_bootup(self, keyboard): def during_bootup(self, keyboard):
if self.pins and self.map: if self.pins and self.map:
for idx, pins in enumerate(self.pins): for idx, pins in enumerate(self.pins):
gpio_pins = pins[:3] try:
new_encoder = Encoder(*gpio_pins) # Check for busio.I2C
# In our case, we need to define keybord and encoder_id for callbacks if isinstance(pins[0], busio.I2C):
new_encoder.on_move_do = lambda x, bound_idx=idx: self.on_move_do( new_encoder = I2CEncoder(*pins)
keyboard, bound_idx, x
) # Else fall back to GPIO
new_encoder.on_button_do = lambda x, bound_idx=idx: self.on_button_do( else:
keyboard, bound_idx, x gpio_pins = pins[:3]
) new_encoder = GPIOEncoder(*gpio_pins)
self.encoders.append(new_encoder)
# 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 return
def on_move_do(self, keyboard, encoder_id, state): def on_move_do(self, keyboard, encoder_id, state):

View File

@ -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