Files
MeshCore/src/helpers/ui/MomentaryButton.cpp
T

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;
}