From 15918db7ac5ed3e90f144e2f2fc70fb6231ea922 Mon Sep 17 00:00:00 2001 From: pullenrc Date: Mon, 19 Jul 2021 10:30:28 -0500 Subject: [PATCH] Encoder module! (#211) * added atreus62 board * Uploaded module for encoder support * Update README.md Co-authored-by: Ryan Pullen --- boards/atreus62/README.md | 16 +++ boards/atreus62/kb.py | 28 +++++ boards/atreus62/main.py | 220 ++++++++++++++++++++++++++++++++++++ docs/encoder.md | 58 ++++++++++ kmk/modules/encoder.py | 230 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 552 insertions(+) create mode 100644 boards/atreus62/README.md create mode 100644 boards/atreus62/kb.py create mode 100644 boards/atreus62/main.py create mode 100644 docs/encoder.md create mode 100644 kmk/modules/encoder.py diff --git a/boards/atreus62/README.md b/boards/atreus62/README.md new file mode 100644 index 0000000..0e8c971 --- /dev/null +++ b/boards/atreus62/README.md @@ -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 + diff --git a/boards/atreus62/kb.py b/boards/atreus62/kb.py new file mode 100644 index 0000000..5bceb09 --- /dev/null +++ b/boards/atreus62/kb.py @@ -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 diff --git a/boards/atreus62/main.py b/boards/atreus62/main.py new file mode 100644 index 0000000..6801157 --- /dev/null +++ b/boards/atreus62/main.py @@ -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() diff --git a/docs/encoder.md b/docs/encoder.md new file mode 100644 index 0000000..da317f8 --- /dev/null +++ b/docs/encoder.md @@ -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] +``` diff --git a/kmk/modules/encoder.py b/kmk/modules/encoder.py new file mode 100644 index 0000000..86c841f --- /dev/null +++ b/kmk/modules/encoder.py @@ -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)