|
| 1 | +# SPDX-FileCopyrightText: 2022 Jeff Epler for Adafruit Industries |
| 2 | +# SPDX-License-Identifier: MIT |
| 3 | + |
| 4 | +# KeyMatrix Whisperer |
| 5 | +# |
| 6 | +# Interactively determine a matrix keypad's row and column pins |
| 7 | +# |
| 8 | +# Wait until the program prints "press keys now". Then, press and hold a key |
| 9 | +# until it registers. Repeat until all rows and columns are identified. If your |
| 10 | +# keyboard matrix does NOT have dioes, you MUST take care to only press a |
| 11 | +# single key at a time. |
| 12 | +# |
| 13 | +# How identification is performed: When a key is pressed _some_ pair of I/Os |
| 14 | +# will be connected. This code repeatedly scans all possible pairs, recording |
| 15 | +# them. The very first pass when no key is pressed is recorded as "junk" so it |
| 16 | +# can be ignored. |
| 17 | +# |
| 18 | +# Then, the first I/O involved in the first non-junk press is arbitrarily |
| 19 | +# recorded as a "row pin". If the matrix does not have diodes, this can |
| 20 | +# actually vary from run to run or depending on the first key you pressed. The |
| 21 | +# only net effect of this is that the row & column lists are exchanged. |
| 22 | +# |
| 23 | +# After enough key presses, you'll get a full list of "row" and "column" pins. |
| 24 | +# For instance, on the Commodore 16 keyboard you'd get 8 row pins and 8 column pins. |
| 25 | +# |
| 26 | +# This doesn't help determine the LOGICAL ORDER of rows and columns or the |
| 27 | +# physical layout of the keyboard. You still have to do that for yourself. |
| 28 | + |
| 29 | +import board |
| 30 | +import microcontroller |
| 31 | +from digitalio import DigitalInOut, Pull |
| 32 | + |
| 33 | +# List of pins to test, or None to test all pins |
| 34 | +IO_PINS = None # [board.D0, board.D1] |
| 35 | +# Which value(s) to set the driving pin to |
| 36 | +values = [True] # [True, False] |
| 37 | + |
| 38 | +def discover_io(): |
| 39 | + return [pin_maybe for name in dir(microcontroller.pin) if isinstance(pin_maybe := getattr(microcontroller.pin, name), microcontroller.Pin)] |
| 40 | + |
| 41 | +def pin_lookup(pin): |
| 42 | + for i in dir(board): |
| 43 | + if getattr(board, i) is pin: return i |
| 44 | + for i in dir(microcontroller.pin): |
| 45 | + if getattr(microcontroller.pin, i) is pin: return i |
| 46 | + |
| 47 | +# Find all I/O pins, if IO_PINS is not explicitly set above |
| 48 | +if IO_PINS is None: |
| 49 | + IO_PINS = discover_io() |
| 50 | + |
| 51 | +# Initialize all pins as inputs, make a lookup table to get the name from the pin |
| 52 | +ios_lookup = dict([(pin_lookup(pin), DigitalInOut(pin)) for pin in IO_PINS]) |
| 53 | +ios = ios_lookup.values() |
| 54 | +ios_items = ios_lookup.items() |
| 55 | +for io in ios: |
| 56 | + io.switch_to_input(pull=Pull.UP) |
| 57 | + |
| 58 | +# Partial implementation of 'defaultdict' class from standard Python |
| 59 | +# from https://github.com/micropython/micropython-lib/blob/master/python-stdlib/collections.defaultdict/collections/defaultdict.py |
| 60 | +class defaultdict: |
| 61 | + @staticmethod |
| 62 | + def __new__(cls, default_factory=None, **kwargs): |
| 63 | + # Some code (e.g. urllib.urlparse) expects that basic defaultdict |
| 64 | + # functionality will be available to subclasses without them |
| 65 | + # calling __init__(). |
| 66 | + self = super(defaultdict, cls).__new__(cls) |
| 67 | + self.d = {} |
| 68 | + return self |
| 69 | + |
| 70 | + def __init__(self, default_factory=None, **kwargs): |
| 71 | + self.d = kwargs |
| 72 | + self.default_factory = default_factory |
| 73 | + |
| 74 | + def __getitem__(self, key): |
| 75 | + try: |
| 76 | + return self.d[key] |
| 77 | + except KeyError: |
| 78 | + v = self.__missing__(key) |
| 79 | + self.d[key] = v |
| 80 | + return v |
| 81 | + |
| 82 | + def __setitem__(self, key, v): |
| 83 | + self.d[key] = v |
| 84 | + |
| 85 | + def __delitem__(self, key): |
| 86 | + del self.d[key] |
| 87 | + |
| 88 | + def __contains__(self, key): |
| 89 | + return key in self.d |
| 90 | + |
| 91 | + def __missing__(self, key): |
| 92 | + if self.default_factory is None: |
| 93 | + raise KeyError(key) |
| 94 | + return self.default_factory() |
| 95 | + |
| 96 | +# Track combinations that were pressed, including ones during the "junk" scan |
| 97 | +pressed_or_junk = defaultdict(set) |
| 98 | +# Track combinations that were pressed, excluding the "junk" scan |
| 99 | +pressed = defaultdict(set) |
| 100 | +# During the first run, anything scanned is "junk". Could occur for unused pins. |
| 101 | +first_run = True |
| 102 | +# List of pins identified as rows and columns |
| 103 | +rows = [] |
| 104 | +cols = [] |
| 105 | +# The first pin identified is arbitrarily called a 'row' pin. |
| 106 | +row_arbitrarily = None |
| 107 | + |
| 108 | +while True: |
| 109 | + changed = False |
| 110 | + last_pressed = None |
| 111 | + for value in values: |
| 112 | + pull = [Pull.UP, Pull.DOWN][value] |
| 113 | + for io in ios: |
| 114 | + io.switch_to_input(pull=pull) |
| 115 | + for name1, io1 in ios_items: |
| 116 | + io1.switch_to_output(value) |
| 117 | + for name2, io2 in ios_items: |
| 118 | + if io2 is io1: continue |
| 119 | + if io2.value == value: |
| 120 | + if first_run: |
| 121 | + pressed_or_junk[name1].add(name2) |
| 122 | + pressed_or_junk[name2].add(name1) |
| 123 | + elif name2 not in pressed_or_junk[name1]: |
| 124 | + if row_arbitrarily is None: row_arbitrarily = name1 |
| 125 | + pressed_or_junk[name1].add(name2) |
| 126 | + pressed_or_junk[name2].add(name1) |
| 127 | + if name2 not in pressed[name1]: |
| 128 | + pressed[name1].add(name2) |
| 129 | + pressed[name2].add(name1) |
| 130 | + changed = True |
| 131 | + if name2 in pressed[name1]: |
| 132 | + last_pressed = (name1, name2) |
| 133 | + print("Key registered. Release to continue") |
| 134 | + while io2.value == value: pass |
| 135 | + io1.switch_to_input(pull=pull) |
| 136 | + if first_run: |
| 137 | + print("Press keys now") |
| 138 | + first_run = False |
| 139 | + elif changed: |
| 140 | + rows = set([row_arbitrarily]) |
| 141 | + cols = set() |
| 142 | + to_check = [row_arbitrarily] |
| 143 | + for check in to_check: |
| 144 | + for other in pressed[check]: |
| 145 | + if other in rows or other in cols: continue |
| 146 | + if check in rows: |
| 147 | + cols.add(other) |
| 148 | + else: |
| 149 | + rows.add(other) |
| 150 | + to_check.append(other) |
| 151 | + |
| 152 | + rows = sorted(rows) |
| 153 | + cols = sorted(cols) |
| 154 | + if changed or last_pressed: |
| 155 | + print("Rows", len(rows), *rows) |
| 156 | + print("Cols", len(cols), *cols) |
| 157 | + print("Last pressed", *last_pressed) |
| 158 | + print() |
0 commit comments