Skip to content

Commit 39ceff4

Browse files
authored
Adding Easier Configuration for Dual-Side Encoders On Split Keyboards (#1133)
* added arg and property for `add_buttons: int` for adding singleton/single-pin buttons to the `coord_mapping`, rather than the user needing to create the coord_mapping manually. * added documentation for the `add_buttons` argument * updated description for Multiple Scanners with Custom `coord_mapping`
1 parent 7e8b569 commit 39ceff4

4 files changed

Lines changed: 125 additions & 20 deletions

File tree

docs/en/encoder.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ Add twist control to your keyboard! Volume, zoom, anything you want.
33

44
I2C encoder type has been tested with the Adafruit I2C QT Rotary Encoder with NeoPixel.
55

6-
**Note:** If you have a **split** keyboard and encoders on **both sides** should work, it's currently necessary to use the encoder-scanner explained at the bottom of [scanners docs](scanners.md).
6+
**Note:** If you have a **split** keyboard and encoders on **both sides** should work, it's currently necessary to use the encoder-scanner. See the Advanced Configuration section of the [scanners docs](scanners.md).
77

88
## Enabling the extension
99
The constructor(`EncoderHandler` class) takes a list of encoders, each one defined as either:

docs/en/scanners.md

Lines changed: 88 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -150,24 +150,42 @@ class MyKeyboard(KMKKeyboard):
150150

151151
### RotaryioEncoder
152152

153-
Matrix events from a quadrature ("rotary") encoder?
153+
Matrix events from a quadrature ("rotary") encoder.
154+
155+
For any rotary encoders that you may include in your keyboard configuration, you can add this scanner to handle the input of the encoder actions (usually: left turn, right turn, and click) and to be able to configure them via the `keyboard.keymap` as if they were regular keys.
156+
157+
Often, rotary encoders are attached as accessories, i.e. alongside a key/button matrix. The below example shows how this can be configured.
154158

155159
```python
156160
from kmk.scanners.encoder import RotaryioEncoder
161+
from kmk.scanners import DiodeOrientation
157162

158163
class MyKeyboard(KMKKeyboard):
159164
def __init__(self):
160165
super().__init__()
166+
row_pins = (board.GP2, board.GP3, board.GP4, board.GP5, board.GP6)
167+
col_pins = (board.GP29, board.GP28, board.GP27, board.GP26, board.GP22, board.GP20)
161168

162169
# create and register the scanner
163-
self.matrix = RotaryioEncoder(
170+
rotary = RotaryioEncoder(
164171
pin_a=board.GP0,
165172
pin_b=board.GP1,
166173
# optional
167174
divisor=4,
168175
)
176+
matrix = MatrixScanner(
177+
row_pins=self.row_pins,
178+
column_pins=self.col_pins,
179+
columns_to_anodes=DiodeOrientation.ROW2COL
180+
)
181+
self.matrix = [
182+
matrix,
183+
rotary
184+
]
169185
```
170186

187+
If your design requires symetrical encoders (e.g. one on each half of a split keyboard), see Multiple Scanners section below for more details.
188+
171189

172190
## `Scanner` base class
173191

@@ -201,14 +219,66 @@ class MyKeyboard(KMKKeyboard):
201219
# etc...
202220
]
203221
```
204-
#### Multiple Scanners `coord_mapping` and keymap changes
205-
To add more scanners you need to add onto your `coord_mapping`.
222+
223+
224+
#### Adding Single-Pin Buttons or Rotary Encoders to Keymap Using Scanners and Split
225+
226+
In many cases, split keybaords are symetrical in form and function. Some split keyboards also have additional hardware like one or more rotary encoders, or non-matrix-connected media or macro buttons. In this case, you will want to configure multiple scanners.
227+
228+
The `Split` class that you're probably already using creates the `coord_mapping` indexes automatically. However, the `add_buttons` argument will cause it to append any additional "buttons" (or encoder actions) to the `coord_mapping` for _each half_ of the keyboard.
229+
230+
By default, `Split` will also configure `MatrixScanner` and assign it to `KMKKeyboard.matrix` as a single/default scanner, but with the additional actions/keys from `RotaryioEncoder`, it will need to be configured in your custom class so that `RotaryioEncoder` can be configured and appended to `KNKKeyboard.matrix`. This enables the rotary actions to map from the `coord_mapping` to the `keybaord.keymap` correctly, making it easy to assign keycodes to the actions or buttons.
206231

207232
Example:
233+
```python
234+
from kmk.scanners import DiodeOrientation
235+
from kmk.scanners.keypad import MatrixScanner
236+
from kmk.scanners.encoder import RotaryioEncoder
237+
from kmk.modules.split import Split
238+
239+
240+
class MyKeyboard(KMKKeyboard):
241+
def __init__(self) -> None:
242+
super().__init__()
243+
self.diode_orientation = DiodeOrientation.ROW2COL
244+
split_args = {
245+
'split_side': None, # EE Hands
246+
'data_pin': board.GP1,
247+
'data_pin2': board.GP0,
248+
'split_flip': True,
249+
'use_pio': True,
250+
'uart_flip': True,
251+
'add_buttons': 2 # add left- and right-turn actions for one encoder on each side; see `split_keyboards.md`
252+
}
253+
self.row_pins = (board.GP2, board.GP3, board.GP4, board.GP5, board.GP6)
254+
self.col_pins = (board.GP29, board.GP28, board.GP27, board.GP26, board.GP22, board.GP20)
255+
self.split = Split(**split_args)
256+
self.modules.append(self.split)
257+
258+
matrix = MatrixScanner(row_pins=self.row_pins, column_pins=self.col_pins, columns_to_anodes=DiodeOrientation.ROW2COL)
259+
rotary = RotaryioEncoder(pin_a=board.D7, pin_b=board.D8)
260+
self.matrix = [
261+
matrix,
262+
rotary
263+
]
264+
265+
266+
if __name__ == '__main__':
267+
keyboard = MyKeyboard()
268+
keyboard.go()
269+
```
270+
271+
272+
#### Multiple Scanners `coord_mapping` and keymap changes
273+
For a more manually-controlled configuration, you can add any other scanners that you need and create your `coord_mapping` in your own code (leaving off the `add_buttons` argument when initializing `Split`)
274+
Creating and assigning a custom `coord_mapping` should be done before intitializing `Split` or any scanners.
275+
276+
The below examples illustrate how the additional encoder actions are assigned to the `coord_mapping`. Your configuration should follow this pattern.
277+
208278

209279
`coord_mapping` with just one `MatrixScanner` on a 58 key split keyboard:
210280
```python
211-
coord_mapping = [
281+
keyboard.coord_mapping = [
212282
0, 1, 2, 3, 4, 5, 35, 34, 33, 32, 31, 30,
213283
6, 7, 8, 9, 10, 11, 41, 40, 39, 38, 37, 36,
214284
12, 13, 14, 15, 16, 17, 47, 46, 45, 44, 43, 42,
@@ -217,9 +287,9 @@ coord_mapping = [
217287
]
218288
```
219289

220-
`coord_mapping` using `MatrixScanner` and `RotaryioEncoder` on the same 58 key split keyboard with an encoder on each half:
290+
`coord_mapping` using `MatrixScanner` and `RotaryioEncoder` on the same 58 key split keyboard, adding an encoder to each half:
221291
```python
222-
coord_mapping = [
292+
keyboard.coord_mapping = [
223293
0, 1, 2, 3, 4, 5, 37, 36, 35, 34, 33, 32,
224294
6, 7, 8, 9, 10, 11, 43, 42, 41, 40, 39, 38,
225295
12, 13, 14, 15, 16, 17, 49, 48, 47, 46, 45, 44,
@@ -229,9 +299,14 @@ coord_mapping = [
229299
]
230300
```
231301

232-
On the top left side of a standard split keyboard `coord_mapping`, right below that you see a split keyboard where `RotaryioEncoder` and `MatrixScanner` (the default scanner) are used.
233-
In the single scanner example, we used to count from 0 to 29 while the top right side starts at 30.
234-
With the addition of the encoder scanner, the left side has 2 additional keys making it count up to 31 and the right side would then start at 32 and count to 63.
235-
This means that keys 30, 31, 62, and 63 are for encoders.
236-
Notice that all of the encoders are at the end of the array, because we put the encoder scanner after the matrix scanner in `keyboard.matrix`.
237-
Therefore, we need to add 4 more key codes in the corresponding places of our `keyboard.keymap`, they will be used for the encoders.
302+
Note that in both examples, the left-side indexes count from 0 to 29 for the first five rows. But the right side of the first (single scanner) example starts at 30 (in the top right corner) and ends at 59 in the center.
303+
304+
With the addition of the `RotaryioEncoder` scanner, the left side has 2 additional keys (30 and 31) causing the right side to start at 32 and count to 61 in the center, with two more keys at the bottom (62 and 63).
305+
**This means that keys 30, 31, 62, and 63 are for the encoders.**
306+
307+
Notice that, despite the visual layout, all of the encoder keys are at the **_end_** of the array. This is because `RotaryioEncoder` was **_after_** `MatrixScanner` **_in the list_** when the `keyboard.matrix` was assigned. (see example in the previous section above)
308+
309+
Therefore, 4 more key codes can be added in the corresponding places in the `keyboard.keymap`, and they will be assigned to the encoders' actions.
310+
311+
312+
Also note, it may be necessary to configure `Split().split_offset` when configuring your own `coord_mapping` to make sure that the encoders are assigned properly. The value will usually be the first/lowest index value of the right side. In the case of the second example above, the offset value would be 32, but this also depends on your `coord_mapping` layout.

docs/en/split_keyboards.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ split = Split(
5050
data_pin2=None, # Second uart pin to allow 2 way communication
5151
uart_flip=True, # Reverses the RX and TX pins if both are provided
5252
use_pio=False, # Use RP2040 PIO implementation of UART. Required if you want to use other pins than RX/TX
53+
add_buttons = 0 # add single-pin buttons, rotary encoder actions, etc. per-side.
5354
)
5455

5556
```
@@ -111,6 +112,10 @@ In order to enable it, you must:
111112
- pass `use_pio=True` into the `Split()` constructor.
112113

113114

115+
### `add_buttons`
116+
if you have additional single-pin buttons, rotary encoders, or other non-matrix actions that will be assigned to keys, you can add this argument with the number of buttons per-side. This will enable you to include them at the end of your keymap without needing to manually configure `keyboard.coord_mapping`.
117+
118+
114119
### `data_pin`/`data_pin2`
115120
For UART `SplitType`: on the `split_target` side, `data_pin` is the one use for RX, `data_pin2` the one for TX.
116121

kmk/modules/split.py

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ def __init__(
4141
data_pin2=None,
4242
uart_flip=True,
4343
use_pio=False,
44+
add_buttons=0, # add single-pin buttons, rotary encoder actions, etc. per-side.
4445
):
4546
self._is_target = True
4647
self._uart_buffer = []
@@ -54,9 +55,10 @@ def __init__(
5455
self.uart_flip = uart_flip
5556
self._use_pio = use_pio
5657
self._uart = None
58+
self.add_buttons = add_buttons
5759
self._uart_interval = uart_interval
5860
self.uart_header = bytearray([0xB2]) # Any non-zero byte should work
59-
61+
debug('Split module initializing...')
6062
if self.split_type == SplitType.BLE:
6163
try:
6264
from adafruit_ble import BLERadio
@@ -99,6 +101,7 @@ def during_bootup(self, keyboard):
99101
if not self.data_pin:
100102
self.data_pin = keyboard.data_pin
101103

104+
debug('Checking split side...')
102105
# if split side was given, find target from split_side.
103106
if self.split_side == SplitSide.LEFT:
104107
self._is_target = bool(self.split_target_left)
@@ -119,11 +122,18 @@ def during_bootup(self, keyboard):
119122
elif name.endswith('R'):
120123
self.split_side = SplitSide.RIGHT
121124

125+
debug(f'Split side assigned to as: {self.split_side}')
126+
122127
if not self._is_target:
123128
keyboard._hid_send_enabled = False
124129

125130
if self.split_offset is None:
126-
self.split_offset = keyboard.matrix[-1].coord_mapping[-1] + 1
131+
if self.add_buttons > 0:
132+
self.split_offset = (
133+
keyboard.matrix[-1].coord_mapping[-1] + 1 + self.add_buttons
134+
)
135+
else:
136+
self.split_offset = keyboard.matrix[-1].coord_mapping[-1] + 1
127137

128138
if self.split_type == SplitType.UART and self.data_pin is not None:
129139
if self._is_target or not self.uart_flip:
@@ -140,11 +150,11 @@ def during_bootup(self, keyboard):
140150
self._uart = busio.UART(
141151
tx=self.data_pin, rx=self.data_pin2, timeout=self._uart_interval
142152
)
143-
153+
debug(f'Split type assigned as: {self.split_type}')
144154
# Attempt to sanely guess a coord_mapping if one is not provided.
145155
if not keyboard.coord_mapping and keyboard.row_pins and keyboard.col_pins:
146156
cm = []
147-
157+
debug('Calculating coord_mapping...')
148158
rows_to_calc = len(keyboard.row_pins)
149159
cols_to_calc = len(keyboard.col_pins)
150160

@@ -157,8 +167,23 @@ def during_bootup(self, keyboard):
157167
for cidx in range(cols_to_calc):
158168
cm.append(cols_to_calc * ridx + cidx)
159169
for cidx in cols_rhs:
160-
cm.append(cols_to_calc * (rows_to_calc + ridx) + cidx)
161-
170+
# add indexes accounting for any added buttons
171+
if self.add_buttons != 0:
172+
cm.append(
173+
cols_to_calc * (rows_to_calc + ridx)
174+
+ cidx
175+
+ (self.add_buttons),
176+
)
177+
else:
178+
cm.append(cols_to_calc * (rows_to_calc + ridx) + cidx)
179+
# append addded buttons to the final list
180+
for a in range(self.add_buttons):
181+
cm.append(cols_to_calc * rows_to_calc + cols_rhs[-1] + (a))
182+
for a in range(self.add_buttons, self.add_buttons * 2):
183+
cm.append(cols_to_calc * (rows_to_calc + cols_rhs[0]) + (a))
184+
185+
debug('Done calculating coord_mapping:')
186+
debug(f'{cm}')
162187
keyboard.coord_mapping = tuple(cm)
163188

164189
if not keyboard.coord_mapping and debug.enabled:

0 commit comments

Comments
 (0)