Skip to content

Commit 8afe569

Browse files
authored
Add DuplexMatrixScanner (#1138)
Add scanner for J-duplex and its documentation
1 parent 51d6c6a commit 8afe569

2 files changed

Lines changed: 131 additions & 1 deletion

File tree

docs/en/scanners.md

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ documentation](https://docs.circuitpython.org/en/latest/shared-bindings/keypad/i
2020

2121
### keypad MatrixScanner
2222
This is the default scanner used by KMK.
23-
It uses the CircuitPython builtin `keypad.KeyMatrix`.
23+
It uses the CircuitPython built-in `keypad.KeyMatrix`.
2424

2525
```python
2626
from kmk.scanners.keypad import MatrixScanner
@@ -145,6 +145,41 @@ class MyKeyboard(KMKKeyboard):
145145
)
146146
```
147147

148+
### DuplexMatrixScanner
149+
150+
The DuplexMatrixScanner dynamically switch GPIO pins that can change between input and output modes during the scan cycle; this allows to first scan COL2ROW and then ROW2COL.
151+
152+
GPIO column pins must be wired physically to consecutive columns `GP0 to Col0 and Col1, GP1 to Col2 and Col3`. GPIO pin must not be wired as `GP0 to Col0 and Col2`
153+
154+
It reports pressed keys as:
155+
| 1st Scan COL2ROW | 2nd Scan ROW2COL |
156+
|---------------------|----------------------|
157+
|Col0->Row0 = 0 | Row0->Col0 = 1 |
158+
|Col1->Row0 = 2 | Row0->Col1 = 3 |
159+
|and so on. | ... |
160+
161+
```python
162+
from kmk.scanners.duplexmatrix import DuplexMatrixScanner
163+
164+
# For a 2x3 matrix you will have 12 keys
165+
cols = [board.GP0, board.GP1, board.GP2]
166+
rows = [board.GP3, board.GP4]
167+
168+
class MyKeyboard(KMKKeyboard):
169+
def __init__(self):
170+
super().__init__()
171+
172+
# create and register the scanner
173+
self.matrix = DuplexMatrixScanner(
174+
cols=cols,
175+
rows=rows,
176+
)
177+
self.coord_mapping = [
178+
0, 1, 2, 3, 4, 5,
179+
6, 7, 8, 9, 10, 11,
180+
]
181+
182+
```
148183

149184
## Rotary Encoder Scanners
150185

kmk/scanners/duplexmatrix.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import digitalio
2+
3+
from keypad import Event as KeyEvent
4+
5+
from kmk.scanners import Scanner
6+
7+
8+
def ensure_DIO(x):
9+
# __class__.__name__ is used instead of isinstance as the MCP230xx lib
10+
# does not use the digitalio.DigitalInOut, but rather a self defined one:
11+
# https://github.com/adafruit/Adafruit_CircuitPython_MCP230xx/blob/3f04abbd65ba5fa938fcb04b99e92ae48a8c9406/adafruit_mcp230xx/digital_inout.py#L33
12+
if x.__class__.__name__ == 'DigitalInOut':
13+
return x
14+
else:
15+
return digitalio.DigitalInOut(x)
16+
17+
18+
class DuplexMatrixScanner(Scanner):
19+
'''
20+
Duplex (bidirectional) matrix scanner.
21+
It maps:
22+
Col0->Row0 = 0, Row0->Col0 = 1
23+
Col1->Row0 = 2, Row0->Col1 = 3
24+
and so on.
25+
'''
26+
27+
def __init__(self, rows, cols, pull=digitalio.Pull.UP, offset=0):
28+
self.len_rows = len(rows)
29+
self.len_cols = len(cols)
30+
self.pull = pull
31+
self.offset = offset
32+
33+
# A pin cannot be both a row and column, detect this by combining the
34+
# two tuples into a set and validating that the length did not drop
35+
#
36+
# repr() hackery is because CircuitPython Pin objects are not hashable
37+
unique_pins = {repr(c) for c in cols} | {repr(r) for r in rows}
38+
assert (
39+
len(unique_pins) == self.len_cols + self.len_rows
40+
), 'Cannot use a pin as both a column and row'
41+
del unique_pins
42+
43+
self.dio_rows = [ensure_DIO(r) for r in rows]
44+
self.dio_cols = [ensure_DIO(c) for c in cols]
45+
46+
# Initialize all pins as inputs
47+
for pin in self.dio_rows + self.dio_cols:
48+
pin.switch_to_input(pull=self.pull)
49+
50+
# Total keys = rows * cols * 2 (bidirectional)
51+
self._key_count = self.len_rows * self.len_cols * 2
52+
initial_state_value = b'\x01' if self.pull is digitalio.Pull.UP else b'\x00'
53+
self.state = bytearray(initial_state_value) * self._key_count
54+
55+
@property
56+
def key_count(self):
57+
return self._key_count
58+
59+
def _scan_direction(self, outputs, inputs, base_offset):
60+
'''
61+
Generic scan for one direction (COL2ROW or ROW2COL)
62+
outputs: list of pins to drive
63+
inputs: list of pins to read
64+
base_offset: 0 or 1 for key_number calculation
65+
'''
66+
for o_idx, o_pin in enumerate(outputs):
67+
o_pin.switch_to_output()
68+
o_pin.value = False if self.pull == digitalio.Pull.UP else True
69+
70+
for i_idx, i_pin in enumerate(inputs):
71+
new_val = int(i_pin.value)
72+
width = len(outputs)
73+
key_number = (i_idx * width + o_idx) * 2 + base_offset + self.offset
74+
old_val = self.state[key_number]
75+
76+
if old_val != new_val:
77+
if self.pull is digitalio.Pull.UP:
78+
pressed = not new_val
79+
else:
80+
pressed = new_val
81+
self.state[key_number] = new_val
82+
o_pin.switch_to_input(pull=self.pull)
83+
return KeyEvent(key_number, pressed)
84+
85+
o_pin.switch_to_input(pull=self.pull)
86+
return None
87+
88+
def scan_for_changes(self):
89+
# First scan
90+
event = self._scan_direction(self.dio_cols, self.dio_rows, base_offset=1)
91+
if event:
92+
return event
93+
94+
# Second scan
95+
return self._scan_direction(self.dio_rows, self.dio_cols, base_offset=0)

0 commit comments

Comments
 (0)