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 "", line 1, in # 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