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()