Skip to content

keypad: support for vector and matrix key scanning #4891

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 42 commits into from
Jun 24, 2021

Conversation

dhalbert
Copy link
Collaborator

@dhalbert dhalbert commented Jun 15, 2021

keypad module with Keys, KeyMatrix, and Event classes. The scanning is done in the background. Key transition events (pressed or released) are put on an event queue. Direct query of a key state, bypassing the queue, is also possible. Debouncing is done by scanning once every 20 msecs.

Also added a simple atomic locking mechanism that I needed, in supervisor/shared/lock.{h,c}.

[EDIT: Revised example in a new comment below]

Sample code scanning key-per-pin using a NeoKey FeatherWing and RP2040 Feather (D5 and D6 are the default pins for the FeatherWIng):

import keypad
import board
import time

k = keypad.Keys((board.D5, board.D6), value_when_pressed=False, pull=False) # pulls built in

while True:
    print(k.next_event())
    time.sleep(0.1)

Sample code scanning a https://www.adafruit.com/product/1824 (telephone-style matrix) on a Metro M4:

import keypad
import board
import time

k = keypad.KeyMatrix(
    row_pins=(board.A0, board.A1, board.A2, board.A3),
    col_pins=(board.D0, board.D1, board.D2))

while True:
    event = k.next_event()
    if True:
        print(event)
    time.sleep(0.1)
Complete API doc in next comment.

Improved version of #4877

weblate and others added 21 commits June 2, 2021 12:41
Updated by "Update PO files to match POT (msgmerge)" hook in Weblate.

Translation: CircuitPython/main
Translate-URL: https://hosted.weblate.org/projects/circuitpython/main/
Currently translated at 100.0% (993 of 993 strings)

Translation: CircuitPython/main
Translate-URL: https://hosted.weblate.org/projects/circuitpython/main/es/
Currently translated at 100.0% (993 of 993 strings)

Translation: CircuitPython/main
Translate-URL: https://hosted.weblate.org/projects/circuitpython/main/sv/
Currently translated at 100.0% (993 of 993 strings)

Translation: CircuitPython/main
Translate-URL: https://hosted.weblate.org/projects/circuitpython/main/pt_BR/
Currently translated at 100.0% (993 of 993 strings)

Translation: CircuitPython/main
Translate-URL: https://hosted.weblate.org/projects/circuitpython/main/zh_Latn/
keypad.Buttons and keypad.State

Buttons -> Keys; further work

wip

wip

wip: compiles

about to try

keypad.Keys working
keypad.Buttons and keypad.State

Buttons -> Keys; further work

wip

wip

wip: compiles

about to try

keypad.Keys working
@dhalbert dhalbert requested review from ladyada and jepler June 15, 2021 15:48
@dhalbert
Copy link
Collaborator Author

dhalbert commented Jun 15, 2021

[EDIT: Revised API in a new comment below:]


keypad – Support for scanning keys and key matrices

The keypad module provides native support to scan sets of keys or buttons, connected independently to individual pins, or connected in a row-and-column matrix.

class keypad.Event(key_num: int, pressed: bool)

A key transition event.

Create a key transition event, which reports a key-pressed or key-released transition.

Parameters
  • key_num (int) – the key number

  • pressed (bool) – True if the key was pressed; False if it was released.

key_num :int

The key number.

pressed :bool

True if event represents a key down (pressed) transition. The opposite of released.

released :bool

True if event represents a key up (released) transition. The opposite of pressed.

__eq__(self, other: object)bool

Two Event objects are equal if their key_num and pressed/released values are equal.

__hash__(self)int

Returns a hash for the Event, so it can be used in dictionaries, etc..

class keypad.KeyMatrix(row_pins: Sequence[microcontroller.Pin], col_pins: Sequence[microcontroller.Pin], max_events: int = 64)

Manage a 2D matrix of keys with row and column pins.

Create a Keys object that will scan the key matrix attached to the given row and column pins. If the matrix uses diodes, the diode anodes should be connected to the column pins, and the cathodes should be connected to the row pins. If your diodes are reversed, simply exchange the row and column pin sequences.

The keys are numbered sequentially from zero. A key number can be computed by row * len(col_pins) + col.

The keys are debounced by waiting about 20 msecs before reporting a transition.

Parameters
  • row_pins (Sequence[microcontroller.Pin]) – The pins attached to the rows.

  • col_pins (Sequence[microcontroller.Pin]) – The pins attached to the colums.

  • max_events (int) – Size of key event queue: 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.

deinit(self)None

Stop scanning and release the pins.

__enter__(self)KeyMatrix

No-op used by Context Managers.

__exit__(self)None

