code.py
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) 40 if isinstance(pin_maybe := getattr(microcontroller.pin, name), microcontroller.Pin)] 41 42 def pin_lookup(pin): 43 for i in dir(board): 44 if getattr(board, i) is pin: 45 return i 46 for i in dir(microcontroller.pin): 47 if getattr(microcontroller.pin, i) is pin: 48 return i 49 return str(pin) 50 51 # Find all I/O pins, if IO_PINS is not explicitly set above 52 if IO_PINS is None: 53 IO_PINS = discover_io() 54 55 # Initialize all pins as inputs, make a lookup table to get the name from the pin 56 ios_lookup = dict([(pin_lookup(pin), DigitalInOut(pin)) for pin in IO_PINS]) # pylint: disable=consider-using-dict-comprehension 57 ios = ios_lookup.values() 58 ios_items = ios_lookup.items() 59 for io in ios: 60 io.switch_to_input(pull=Pull.UP) 61 62 # Partial implementation of 'defaultdict' class from standard Python from 63 # https://github.com/micropython/micropython-lib/blob/master/python-stdlib/collections.defaultdict/collections/defaultdict.py 64 class defaultdict: 65 @staticmethod 66 def __new__(cls, default_factory=None, **kwargs): # pylint: disable=unused-argument 67 # Some code (e.g. urllib.urlparse) expects that basic defaultdict 68 # functionality will be available to subclasses without them 69 # calling __init__(). 70 self = super(defaultdict, cls).__new__(cls) 71 self.d = {} 72 return self 73 74 def __init__(self, default_factory=None, **kwargs): 75 self.d = kwargs 76 self.default_factory = default_factory 77 78 def __getitem__(self, key): 79 try: 80 return self.d[key] 81 except KeyError: 82 v = self.__missing__(key) 83 self.d[key] = v 84 return v 85 86 def __setitem__(self, key, v): 87 self.d[key] = v 88 89 def __delitem__(self, key): 90 del self.d[key] 91 92 def __contains__(self, key): 93 return key in self.d 94 95 def __missing__(self, key): 96 if self.default_factory is None: 97 raise KeyError(key) 98 return self.default_factory() 99 100 # Track combinations that were pressed, including ones during the "junk" scan 101 pressed_or_junk = defaultdict(set) 102 # Track combinations that were pressed, excluding the "junk" scan 103 pressed = defaultdict(set) 104 # During the first run, anything scanned is "junk". Could occur for unused pins. 105 first_run = True 106 # List of pins identified as rows and columns 107 rows = [] 108 cols = [] 109 # The first pin identified is arbitrarily called a 'row' pin. 110 row_arbitrarily = None 111 112 while True: 113 changed = False 114 last_pressed = None 115 for value in values: 116 pull = [Pull.UP, Pull.DOWN][value] 117 for io in ios: 118 io.switch_to_input(pull=pull) 119 for name1, io1 in ios_items: 120 io1.switch_to_output(value) 121 for name2, io2 in ios_items: 122 if io2 is io1: 123 continue 124 if io2.value == value: 125 if first_run: 126 pressed_or_junk[name1].add(name2) 127 pressed_or_junk[name2].add(name1) 128 elif name2 not in pressed_or_junk[name1]: 129 if row_arbitrarily is None: 130 row_arbitrarily = name1 131 pressed_or_junk[name1].add(name2) 132 pressed_or_junk[name2].add(name1) 133 if name2 not in pressed[name1]: 134 pressed[name1].add(name2) 135 pressed[name2].add(name1) 136 changed = True 137 if name2 in pressed[name1]: 138 last_pressed = (name1, name2) 139 print("Key registered. Release to continue") 140 while io2.value == value: 141 pass 142 io1.switch_to_input(pull=pull) 143 if first_run: 144 print("Press keys now") 145 first_run = False 146 elif changed: 147 rows = set([row_arbitrarily]) 148 cols = set() 149 to_check = [row_arbitrarily] 150 for check in to_check: 151 for other in pressed[check]: 152 if other in rows or other in cols: 153 continue 154 if check in rows: 155 cols.add(other) 156 else: 157 rows.add(other) 158 to_check.append(other) 159 160 rows = sorted(rows) 161 cols = sorted(cols) 162 if changed or last_pressed: 163 print("Rows", len(rows), *rows) 164 print("Cols", len(cols), *cols) 165 print("Last pressed", *last_pressed) 166 print()