From 9d748f0ab861d29239f96be2df5d29428c02b392 Mon Sep 17 00:00:00 2001 From: lhahn Date: Mon, 11 Nov 2024 11:38:09 +0100 Subject: [PATCH] Setup first approach on how to handle membran keypad. Provides class with external readable buffer; pin inputs are IRQ FALLING based. Handler can be overwritten, keypad size is adjustable and character map can be provided. --- src/input/ukeypad.py | 256 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 256 insertions(+) create mode 100644 src/input/ukeypad.py diff --git a/src/input/ukeypad.py b/src/input/ukeypad.py new file mode 100644 index 0000000..946a93d --- /dev/null +++ b/src/input/ukeypad.py @@ -0,0 +1,256 @@ +import machine +import utime + + +class membrane: + """Class for interpreting input from a membran-like keypad, + the default is a 4x4 matrix array membrane keypad. + The layout of key bindings and size (i.e. 4x3 alternative) + can be customized. + Also the Callback/Handler can be overwritten if needed. + + In default case, the handler pushes characters into an + internal queue-like list/buffer. These characters can be + returned; default interactions like len, indexing and string + representation are done on this buffer. + + Please keep in mind, that characters are just pushed onto that list. + In order to prevent memory leaking on a pico by randomly pressing + buttons, you should clear the buffer before waiting for sensible + input and afterwards wipe your input characters. + """ + @classmethod + def _validate_input(cls, characters, rows, cols): + """Validates input data to ensure, that all pins are related + to characters, i.e. dimension of keymap fits the number of + row pins and column pins. + If there is a problem, a ValueError will be raised. + + Keyword arguments: + characters -- keypad character map, maps row/col pins + to printable characters + rows -- pins that correspond to the rows of the keypad + cols -- pins that correspond to the columns of the keypad + """ + if not isinstance(rows, tuple) and not isinstance(rows, list): + raise ValueError( + "Row pins need to be either type LIST or TUPLE." + ) + if not isinstance(cols, tuple) and not isinstance(cols, list): + raise ValueError( + "Column pins need to be either type LIST or TUPLE." + ) + if ( + not isinstance(characters, tuple) and + not isinstance(characters, list) + ): + raise ValueError( + "Keypad Characters need to be either type LIST or " + "TUPLE of LIST or TUPLE." + ) + if any( + not isinstance(chrs, tuple) and not isinstance(chrs, list) + for chrs in characters + ): + raise ValueError( + "Keypad Characters need to be either type LIST or " + "TUPLE of LIST or TUPLE." + ) + if len(characters) < len(rows): + raise ValueError( + f"Number of row pins ({len(rows)}) is beyond " + "length of first dimension of " + "character pad." + ) + if any(len(chrs) < len(cols) for chrs in characters): + raise ValueError( + f"Number of column pins ({len(cols)}) is beyond " + "length of second dimension " + "of character pad." + ) + + DEFAULT_KEYPAD = ( + ('1', '2', '3', 'A'), + ('4', '5', '6', 'B'), + ('7', '8', '9', 'C'), + ('*', '0', '#', 'D') + ) + DEFAULT_ROW_PINS = (2, 3, 4, 5) + DEFAULT_COL_PINS = (6, 7, 8, 9) + PUD_UP_ACTIVE_STATE = 0 + DEFAULT_BUFFER_TIME = 0.1 + + def __init__( + self, callback=None, + row_pins=DEFAULT_ROW_PINS, col_pins=DEFAULT_COL_PINS, + characters=DEFAULT_KEYPAD, buffer_time=DEFAULT_BUFFER_TIME + ): + """Constructor to initialise the keypad membrane. + + Keyword arguments: + callback -- Function that is called when a butten is + pressed (IRQ FALLING) + row_pins -- list of pins corresponding to the rows of the keypad + col_pins -- list of pins corresponding to the columns of the keypad + characters -- keypad character map, maps row/col pins to + printable characters + buffer_time -- time to wait in order to validate an input + and not a button issue. + """ + self._validate_input(characters, row_pins, col_pins) + self._characters = characters + self._callback = ( + callback + if callback is not None + else + self._default_callback + ) + self._row_pins = row_pins + self._col_pins = col_pins + self._size = (len(self._row_pins), len(self._col_pins)) + self._pins_row = [ + machine.Pin(pin, mode=machine.Pin.OUT) + for pin in self._row_pins + ] + self._pins_col = [ + machine.Pin(pin, mode=machine.Pin.IN, pull=machine.Pin.PULL_UP) + for pin in self._col_pins + ] + self._buffer = [] + self._buffer_time = buffer_time + self._mutex = False + self._reset() + + def __getitem__(self, index): + """Direct access to an entry of the buffer, i.e. the n-th byte. + + Keyword arguments: + index -- Position to retrieve the input from the buffer + """ + return self._buffer[index] + + def __delitem__(self, index): + """Removes data from the buffer at a certain position. + + Keyword arguments: + index -- Position to delete the data from the buffer + """ + del self._buffer[index] + + def __len__(self): + """Returns the current length/size of the buffer.""" + return len(self._buffer) + + def __contains__(self, item): + """Allows checking if a certain character/item is withing the buffer + at the moment. + + Keyword arguments: + item -- The item to be checked if it is part of the buffer + + Returns boolean if the item is contained by the buffer + """ + return item in self._buffer + + def __str__(self): + """Returns the current content of the buffer.""" + return self.__repr__() + + def __repr__(self): + """Returns the current content of the buffer.""" + return "".join(map(str, self._buffer)) + + def _reset(self): + """Reset function to re-init row pins into Pin.OUT state + and re-init the column pins into Pin.IN with interrup + handling Pin.IRQ_FALLING and reattaching the handler. + Resets the mutex to prevent multiple buttons together pressed. + """ + for pin in self._pins_row: + pin.init(mode=machine.Pin.OUT, pull=None) + for pin in self._pins_col: + pin.init(mode=machine.Pin.IN, pull=machine.Pin.PULL_UP) + pin.irq(trigger=machine.Pin.IRQ_FALLING, handler=self._callback) + self._mutex = False + + def _default_callback(self, cpin): + """Default callback function that is triggered as interrupt + handler when pressing a button. + Based on the column pin of the pressed button, this pin will + be made to an OUT pin and all rows become temporarly to + IN pins to identify the row and column of the pressed button + on the keypad matrix membrane. + With the identified row and column, the corresponding character + is pushed onto the internal buffer/list, so that external + components can read from the buffer. + + Keyword arguments: + cpin -- The machine.Pin object of the triggered pin. + """ + utime.sleep(self._buffer_time) + if ( + cpin not in self._pins_col or + cpin.value() != self.PUD_UP_ACTIVE_STATE + ): + return + col = self._pins_col.index(cpin) + if not self._mutex: + # Do not allow multiple key pressed together + self._mutex = True + + # switch function of rows and cols, first + # we got the column, now we need to find where + # the row is pressed. + for rpin in self._pins_row: + rpin.init(mode=machine.Pin.IN, pull=machine.Pin.PULL_UP) + + # Make selected column to pin state as rows before. + cpin.irq(handler=None) + cpin.init(mode=machine.Pin.OUT, pull=None) + + values = [rpin.value() for rpin in self._pins_row] + if self.PUD_UP_ACTIVE_STATE not in values: + self._reset() + return + row = values.index(self.PUD_UP_ACTIVE_STATE) + self._buffer.append(self._characters[row][col]) + self._reset() + + def clear(self): + """Flushes the buffer list in order to clean up. + Usefull to prevent buffer overflow from time to time. + """ + self._buffer.clear() + + def pop(self, index=0): + """Returns and removes the data from a certain position within + the buffer. + + Keyword arguments: + index -- The position to return and remove data from (default 0) + + Returns the data from the position. + """ + return self._buffer.pop(index) + + def pop_all(self): + """Returns the entire buffer and cleans it. + + Returns the entire buffer data. + """ + data = list(self._buffer) + self.clear() + return data + + +if __name__ == "__main__": + membrane_keypad = membrane() + pin_size = 12 + + while len(membrane_keypad) < pin_size: + utime.sleep(membrane_keypad.DEFAULT_BUFFER_TIME) + print(f"\r{'*'*len(membrane_keypad)}\t", end="") + print(f"\nInputData: {membrane_keypad}") + print(f"'#' is contained in buffer: {'#' in membrane_keypad}") + print(f"'*' is contained in buffer: {'*' in membrane_keypad}") + print(f"Popped data is: {membrane_keypad.pop_all()}")