Automatically deinitializes when exiting a context. See Lifetime and ContextManagers for more info.

next_event(self)Optional[Event]

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. If a new event arrives when the queue is full, the oldest event is discarded.

Returns

the next queued key transition Event

Return type

Optional[Event]

clear_events(self)None

Clear any queued key transition events.

pressed(self, key_num: int)None

Return True if the given key is pressed. This is a debounced read of the key state which bypasses the event queue.

key_num(self, row: int, col: int)int

Return the key number for a given row and column. The key number is calculated by row * len(col_pins) + col.

class keypad.Keys(pins: Sequence[microcontroller.Pin], *, level_when_pressed: bool, pull: bool = True, max_events: int = 64)

Manage a set of independent keys.

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.

The keys are debounced by waiting about 20 msecs before reporting a transition.

Parameters
  • pins (Sequence[microcontroller.Pin]) – The pins attached to the keys. The key numbers correspond to indices into this sequence.

  • value_when_pressed (bool) – 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.

  • pull (bool) – 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.

  • max_events (int) – Size of key event queue: 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.

deinit(self)None

Stop scanning and release the pins.

__enter__(self)Keys

No-op used by Context Managers.

__exit__(self)None

Automatically deinitializes when exiting a context. See Lifetime and ContextManagers for more info.

next_event(self)Optional[Event]

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. If a new event arrives when the queue is full, the oldest event is discarded.

Returns

the next queued key transition Event

Return type

Optional[Event]

clear_events(self)None

Clear any queued key transition events.

pressed(self, key_num: int)None

Return True if the given key is pressed. This is a debounced read of the key state which bypasses the event queue.

@dhalbert dhalbert force-pushed the keypad-scanning-events branch from 154412f to f97e0ec Compare June 15, 2021 15:51
@deshipu
Copy link

deshipu commented Jun 15, 2021

I wonder if it would make sense to make those classes follow the iterator protocol, instead of having a custom named next_event function. Compare:

while True:
    event = matrix.next_event()
    ...

with

for event in matrix:
   ...

and

while True:
    event = next(matrix)
   ...

the latter two using the iterator protocol.

@deshipu
Copy link

deshipu commented Jun 15, 2021

Maybe it would also make sense to have a was_pressed method, like the Miro:bit has on its buttons: https://microbit-micropython.readthedocs.io/en/v1.0.1/button.html#Button.was_pressed

@dhalbert
Copy link
Collaborator Author

I wonder if it would make sense to make those classes follow the iterator protocol

I thought about that, and would be willing to entertain it. But there might be use cases for not using an iterator. I was also thinking about future additions, including things such as:

  • next_pressed_event() -- skips released events
  • wait_for_event() -- does not return None;blocks waiting for a real event
  • next_pressed_key_num() -- skips creating an Event altogether, and just returns an int key_num. Maybe it's blocking.

These could be intermixed with fetching events with next_event().

I have also thought about factoring out the event queue, because the function names are duplicated in each keypad implementation. So for instance:

k = keypad.Keys(...)
event = k.event_queue.next()
# or even
events = k.event_queue
event = events.next()
events.clear()
next_press = events.next_pressed_key_num()

This is more complicated initially, but the factoring out reduces internal code size.

@dhalbert
Copy link
Collaborator Author

Maybe it would also make sense to have a was_pressed method, like the Miro:bit has on its buttons: https://microbit-micropython.readthedocs.io/en/v1.0.1/button.html#Button.was_pressed

Or like gamepad, which latches. I think this is subsumed by the events, which are history. And the question is if was_pressed() clears the history, as it does in gamepad, what happens to the corresponding events, and does it clear history just for that key?

@tannewt tannewt self-requested a review June 21, 2021 22:28
Copy link
Member

@tannewt tannewt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's my take on the APIs. I really like the new arg validation and imagine we'll want to upstream it. Thanks!

@dhalbert
Copy link
Collaborator Author

dhalbert commented Jun 21, 2021

I pushed f052dc4 because the SNES controller uses a shift register (CD4021 or equivalent) with a latch pin whose sense is opposite from 74HC165, so this needs to be parameterized.

@dhalbert dhalbert force-pushed the keypad-scanning-events branch from f47ebbc to f052dc4 Compare June 21, 2021 23:50
@tannewt
Copy link
Member

tannewt commented Jun 22, 2021

Looks like you have more to push (for more resolved items) so let me know when that is pushed.

@dhalbert dhalbert force-pushed the keypad-scanning-events branch from 18dc095 to acf90fb Compare June 23, 2021 13:57
Copy link
Member

