Encoder module! (#211)

* added atreus62 board

* Uploaded module for encoder support

* Update README.md


Co-authored-by: Ryan Pullen <rpullen@martinuav.com>
This commit is contained in:
pullenrc 2021-07-19 10:30:28 -05:00 committed by GitHub
parent eb5756f530
commit 15918db7ac
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 552 additions and 0 deletions

16
boards/atreus62/README.md Normal file
View File

@ -0,0 +1,16 @@
# Atreus62
![Atreus62](https://assets.bigcartel.com/product_images/189335282/BIlqCtd.jpg?auto=format&fit=max&w=1200)
Atreus62 is a 60% column staggered keyboard pinky stagger
kb.py is designed to work with the Teensy 4.1
Retailers (USA)
[Atreus62](https://shop.profetkeyboards.com/product/atreus62-keyboard)
Extentions enabled by default
- [Layers](https://github.com/KMKfw/kmk_firmware/tree/master/docs/layers.md) Need more keys than switches? Use layers.
- [RGB](https://github.com/KMKfw/kmk_firmware/tree/master/docs/rgb.md) Light it up
- [Encoder](https://github.com/KMKfw/kmk_firmware/tree/master/docs/encoder.md) Twist control for all the things

28
boards/atreus62/kb.py Normal file
View File

@ -0,0 +1,28 @@
import board
from kmk.kmk_keyboard import KMKKeyboard as _KMKKeyboard
from kmk.matrix import DiodeOrientation
# from kmk.matrix import intify_coordinate as ic
class KMKKeyboard(_KMKKeyboard):
col_pins = (
board.D24,
board.D25,
board.D26,
board.D27,
board.D28,
board.D29,
board.D30,
board.D31,
board.D32,
board.D33,
board.D34,
board.D35,
)
row_pins = (board.D3, board.D4, board.D5, board.D6, board.D7, board.D8)
diode_orientation = DiodeOrientation.ROWS
# diode_orientation = DiodeOrientation.COLUMNS

220
boards/atreus62/main.py Normal file
View File

@ -0,0 +1,220 @@
import board
from kb import KMKKeyboard
from kmk.handlers.sequences import send_string, simple_key_sequence
from kmk.keys import KC
from kmk.modules.encoder import EncoderHandler
from kmk.modules.layers import Layers
# local_increment = None
# local_decrement = None
keyboard = KMKKeyboard()
# custom keys used for encoder actions
Zoom_in = KC.LCTRL(KC.EQUAL)
Zoom_out = KC.LCTRL(KC.MINUS)
# standard filler keys
_______ = KC.TRNS
XXXXXXX = KC.NO
# for use in the encoder extension
encoder_map = [
[
(
KC.VOLU,
KC.VOLD,
2,
), # Only 1 encoder is being used, so only one tuple per layer is required
],
[
(Zoom_in, Zoom_out, 1),
],
[
(_______, _______, 1), # no action taken by the encoder on this layer
],
]
layers_ext = Layers()
encoder_ext = EncoderHandler([board.D40], [board.D41], encoder_map)
encoder_ext.encoders[0].is_inverted = True
keyboard.modules = [layers_ext, encoder_ext]
keyboard.tap_time = 250
keyboard.debug_enabled = False
# custom keys
NEW = KC.LCTL(KC.N)
NEW_DIR = KC.LCTL(KC.LSFT(KC.N))
CAD = KC.LCTL(KC.LALT(KC.DEL))
RES = KC.LCTL(KC.LSFT(KC.ESC))
FE = KC.LGUI(KC.E)
LT1_DEL = KC.LT(1, KC.DEL)
LT2_ENT = KC.LT(2, KC.ENT)
SAVE_AS = KC.LCTL(KC.LSFT(KC.S))
PSCR = KC.LGUI(KC.PSCR)
SNIP = simple_key_sequence(
(
KC.LGUI,
KC.MACRO_SLEEP_MS(25),
KC.S,
KC.N,
KC.I,
KC.P,
KC.MACRO_SLEEP_MS(25),
KC.ENT,
)
)
# programming layer keys
UINT = simple_key_sequence(
(
KC.U,
KC.I,
KC.N,
KC.T,
)
)
INT = simple_key_sequence(
(
KC.I,
KC.N,
KC.T,
)
)
DOUBLE = simple_key_sequence(
(
KC.D,
KC.O,
KC.U,
KC.B,
KC.L,
KC.E,
)
)
BOOL = simple_key_sequence(
(
KC.B,
KC.O,
KC.O,
KC.L,
)
)
BYTE = simple_key_sequence(
(
KC.B,
KC.Y,
KC.T,
KC.E,
)
)
SBYTE = simple_key_sequence(
(
KC.S,
KC.B,
KC.Y,
KC.T,
KC.E,
)
)
CHAR = simple_key_sequence(
(
KC.C,
KC.H,
KC.A,
KC.R,
)
)
GETSET = simple_key_sequence(
(
KC.LBRC,
KC.SPC,
KC.G,
KC.E,
KC.T,
KC.SCLN,
KC.SPC,
KC.S,
KC.E,
KC.T,
KC.SCLN,
KC.SPC,
KC.RBRC,
)
)
PUBLIC = simple_key_sequence(
(
KC.P,
KC.U,
KC.B,
KC.L,
KC.I,
KC.C,
)
)
DEBUGWL = simple_key_sequence(
(
KC.LSFT(KC.D),
KC.E,
KC.B,
KC.U,
KC.G,
KC.DOT,
KC.LSFT(KC.W),
KC.R,
KC.I,
KC.T,
KC.E,
KC.LSFT(KC.L),
KC.I,
KC.N,
KC.E,
KC.LSFT(KC.N9),
)
)
PRINT = simple_key_sequence(
(
KC.P,
KC.R,
KC.I,
KC.N,
KC.T,
)
)
# make keymap
keyboard.keymap = [
[ # qwerty
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.CAPS, KC.Q, KC.W, KC.E, KC.R, KC.T, KC.Y, KC.U, KC.I, KC.O, KC.P, KC.PSLS,
KC.TAB, KC.A, KC.S, KC.D, KC.F, KC.G, KC.H, KC.J, KC.K, KC.L, KC.SCLN, KC.QUOT,
KC.TRNS, KC.Z, KC.X, KC.C, KC.V, KC.B, KC.N, KC.M, KC.COMM, KC.DOT, KC.SLSH, FE,
KC.BSPC, KC.DEL, KC.LALT, KC.LSFT, KC.LCTL, KC.BSPC, KC.SPC, KC.ENT, KC.RSFT, KC.RCTL, KC.ENT, KC.RGUI,
XXXXXXX, XXXXXXX, XXXXXXX, XXXXXXX, XXXXXXX, KC.MO(1), KC.MO(2), KC.MUTE, XXXXXXX, XXXXXXX, XXXXXXX, XXXXXXX,
],
[ # navnum
KC.TRNS, SAVE_AS, PSCR, SNIP, KC.LGUI, NEW_DIR, KC.PSLS, KC.RGUI, KC.NO, KC.NO, KC.NO, KC.MINS,
KC.BSLS, KC.NO, KC.HOME, KC.UP, KC.END, NEW, KC.N5, KC.N6, KC.N7, KC.N8, KC.N9, KC.BSLS,
KC.F2, KC.NO, KC.LEFT, KC.DOWN, KC.RGHT, KC.HASH, KC.N0, KC.N1, KC.N2, KC.N3, KC.N4, KC.QUOT,
KC.LSFT, KC.NO, KC.NO, KC.NO, KC.TAB, KC.UNDS, KC.MINS, KC.PPLS, KC.MINS, KC.PAST, KC.PSLS, KC.LBRC,
KC.BSPC, KC.NO, KC.NO, KC.NO, KC.NO, KC.TRNS, KC.SPC, KC.EQL, KC.N0, KC.DOT, KC.ENT, KC.RGUI,
XXXXXXX, XXXXXXX, XXXXXXX, XXXXXXX, XXXXXXX, KC.TRNS, KC.TRNS, XXXXXXX, XXXXXXX, XXXXXXX, XXXXXXX, XXXXXXX,
],
[ # sym/prog
KC.TRNS, KC.NO, KC.NO, KC.NO, KC.F2, KC.AMPR, PRINT, DEBUGWL, SAVE_AS, KC.NO, KC.NO, KC.NO,
KC.BSLS, KC.NO, KC.NO, KC.LCBR, KC.RCBR, KC.AT, INT, GETSET, KC.UP, KC.NO, KC.NO, KC.NO,
KC.TAB, KC.NO, KC.NO, KC.LPRN, KC.RPRN, KC.DLR, BOOL, KC.LEFT, KC.DOWN, KC.RGHT, KC.NO, KC.NO,
KC.LSFT, KC.NO, KC.NO, KC.LBRC, KC.RBRC, KC.PERC, UINT, DOUBLE, KC.NO, KC.NO, KC.NO, KC.NO,
KC.BSPC, KC.LGUI, KC.LALT, KC.LSFT, KC.LCTL, KC.DEL, KC.TRNS, PUBLIC, KC.RCTL, KC.RALT, KC.ENT, KC.RESET,
XXXXXXX, XXXXXXX, XXXXXXX, XXXXXXX, XXXXXXX, KC.TRNS, KC.TRNS, XXXXXXX, XXXXXXX, XXXXXXX, XXXXXXX, XXXXXXX,
],
]
if __name__ == '__main__':
keyboard.go()

58
docs/encoder.md Normal file
View File

@ -0,0 +1,58 @@
# Encoder
Add twist control to your keyboard! Volume, zoom, anything you want.
## Enabling the extension
The constructor takes a minimun 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
Create your special keys:
```python
Zoom_in = KC.LCTRL(KC.EQUAL)
Zoom_out = KC.LCTRL(KC.MINUS)
```
Create the encoder_map.
Anatomy of an encoder_map tuple: (increment_key, decrement_key, keys presses per encoder click)
```python
# 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 lsit 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]
```

230
kmk/modules/encoder.py Normal file
View File

@ -0,0 +1,230 @@
import digitalio
from kmk.kmktime import ticks_ms
from kmk.modules import Module
class EncoderPadState:
OFF = False
ON = True
class EndcoderDirection:
Left = False
Right = True
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())
def _to_dict(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,
}
# 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
# 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 self.encoder_state == (self.eps.ON, self.eps.ON): # Resting position
if new_encoder_state == (self.eps.ON, self.eps.OFF): # Turned right 1
self.encoder_direction = self.edr.Right
elif new_encoder_state == (self.eps.OFF, self.eps.ON): # Turned left 1
self.encoder_direction = self.edr.Left
elif self.encoder_state == (self.eps.ON, self.eps.OFF): # R1 or L3 position
if new_encoder_state == (self.eps.OFF, self.eps.OFF): # Turned right 1
self.encoder_direction = self.edr.Right
elif new_encoder_state == (self.eps.ON, self.eps.ON): # Turned left 1
if self.encoder_direction == self.edr.Left:
self.encoder_value = self.encoder_value - 1
elif self.encoder_state == (self.eps.OFF, self.eps.ON): # R3 or L1
if new_encoder_state == (self.eps.OFF, self.eps.OFF): # Turned left 1
self.encoder_direction = self.edr.Left
elif new_encoder_state == (self.eps.ON, self.eps.ON): # Turned right 1
if self.encoder_direction == self.edr.Right:
self.encoder_value = self.encoder_value + 1
else: # self.encoder_state == '11'
if new_encoder_state == (self.eps.ON, self.eps.OFF): # Turned left 1
self.encoder_direction = self.edr.Left
elif new_encoder_state == (self.eps.OFF, self.eps.ON): # Turned right 1
self.encoder_direction = self.edr.Right # 'R'
elif new_encoder_state == (
self.eps.ON,
self.eps.ON,
): # Skipped intermediate 01 or 10 state, however turn completed
if self.encoder_direction == self.edr.Left:
self.encoder_value = self.encoder_value - 1
elif self.encoder_direction == self.edr.Right:
self.encoder_value = self.encoder_value + 1
self.encoder_state = new_encoder_state
if self.vel_mode:
self.vel_ts = ticks_ms()
if self.encoder_state != self.last_encoder_state:
self.position_change = self.invert_rotation(
self.encoder_value, self.last_encoder_value
)
self.last_encoder_state = self.encoder_state
self.last_encoder_value = self.encoder_value
if self.position_change > 0:
self._to_dict()
# return self.increment_key
return 0
elif self.position_change < 0:
self._to_dict()
# return self.decrement_key
return 1
else:
return None
# invert knob direction if encoder pins are soldered backwards
def invert_rotation(self, new, 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):
self.encoder_speed = self.vel_ts - self.last_vel_ts
self.last_vel_ts = self.vel_ts
return self.encoder_speed
class EncoderHandler(Module):
encoders = []
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 on_runtime_enable(self, keyboard):
return
def on_runtime_disable(self, keyboard):
return
def during_bootup(self, keyboard):
return
def before_matrix_scan(self, keyboard):
'''
Return value will be injected as an extra matrix update
'''
return self.get_reports(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
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)