1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
|
from __future__ import annotations
from digitalio import DigitalInOut, Pull
def mapping_left_right(cols, rows):
"""
Maps keys according to the following diagram:
1 2 3 1 2 3
+-------------
1| A A A B B B
2| A A A B B B
"""
size = cols * rows
coord_mapping = []
for y in range(rows):
yy = y * cols
coord_mapping.extend(range(yy, yy + cols))
coord_mapping.extend(range(yy + size, yy + cols + size))
return coord_mapping
class Matrix:
"""
A Scanner for Keyboard Matrices
The raw key numbers returned by this scanner are based on this layout:
C1 C2 C3
+-----------
R1| 0 1 2
R2| 3 4 5
:param cols: A sequence of pins that are the columns for matrix A.
:param rows: A sequence of pins that are the rows for matrix A.
"""
def __init__(self, cols, rows, mapping=mapping_left_right):
self.len_cols = len(cols)
self.len_rows = len(rows)
self.keys = self.len_cols * self.len_rows
self.coord_mapping = mapping(self.len_cols, self.len_rows // 2)
self.coord_mapping.extend(range(max(self.coord_mapping) + 1, self.keys))
# A pin cannot be both a row and column, detect this by combining the
# two tuples into a set and validating that the length did not drop
#
# repr() hackery is because CircuitPython Pin objects are not hashable
unique_pins = {repr(c) for c in cols} | {repr(r) for r in rows}
assert (
len(unique_pins) == self.len_cols + self.len_rows
), "Cannot use a pin as both a column and row"
del unique_pins
# __class__.__name__ is used instead of isinstance as the MCP230xx lib
# does not use the digitalio.DigitalInOut, but rather a self defined one:
# https://github.com/adafruit/Adafruit_CircuitPython_MCP230xx/blob/3f04abbd65ba5fa938fcb04b99e92ae48a8c9406/adafruit_mcp230xx/digital_inout.py#L33
self.cols = [
x if x.__class__.__name__ == "DigitalInOut" else DigitalInOut(x)
for x in cols
]
self.rows = [
x if x.__class__.__name__ == "DigitalInOut" else DigitalInOut(x)
for x in rows
]
self.state = bytearray(self.keys)
def scan_for_changes(self):
for row_pin in self.rows:
row_pin.switch_to_output(value=True)
for col_pin in self.cols:
col_pin.switch_to_input(pull=Pull.UP)
for row, row_pin in enumerate(self.rows):
row_pin.value = False
for col, col_pin in enumerate(self.cols):
ba_idx = col + row * self.len_cols
# cast to int to avoid
#
# >>> xyz = bytearray(3)
# >>> xyz[2] = True
# Traceback (most recent call last):
# File "<stdin>", line 1, in <module>
# OverflowError: value would overflow a 1 byte buffer
#
# I haven't dived too far into what causes this, but it's
# almost certainly because bool types in Python aren't just
# aliases to int values, but are proper pseudo-types
new_val = int(not col_pin.value)
old_val = self.state[ba_idx]
if old_val != new_val:
map_idx = self.coord_mapping.index(ba_idx)
self.state[ba_idx] = new_val
yield map_idx, new_val
row_pin.value = True
|