@tannewt tannewt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove pressed and get_states_into. They provide more than one way to use the API and that may lead to confusion. They are also timing sensitive because they do not latch. If someone wants this API, they can be derived from the event queue.

One question about overflow as well.

Thanks! It's getting close.

@dhalbert dhalbert requested a review from tannewt June 23, 2021 21:08
@dhalbert
Copy link
Collaborator Author

dhalbert commented Jun 23, 2021

@tannewt Ready for re-review.

  1. acf90fb: much renaming: num -> number, col -> column, etc.

  2. 7774b18: Overflow handling changes:
    a. EventQueue.overflowed is now read-only. It is set when an event arrives but the queue is full. Nothing else happens immediately; the extra event is just discarded.
    b. EventQueue.clear() will reset .overflowed back to False, and also empties the queue. It does not affect the keypress history kept by the scanners Keys, KeyMatrix, or ShiftRegisterKeys.
    c. scanner.reset() resets the internal scanner state, forcing all keys to be consider as released. So if any key is being held down at scanner.reset() time, it will immediately send an event indicating a key-pressed transition. This is in the doc. scanner.reset() does not affect the state of EventQueue.overflowed nor does it call EventQueue.clear().

  3. 4f538b6 and 2e67d67: Remove scanner.pressed() and scanner.get_states_into().

Copy link
Member

@tannewt tannewt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One minor thing. Looks good otherwise.

@klardotsh
Copy link

klardotsh commented Jun 23, 2021

Reading through the docs in #4891 (comment) and the example a comment or two below it, this looks nice to work with and it'd probably be pretty straightforward to port KMK over to this API and (1) rip out a bunch of messy DIY code I haven't really touched in years, and (2) get the C speed boost.

The only thing our matrix implementation handles that this implementation doesn't appear to have an obvious-to-me direct equivalent for is what KMK calls rollover_cols_every_rows, which is a hack to implement a particular keyboard (I think it was the Planck Rev6, QMK implementation in https://github.com/qmk/qmk_firmware/tree/master/keyboards/planck/rev6) that "reuses" pins, in a sense (there's 12 columns and 4 rows, but 6/8 pins being used). This should still be fine; if moved to this new API, KMK can probably just hack around it in the board definition by defining a 6x8 matrix and remapping the entire right half using our other remapping hack that lives in keymap land, rather than matrix land.

tl;dr 👍 :shipit: as far as I and KMK are concerned, and thanks for the heads up!

Co-authored-by: Scott Shawcroft <[email protected]>
@klardotsh
Copy link

(Oh - is it safe to assume this would make whatever the next stable CircuitPython is after merge? We'd have to communicate a new minimum CPy version and rebase KMKPython off of that version, and we try not to rebase off of HEAD)

@dhalbert
Copy link
Collaborator Author

(Oh - is it safe to assume this would make whatever the next stable CircuitPython is after merge? We'd have to communicate a new minimum CPy version and rebase KMKPython off of that version, and we try not to rebase off of HEAD)

Definitely yes, this will go in the next 7.0.0 alpha (or maybe it will be beta) release, and will be in 7.0.0 final.

@dhalbert dhalbert requested a review from tannewt June 24, 2021 02:21
Copy link
Member

@tannewt tannewt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the update and your patience with my feedback!

@tannewt tannewt merged commit b81573d into adafruit:main Jun 24, 2021
@dhalbert dhalbert deleted the keypad-scanning-events branch June 24, 2021 17:26
@dtwilliamson
Copy link

I am looking at this as a replacement for adafruit_debouncer for use with an Adafruit Adafruit NeoKey 1x4 QT I2C Breakout. That library supports keys attached to pins but it also supports a boolean function. This is needed because the board is I2C instead of they keys being directly connected to pins, if I understand things correctly.

neokey1 = NeoKey1x4(board.I2C())
key0 = Debouncer(lambda: neokey1[0])    # subscripts 0..3 access the four keys' boolean states
while True:
    key0.update()
    if key0.rose:
        print('key 0 pressed')

Polling four keys in Python in this way works but is slow and misses some key presses since it happens in the main flow of execution.

Does (or will) your lib support this use case? Please feel free to correct any misunderstandings I may have.

@dhalbert
Copy link
Collaborator Author

This is not a library per se but is native C code. So the low-level background scanning would need to do I2C transactions to read the keys. We will consider adding more native C scanners. Feel free to open an enhancement request issue.

@durapensa
Copy link

@dhalbert #4891 (comment) Just today I came across a use case with the NeoKey Trinkey where it would be nice to have a single lib handle events & debounce, etc. for both the switch and the cap touch, so created the issue #5014 (comment). I hope the issue meets guidelines - I'm happy to rework it if not.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.