From: Melissa LeBlanc-Williams Date: Tue, 27 Jul 2021 18:32:52 +0000 (-0700) Subject: Added keypad module X-Git-Tag: 6.13.0^2~1 X-Git-Url: https://git.ayoreis.com/Adafruit_Blinka-hackapet.git/commitdiff_plain/d8fb7b4e16ecf2b114845a9915ea8c6d26a80e8b Added keypad module --- diff --git a/src/keypad.py b/src/keypad.py new file mode 100644 index 0000000..b602a68 --- /dev/null +++ b/src/keypad.py @@ -0,0 +1,488 @@ +""" +`keypad` - Support for scanning keys and key matrices +=========================================================== +See `CircuitPython:keypad` in CircuitPython for more details. + +* Author(s): Melissa LeBlanc-Williams +""" + +import time +import threading +from collections import deque +import digitalio + + +class Event: + """A key transition event.""" + + def __init__(self, key_number=0, pressed=True): + """ + Create a key transition event, which reports a key-pressed or key-released transition. + + :param int key_number: the key number + :param bool pressed: ``True`` if the key was pressed; ``False`` if it was released. + """ + self._key_number = key_number + self._pressed = pressed + + @property + def key_number(self): + """The key number.""" + return self._key_number + + @property + def pressed(self): + """ + ``True`` if the event represents a key down (pressed) transition. + The opposite of `released`. + """ + return self._pressed + + @property + def released(self): + """ + ``True`` if the event represents a key up (released) transition. + The opposite of `pressed`. + """ + return not self._pressed + + def __eq__(self, other): + """ + Two `Event` objects are equal if their `key_number` + and `pressed`/`released` values are equal. + """ + return self.key_number == other.key_number and self.pressed == other.pressed + + def __hash__(self): + """Returns a hash for the `Event`, so it can be used in dictionaries, etc..""" + return hash(self._key_number) + + def __repr__(self): + """Return a textual representation of the object""" + return "".format( + self.key_number, "pressed" if self._pressed else "released" + ) + + +class _EventQueue: + """ + A queue of `Event` objects, filled by a `keypad` scanner such as `Keys` or `KeyMatrix`. + + You cannot create an instance of `_EventQueue` directly. Each scanner creates an + instance when it is created. + """ + + def __init__(self, max_events): + self._events = deque([], max_events) + self._overflowed = False + + def get(self): + """ + Return the next key transition event. Return ``None`` if no events are pending. + + Note that the queue size is limited; see ``max_events`` in the constructor of + a scanner such as `Keys` or `KeyMatrix`. + If a new event arrives when the queue is full, the event is discarded, and + `overflowed` is set to ``True``. + + :return: the next queued key transition `Event` + :rtype: Optional[Event] + """ + if not self._events: + return None + return self._events.popleft() + + def get_into(self, event): + """Store the next key transition event in the supplied event, if available, + and return ``True``. + If there are no queued events, do not touch ``event`` and return ``False``. + + The advantage of this method over ``get()`` is that it does not allocate storage. + Instead you can reuse an existing ``Event`` object. + + Note that the queue size is limited; see ``max_events`` in the constructor of + a scanner such as `Keys` or `KeyMatrix`. + + :return ``True`` if an event was available and stored, ``False`` if not. + :rtype: bool + """ + if not self._events: + return False + next_event = self._events.popleft() + # pylint: disable=protected-access + event._key_number = next_event._key_number + event._pressed = next_event._pressed + # pylint: enable=protected-access + return True + + def clear(self): + """ + Clear any queued key transition events. Also sets `overflowed` to ``False``. + """ + self._events.clear() + self._overflowed = False + + def __bool__(self): + """``True`` if `len()` is greater than zero. + This is an easy way to check if the queue is empty. + """ + return len(self._events) > 0 + + def __len__(self): + """Return the number of events currently in the queue. Used to implement ``len()``.""" + return len(self._events) + + @property + def overflowed(self): + """ + ``True`` if an event could not be added to the event queue because it was full. (read-only) + Set to ``False`` by `clear()`. + """ + return self._overflowed + + def keypad_eventqueue_record(self, key_number, current): + """Record a new event""" + if len(self._events) == self._events.maxlen: + self._overflowed = True + else: + self._events.append(Event(key_number, current)) + + +class _KeysBase: + def __init__(self, interval, max_events, scanning_function): + self._interval = interval + self._last_scan = time.monotonic() + self._events = _EventQueue(max_events) + self._scanning_function = scanning_function + self._scan_thread = threading.Thread(target=self._scanning_loop, daemon=True) + self._scan_thread.start() + + @property + def events(self): + """The EventQueue associated with this Keys object. (read-only)""" + return self._events + + def deinit(self): + """Stop scanning""" + if self._scan_thread.is_alive(): + self._scan_thread.join() + + def __enter__(self): + """No-op used by Context Managers.""" + return self + + def __exit__(self, exception_type, exception_value, traceback): + """ + Automatically deinitializes when exiting a context. See + :ref:`lifetime-and-contextmanagers` for more info. + """ + self.deinit() + + def _scanning_loop(self): + while True: + self._scanning_function() + time.sleep(0.001) + + +class Keys(_KeysBase): + """Manage a set of independent keys.""" + + def __init__( + self, pins, *, value_when_pressed, pull=True, interval=0.02, max_events=64 + ): + """ + Create a `Keys` object that will scan keys attached to the given sequence of pins. + Each key is independent and attached to its own pin. + + An `EventQueue` is created when this object is created and is available in the + `events` attribute. + + :param Sequence[microcontroller.Pin] pins: The pins attached to the keys. + The key numbers correspond to indices into this sequence. + :param bool value_when_pressed: ``True`` if the pin reads high when the key is pressed. + ``False`` if the pin reads low (is grounded) when the key is pressed. + All the pins must be connected in the same way. + :param bool pull: ``True`` if an internal pull-up or pull-down should be + enabled on each pin. A pull-up will be used if ``value_when_pressed`` is ``False``; + a pull-down will be used if it is ``True``. + If an external pull is already provided for all the pins, you can set + ``pull`` to ``False``. + However, enabling an internal pull when an external one is already present is not + a problem; + it simply uses slightly more current. + :param float interval: Scan keys no more often than ``interval`` to allow for debouncing. + ``interval`` is in float seconds. The default is 0.020 (20 msecs). + :param int max_events: maximum size of `events` `EventQueue`: + maximum number of key transition events that are saved. + Must be >= 1. + If a new event arrives when the queue is full, the oldest event is discarded. + """ + self._digitalinouts = [] + for pin in pins: + dio = digitalio.DigitalInOut(pin) + if pull: + dio.pull = ( + digitalio.Pull.DOWN if value_when_pressed else digitalio.Pull.UP + ) + self._digitalinouts.append(dio) + + self._currently_pressed = [False] * len(pins) + self._previously_pressed = [False] * len(pins) + self._value_when_pressed = value_when_pressed + + super().__init__(interval, max_events, self._keypad_keys_scan) + + def deinit(self): + """Stop scanning and release the pins.""" + super().deinit() + for dio in self._digitalinouts: + dio.deinit() + + def reset(self): + """Reset the internal state of the scanner to assume that all keys are now released. + Any key that is already pressed at the time of this call will therefore immediately cause + a new key-pressed event to occur. + """ + self._currently_pressed = self._previously_pressed = [False] * self.key_count + + @property + def key_count(self): + """The number of keys that are being scanned. (read-only)""" + return len(self._digitalinouts) + + def _keypad_keys_scan(self): + if time.monotonic() - self._last_scan < self._interval: + return + self._last_scan = time.monotonic() + + for key_number, dio in enumerate(self._digitalinouts): + self._previously_pressed[key_number] = self._currently_pressed[key_number] + current = dio.value == self._value_when_pressed + self._currently_pressed[key_number] = current + if self._previously_pressed[key_number] != current: + self._events.keypad_eventqueue_record(key_number, current) + + +class KeyMatrix(_KeysBase): + """Manage a 2D matrix of keys with row and column pins.""" + + # pylint: disable=too-many-arguments + def __init__( + self, + row_pins, + column_pins, + columns_to_anodes=True, + interval=0.02, + max_events=64, + ): + """ + Create a `Keys` object that will scan the key matrix attached to the given row and + column pins. + There should not be any external pull-ups or pull-downs on the matrix: + ``KeyMatrix`` enables internal pull-ups or pull-downs on the pins as necessary. + + The keys are numbered sequentially from zero. A key number can be computed + by ``row * len(column_pins) + column``. + + An `EventQueue` is created when this object is created and is available in the `events` + attribute. + + :param Sequence[microcontroller.Pin] row_pins: The pins attached to the rows. + :param Sequence[microcontroller.Pin] column_pins: The pins attached to the colums. + :param bool columns_to_anodes: Default ``True``. + If the matrix uses diodes, the diode anodes are typically connected to the column pins, + and the cathodes should be connected to the row pins. If your diodes are reversed, + set ``columns_to_anodes`` to ``False``. + :param float interval: Scan keys no more often than ``interval`` to allow for debouncing. + ``interval`` is in float seconds. The default is 0.020 (20 msecs). + :param int max_events: maximum size of `events` `EventQueue`: + maximum number of key transition events that are saved. + Must be >= 1. + If a new event arrives when the queue is full, the oldest event is discarded. + """ + self._row_digitalinouts = [] + for row_pin in row_pins: + row_dio = digitalio.DigitalInOut(row_pin) + row_dio.switch_to_input( + pull=(digitalio.Pull.UP if columns_to_anodes else digitalio.Pull.DOWN) + ) + self._row_digitalinouts.append(row_dio) + + self._column_digitalinouts = [] + for column_pin in column_pins: + col_dio = digitalio.DigitalInOut(column_pin) + col_dio.switch_to_input( + pull=(digitalio.Pull.UP if columns_to_anodes else digitalio.Pull.DOWN) + ) + self._column_digitalinouts.append(col_dio) + self._currently_pressed = [False] * len(column_pins) * len(row_pins) + self._previously_pressed = [False] * len(column_pins) * len(row_pins) + self._columns_to_anodes = columns_to_anodes + + super().__init__(interval, max_events, self._keypad_keymatrix_scan) + + # pylint: enable=too-many-arguments + + @property + def key_count(self): + """The number of keys that are being scanned. (read-only)""" + return len(self._row_digitalinouts) * len(self._column_digitalinouts) + + def deinit(self): + """Stop scanning and release the pins.""" + super().deinit() + for row_dio in self._row_digitalinouts: + row_dio.deinit() + for col_dio in self._column_digitalinouts: + col_dio.deinit() + + def reset(self): + """ + Reset the internal state of the scanner to assume that all keys are now released. + Any key that is already pressed at the time of this call will therefore immediately cause + a new key-pressed event to occur. + """ + self._previously_pressed = self._currently_pressed = [False] * self.key_count + + def _row_column_to_key_number(self, row, column): + return row * len(self._column_digitalinouts) + column + + def _keypad_keymatrix_scan(self): + if time.monotonic() - self._last_scan < self._interval: + return + self._last_scan = time.monotonic() + + for row, row_dio in enumerate(self._row_digitalinouts): + row_dio.switch_to_output( + value=(not self._columns_to_anodes), + drive_mode=digitalio.DriveMode.PUSH_PULL, + ) + for col, col_dio in enumerate(self._column_digitalinouts): + key_number = self._row_column_to_key_number(row, col) + self._previously_pressed[key_number] = self._currently_pressed[ + key_number + ] + current = col_dio.value != self._columns_to_anodes + self._currently_pressed[key_number] = current + if self._previously_pressed[key_number] != current: + self._events.keypad_eventqueue_record(key_number, current) + row_dio.value = self._columns_to_anodes + row_dio.switch_to_input( + pull=( + digitalio.Pull.UP + if self._columns_to_anodes + else digitalio.Pull.DOWN + ) + ) + + +class ShiftRegisterKeys(_KeysBase): + """Manage a set of keys attached to an incoming shift register.""" + + def __init__( + self, + *, + clock, + data, + latch, + value_to_latch=True, + key_count, + value_when_pressed, + interval=0.02, + max_events=64 + ): + """ + Create a `Keys` object that will scan keys attached to a parallel-in serial-out + shift register like the 74HC165 or CD4021. + Note that you may chain shift registers to load in as many values as you need. + + Key number 0 is the first (or more properly, the zero-th) bit read. In the + 74HC165, this bit is labeled ``Q7``. Key number 1 will be the value of ``Q6``, etc. + + An `EventQueue` is created when this object is created and is available in the + `events` attribute. + + :param microcontroller.Pin clock: The shift register clock pin. + The shift register should clock on a low-to-high transition. + :param microcontroller.Pin data: the incoming shift register data pin + :param microcontroller.Pin latch: + Pin used to latch parallel data going into the shift register. + :param bool value_to_latch: Pin state to latch data being read. + ``True`` if the data is latched when ``latch`` goes high + ``False`` if the data is latched when ``latch goes low. + The default is ``True``, which is how the 74HC165 operates. The CD4021 latch is + the opposite. Once the data is latched, it will be shifted out by toggling the + clock pin. + :param int key_count: number of data lines to clock in + :param bool value_when_pressed: ``True`` if the pin reads high when the key is pressed. + ``False`` if the pin reads low (is grounded) when the key is pressed. + :param float interval: Scan keys no more often than ``interval`` to allow for debouncing. + ``interval`` is in float seconds. The default is 0.020 (20 msecs). + :param int max_events: maximum size of `events` `EventQueue`: + maximum number of key transition events that are saved. + Must be >= 1. + If a new event arrives when the queue is full, the oldest event is discarded. + """ + clock_dio = digitalio.DigitalInOut(clock) + clock_dio.switch_to_output( + value=False, drive_mode=digitalio.DriveMode.PUSH_PULL + ) + self._clock = clock_dio + + data_dio = digitalio.DigitalInOut(data) + data_dio.switch_to_input() + self._data = data_dio + + latch_dio = digitalio.DigitalInOut(latch) + latch_dio.switch_to_output(value=True, drive_mode=digitalio.DriveMode.PUSH_PULL) + self._latch = latch_dio + self._value_to_latch = value_to_latch + + self._currently_pressed = [False] * key_count + self._previously_pressed = [False] * key_count + self._value_when_pressed = value_when_pressed + self._key_count = key_count + + super().__init__(interval, max_events, self._keypad_shiftregisterkeys_scan) + + def deinit(self): + """Stop scanning and release the pins.""" + super().deinit() + self._clock.deinit() + self._data.deinit() + self._latch.deinit() + + def reset(self): + """ + Reset the internal state of the scanner to assume that all keys are now released. + Any key that is already pressed at the time of this call will therefore immediately cause + a new key-pressed event to occur. + """ + self._currently_pressed = self._previously_pressed = [False] * self._key_count + + @property + def key_count(self): + """The number of keys that are being scanned. (read-only)""" + return self._key_count + + @property + def events(self): + """The `EventQueue` associated with this `Keys` object. (read-only)""" + return self._events + + def _keypad_shiftregisterkeys_scan(self): + if time.monotonic() - self._last_scan < self._interval: + return + self._last_scan = time.monotonic() + + self._latch.value = self._value_to_latch + for key_number in range(self._key_count): + self._clock.value = False + self._previously_pressed[key_number] = self._currently_pressed[key_number] + current = self._data.value == self._value_when_pressed + self._currently_pressed[key_number] = current + self._clock.value = True + if self._previously_pressed[key_number] != current: + self._events.keypad_eventqueue_record(key_number, current) + + self._latch.value = not self._value_to_latch