aboutsummaryrefslogtreecommitdiffstats
path: root/hex33board/matrix.py
blob: 72ed7651c91f8a86d7db854eba08c60dce9edab4 (plain)
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