Skip to content

Implement keypad scanning #4877

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

Closed
wants to merge 16 commits into from
Closed

Conversation

dhalbert
Copy link
Collaborator

@dhalbert dhalbert commented Jun 10, 2021

Draft PR (for now) with simple keypad scanning for comments on API. Works. Documentation in shared-bindings/keypad/*.c is complete. This implements only single-pin-per-key, but the API for matrix keypads and shift-register will be similar, with the ability to convert from row/col to key number in the matrix API.

Sample code, tested with a Feather RP2040 and a NeoKey 2 FeatherWing:

import keypad
import board

k = keypad.Keys((board.D5, board.D6), value_when_pressed=False, pull=False)

# key state is written into this list, to avoid storage allocation.
state_list = [None, None]

while True:
    if k.scan():
        print("D5:", k.state(0), "D6:", k.state(1))
        k.keys_with_state(keypad.State.JUST_PRESSED, state_list)
        print("just pressed: ", state_list)
        k.keys_with_state(keypad.State.JUST_RELEASED, state_list)
        print("just released: ", state_list)
        k.keys_with_state(keypad.State.PRESSED, state_list)
        print("just pressed or still pressed: ", state_list)

Sample output:

D5: keypad.State.STILL_RELEASED D6: keypad.State.STILL_RELEASED
just pressed:  [None, None]
just released:  [None, None]
just pressed or still pressed:  [None, None]
D5: keypad.State.STILL_RELEASED D6: keypad.State.JUST_PRESSED
just pressed:  [1, None]
just released:  [None, None]
just pressed or still pressed:  [1, None]
D5: keypad.State.STILL_RELEASED D6: keypad.State.STILL_PRESSED
just pressed:  [None, None]
just released:  [None, None]
just pressed or still pressed:  [1, None]
D5: keypad.State.STILL_RELEASED D6: keypad.State.STILL_PRESSED
just pressed:  [None, None]
just released:  [None, None]
just pressed or still pressed:  [1, None]
D5: keypad.State.STILL_RELEASED D6: keypad.State.STILL_PRESSED
just pressed:  [None, None]
just released:  [None, None]
just pressed or still pressed:  [1, None]
D5: keypad.State.STILL_RELEASED D6: keypad.State.JUST_RELEASED
just pressed:  [None, None]
just released:  [1, None]
just pressed or still pressed:  [None, None]
D5: keypad.State.STILL_RELEASED D6: keypad.State.STILL_RELEASED
just pressed:  [None, None]
just released:  [None, None]
just pressed or still pressed:  [None, None]

weblate and others added 13 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 marked this pull request as draft June 10, 2021 04:30
@dhalbert
Copy link
Collaborator Author

dhalbert commented Jun 10, 2021

Some more comments, added to continue some internal discussions we have had internally:

This works rather differently than gamepad, which I was originally interested in emulating, but eventually did not.

  1. The adafruit_debouncer library, the adafruit_matrix_keypad library, and the x0xb0x code all scan the keys on demand. The user program calls the scan routine as part of a loop, and then queries the latest scan results. Most do debouncing, so if you call scan too soon the scanner will notice and return without changing the states.
  2. gamepad is different. It works in the background and remembers all the buttons that were ever pressed. You could call it once a minute if you liked, and it would tell you all the buttons that were pressed (and possibly released) in the past minute. In other words, it is latching, so even if a button was pressed and released in the distant passed, it will have recorded a press.
    After you query the state, it resets the state to zero (all unpressed). It doesn't tell you "just pressed" kind of info. It might be possible to detect that, but then you need some explicit or implicit reset of that info, and the explicit scan() call would serve the same purpose.

Case 1 is what all the keyboard platforms like QMK, KMK, etc., and also x0xb0x, etc, do. There's a big loop, and they call scan() when they're ready and then act on it. So they are getting real-time info, unlike case 2, which returns historical info.

@dhalbert
Copy link
Collaborator Author

Module doc as of this comment (this HTML rendering came out better than I expected; it's just part of the raw HTML from the readthedocs page):

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.Keys(pins: Sequence[microcontroller.Pin], *, level_when_pressed: bool, pull: bool = True)

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.

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.

Calls scan() once before returning, to initialize internal state.

scan(self)bool

Scan the keys and record which are newly pressed, still pressed, newly released, and still released. If not enough time has elapsed since the last scan for debouncing, do nothing and return False.

Returns

True if sufficient time has elapsed for debouncing (about 20 msecs), otherwise False.

Return type

bool

state(self, key_num: int)State

Return the state for the given key_num, based on the results of the most recent scan().

Parameters

key_num (int) – Key number: corresponds to the sequence of pins

Returns

state of key number key_num

Return type

keypad.State: One of State.JUST_PRESSED, State.STILL_PRESSED, State.JUST_RELEASED, or State.STILL_RELEASED. The inclusive states State.PRESSED and State.RELEASED will not be returned.

keys_with_state(self, state: State, into_list: List[Optional[int]])None

Store key numbers of keys with state state in into_list. The states checked are based on the results of the most recent scan().

You can use the inclusive states State.PRESSED and State.RELEASED. State.PRESSED includes states State.JUST_PRESSED and State.STILL_PRESSED. State.RELEASED includes State.JUST_RELEASED and State.STILL_RELEASED.

The key numbers are stored in into_list consecutively, up to len(into_list). The into_list is not extended if there are more keys with the given state than list slots. Instead, leftover key numbers are discarded. If there are fewer keys with the given state, the rest of into_list is padded with None. For example, if four keys are being monitored, and only key numbers 0 and 2 have the given state, into_list will be set to [0, 2, None, None]. You can iterate over into_list and stop when you find the first None.

class keypad.State

The state of a key, based on the last call to scan().

JUST_PRESSED :State

The key transitioned from released to pressed.

STILL_PRESSED :State

The key was already pressed, and continues to be pressed.

PRESSED :State

The key is now pressed. Used to indicate states JUST_PRESSED and STILL_PRESSED inclusively.

JUST_RELEASED :State

The key transitioned from pressed to released.

STILL_RELEASED :State

The key was already released, and continues to be released.

RELEASED :State

The key is now released. Used to indicate states JUST_RELEASED and STILL_RELEASED inclusively.

@dhalbert
Copy link
Collaborator Author

Some API naming and details discussion

  • keys_with_state() writes into an existing list, to avoid storage churn. There could be a variant that returns a fresh list or tuple.
    Naming alternatives:
    Keys.keys_with_state(state, into_list) ->
    Keys.keys_with_state_into(state, into_list)
    Keys.write_keys_with_state_to(state, into_list)

  • I experimented with the State being a bitmask, but it was more awkward. The current set of states is not an exclusive enum, because of PRESSED and RELEASED, which are unions of other states. These could be called DOWN and UP, too, though the current names are more evocative of the individual states.

  • I'll be adding matrix support and maybe shift register support. Shift register support could be for a single vector of keys and/or support the row or column being an in or out shift register. These will probably be separate classes, though they could be keyword argument variants on __init__(). The state() and keys_with_state() returns would be the same. A matrix key_num could also be expressed as (row, col), and there will be a function like key_num(row, col), to convert from row/col to key_num.

@dhalbert
Copy link
Collaborator Author

dhalbert commented Jun 12, 2021

Closing this because I have a Better Idea of how to do background scanning and represent scan results independent of scan method.

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.

6 participants