|
| 1 | +"""Glorified button class with debounced tap, double-tap, hold and release""" |
| 2 | + |
| 3 | +# pylint: disable=import-error |
| 4 | +from time import monotonic |
| 5 | +from digitalio import DigitalInOut, Direction, Pull |
| 6 | + |
| 7 | +# pylint: disable=too-many-instance-attributes, too-few-public-methods |
| 8 | +class RichButton: |
| 9 | + """ |
| 10 | + A button class handling more than basic taps: adds debounced tap, |
| 11 | + double-tap, hold and release. |
| 12 | + """ |
| 13 | + |
| 14 | + TAP = 0 |
| 15 | + DOUBLE_TAP = 1 |
| 16 | + HOLD = 2 |
| 17 | + RELEASE = 3 |
| 18 | + |
| 19 | + def __init__(self, pin, *, debounce_period=0.05, hold_period=0.75, |
| 20 | + double_tap_period=0.3): |
| 21 | + """ |
| 22 | + Constructor for RichButton class. |
| 23 | +
|
| 24 | + Arguments: |
| 25 | + pin (int) : Digital pin connected to button |
| 26 | + (opposite leg to GND). Pin will be |
| 27 | + configured as INPUT with pullup. |
| 28 | + Keyword arguments: |
| 29 | + debounce_period (float) : interval, in seconds, in which multiple |
| 30 | + presses are ignored (debounced) |
| 31 | + (default = 0.05 seconds). |
| 32 | + hold_period (float) : interval, in seconds, when a held |
| 33 | + button will return a HOLD value from |
| 34 | + the action() function (default = 0.75). |
| 35 | + double_tap_period (float): interval, in seconds, when a double- |
| 36 | + tap can be sensed (vs returning |
| 37 | + a second single-tap) (default = 0.3). |
| 38 | + Longer double-tap periods will make |
| 39 | + single-taps less responsive. |
| 40 | + """ |
| 41 | + self.in_out = DigitalInOut(pin) |
| 42 | + self.in_out.direction = Direction.INPUT |
| 43 | + self.in_out.pull = Pull.UP |
| 44 | + self._debounce_period = debounce_period |
| 45 | + self._hold_period = hold_period |
| 46 | + self._double_tap_period = double_tap_period |
| 47 | + self._holding = False |
| 48 | + self._tap_time = -self._double_tap_period |
| 49 | + self._press_time = monotonic() |
| 50 | + self._prior_state = self.in_out.value |
| 51 | + |
| 52 | + def action(self): |
| 53 | + """ |
| 54 | + Process pin input. This MUST be called frequently for debounce, etc. |
| 55 | + to work, since interrupts are not available. |
| 56 | + Returns: |
| 57 | + None, TAP, DOUBLE_TAP, HOLD or RELEASE. |
| 58 | + """ |
| 59 | + new_state = self.in_out.value |
| 60 | + if new_state != self._prior_state: |
| 61 | + # Button state changed since last call |
| 62 | + self._prior_state = new_state |
| 63 | + if not new_state: |
| 64 | + # Button initially pressed (TAP not returned until debounce) |
| 65 | + self._press_time = monotonic() |
| 66 | + else: |
| 67 | + # Button initially released |
| 68 | + if self._holding: |
| 69 | + # Button released after hold |
| 70 | + self._holding = False |
| 71 | + return self.RELEASE |
| 72 | + if (monotonic() - self._press_time) >= self._debounce_period: |
| 73 | + # Button released after valid debounce time |
| 74 | + if monotonic() - self._tap_time < self._double_tap_period: |
| 75 | + # Followed another recent tap, reset double timer |
| 76 | + self._tap_time = 0 |
| 77 | + return self.DOUBLE_TAP |
| 78 | + # Else regular debounced release, maybe 1st tap, keep time |
| 79 | + self._tap_time = monotonic() |
| 80 | + else: |
| 81 | + # Button is in same state as last call |
| 82 | + if self._prior_state: |
| 83 | + # Is not pressed |
| 84 | + if (self._tap_time > 0 and |
| 85 | + (monotonic() - self._tap_time) > self._double_tap_period): |
| 86 | + # Enough time since last tap that it's not a double |
| 87 | + self._tap_time = 0 |
| 88 | + return self.TAP |
| 89 | + elif (not self._holding and |
| 90 | + (monotonic() - self._press_time) >= self._hold_period): |
| 91 | + # Is pressed, and has been for the holding period |
| 92 | + self._holding = True |
| 93 | + return self.HOLD |
| 94 | + return None |
0 commit comments