224 lines
6.8 KiB
C++
224 lines
6.8 KiB
C++
#include "MomentaryButton.h"
|
|
|
|
#define BUTTON_DEBOUNCE_MS 25
|
|
|
|
#if defined(NRF52_PLATFORM) && \
|
|
defined(MOMENTARY_BUTTON_WAKE_FROM_SLEEP) && \
|
|
MOMENTARY_BUTTON_WAKE_FROM_SLEEP
|
|
namespace {
|
|
|
|
// nRF52 companion builds use sd_app_evt_wait() between loop iterations. The
|
|
// button state machine remains intentionally polling-based, but it still needs
|
|
// a GPIO event to wake that poller. Both edges matter: a falling edge records
|
|
// button-down and a rising edge records release. The companion loop stays
|
|
// awake while needsPolling() reports a gesture deadline so long/multi-click
|
|
// timing can complete without another GPIO edge.
|
|
void wakeMomentaryButtonPoller() {}
|
|
|
|
} // namespace
|
|
#endif
|
|
|
|
MomentaryButton::MomentaryButton(int8_t pin, int long_press_millis, bool reverse, bool pulldownup, bool multiclick) {
|
|
_pin = pin;
|
|
_reverse = reverse;
|
|
_pull = pulldownup;
|
|
down_at = 0;
|
|
prev = _reverse ? HIGH : LOW;
|
|
cancel = 0;
|
|
_long_millis = long_press_millis;
|
|
_threshold = 0;
|
|
_press_active = false;
|
|
_click_count = 0;
|
|
_last_click_time = 0;
|
|
_multi_click_window = multiclick ? MOMENTARY_BUTTON_MULTI_CLICK_MS : 0;
|
|
_pending_click = false;
|
|
_candidate_level = prev;
|
|
_candidate_since = 0;
|
|
_debouncing = false;
|
|
}
|
|
|
|
MomentaryButton::MomentaryButton(int8_t pin, int long_press_millis, int analog_threshold) {
|
|
_pin = pin;
|
|
_reverse = false;
|
|
_pull = false;
|
|
down_at = 0;
|
|
prev = LOW;
|
|
cancel = 0;
|
|
_long_millis = long_press_millis;
|
|
_threshold = analog_threshold;
|
|
_press_active = false;
|
|
_click_count = 0;
|
|
_last_click_time = 0;
|
|
_multi_click_window = MOMENTARY_BUTTON_MULTI_CLICK_MS;
|
|
_pending_click = false;
|
|
_candidate_level = prev;
|
|
_candidate_since = 0;
|
|
_debouncing = false;
|
|
}
|
|
|
|
void MomentaryButton::begin() {
|
|
if (_pin >= 0 && _threshold == 0) {
|
|
pinMode(_pin, _pull ? (_reverse ? INPUT_PULLUP : INPUT_PULLDOWN) : INPUT);
|
|
#if defined(NRF52_PLATFORM) && \
|
|
defined(MOMENTARY_BUTTON_WAKE_FROM_SLEEP) && \
|
|
MOMENTARY_BUTTON_WAKE_FROM_SLEEP
|
|
// The ISR deliberately owns no button logic; it only makes a sleeping
|
|
// event-driven main loop run the normal debounced polling state machine.
|
|
attachInterrupt((uint32_t)_pin, wakeMomentaryButtonPoller, CHANGE);
|
|
#endif
|
|
}
|
|
}
|
|
|
|
bool MomentaryButton::isPressed() const {
|
|
int btn = _threshold > 0 ? (analogRead(_pin) < _threshold) : digitalRead(_pin);
|
|
return isPressed(btn);
|
|
}
|
|
|
|
void MomentaryButton::cancelClick() {
|
|
cancel = 1;
|
|
down_at = 0;
|
|
_press_active = false;
|
|
_click_count = 0;
|
|
_last_click_time = 0;
|
|
_pending_click = false;
|
|
}
|
|
|
|
bool MomentaryButton::isPressed(int level) const {
|
|
if (_threshold > 0) {
|
|
return level;
|
|
}
|
|
if (_reverse) {
|
|
return level == LOW;
|
|
} else {
|
|
return level != LOW;
|
|
}
|
|
}
|
|
|
|
int MomentaryButton::check(bool repeat_click) {
|
|
if (_pin < 0) return BUTTON_EVENT_NONE;
|
|
|
|
int event = BUTTON_EVENT_NONE;
|
|
const uint32_t now = millis();
|
|
const int raw_btn =
|
|
_threshold > 0 ? (analogRead(_pin) < _threshold) : digitalRead(_pin);
|
|
|
|
#if MOMENTARY_BUTTON_WAKE_HOLD_MS > 0
|
|
// Keep the main loop awake after button activity, including when a press
|
|
// starts at millis zero or spans rollover. Holding/repressing extends it.
|
|
if (isPressed(raw_btn)) {
|
|
_last_pressed_at = now;
|
|
_wake_hold_active = true;
|
|
} else if (_wake_hold_active && !isWakeHoldActive()) {
|
|
_wake_hold_active = false;
|
|
}
|
|
#endif
|
|
|
|
// Treat prev as the stable electrical level. A raw transition must remain
|
|
// unchanged for a short interval before it can alter gesture state. Without
|
|
// this, one physical release can be counted two or three times and a short
|
|
// click can silently turn into KEY_PREV or a triple-click action.
|
|
int btn = prev;
|
|
if (raw_btn == prev) {
|
|
_debouncing = false;
|
|
} else if (!_debouncing || raw_btn != _candidate_level) {
|
|
_candidate_level = raw_btn;
|
|
_candidate_since = now;
|
|
_debouncing = true;
|
|
} else if ((uint32_t)(now - _candidate_since) >= BUTTON_DEBOUNCE_MS) {
|
|
btn = raw_btn;
|
|
_debouncing = false;
|
|
}
|
|
|
|
if (btn != prev) {
|
|
if (isPressed(btn)) {
|
|
down_at = now;
|
|
_press_active = true;
|
|
} else {
|
|
// button UP
|
|
if (!cancel && _press_active && _long_millis > 0) {
|
|
if ((uint32_t)(now - down_at) >= (uint32_t)_long_millis) {
|
|
// Normally the polling loop reports this while the button is still
|
|
// down. Also handle release-only polling so event-driven platforms
|
|
// cannot lose a long press if no intermediate poll occurred.
|
|
event = BUTTON_EVENT_LONG_PRESS;
|
|
_click_count = 0;
|
|
_last_click_time = 0;
|
|
_pending_click = false;
|
|
} else {
|
|
_click_count++;
|
|
_last_click_time = now;
|
|
_pending_click = true;
|
|
}
|
|
} else if (!cancel && _press_active) {
|
|
_click_count++;
|
|
_last_click_time = now;
|
|
_pending_click = true;
|
|
}
|
|
_press_active = false;
|
|
down_at = 0;
|
|
}
|
|
prev = btn;
|
|
}
|
|
if (!isPressed(btn) && cancel) { // always clear the pending 'cancel' once button is back in UP state
|
|
cancel = 0;
|
|
}
|
|
|
|
if (_long_millis > 0 && _press_active
|
|
&& (uint32_t)(now - down_at) >= (uint32_t)_long_millis) {
|
|
if (_pending_click) {
|
|
// long press during multi-click detection - cancel pending clicks
|
|
cancelClick();
|
|
} else {
|
|
event = BUTTON_EVENT_LONG_PRESS;
|
|
_press_active = false;
|
|
down_at = 0;
|
|
_click_count = 0;
|
|
_last_click_time = 0;
|
|
_pending_click = false;
|
|
}
|
|
}
|
|
if (_press_active && repeat_click) {
|
|
uint32_t diff = (uint32_t)(now - down_at);
|
|
if (diff >= 700) {
|
|
event = BUTTON_EVENT_CLICK; // wait 700 millis before repeating the click events
|
|
}
|
|
}
|
|
|
|
if (_pending_click
|
|
&& (uint32_t)(now - _last_click_time) >= (uint32_t)_multi_click_window) {
|
|
// A press candidate first observed before the deadline may legitimately
|
|
// become the next click even if its debounce interval crosses the window.
|
|
// A candidate first observed at/after the deadline must not revive and
|
|
// merge an already-expired sequence.
|
|
const bool press_candidate_in_window = _debouncing
|
|
&& isPressed(_candidate_level)
|
|
&& (uint32_t)(_candidate_since - _last_click_time)
|
|
< (uint32_t)_multi_click_window;
|
|
if (press_candidate_in_window || _press_active) {
|
|
// still pressed - wait for button release before processing clicks
|
|
return event;
|
|
}
|
|
switch (_click_count) {
|
|
case 1:
|
|
event = BUTTON_EVENT_CLICK;
|
|
break;
|
|
case 2:
|
|
event = BUTTON_EVENT_DOUBLE_CLICK;
|
|
break;
|
|
case 3:
|
|
event = BUTTON_EVENT_TRIPLE_CLICK;
|
|
break;
|
|
default:
|
|
// Screens with group navigation opt into a distinct four-tap event.
|
|
event = _quadruple_click ? BUTTON_EVENT_QUADRUPLE_CLICK
|
|
: BUTTON_EVENT_TRIPLE_CLICK;
|
|
break;
|
|
}
|
|
_click_count = 0;
|
|
_last_click_time = 0;
|
|
_pending_click = false;
|
|
}
|
|
|
|
return event;
|
|
}
|