mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-07-26 14:58:12 +00:00
fix(eink): capture button edges via GPIO interrupt to survive display refresh
E-ink panel refreshes block the main loop for 500ms-3.6s (GxEPD2's display.display() waits on the BUSY pin), and MomentaryButton::check() only ever samples the live pin level once per loop iteration. A full press+release that lands entirely inside a refresh is never sampled, not just delayed - users on WioTracker L1 Eink were losing taps. Add an opt-in ISR path (BUTTON_USE_INTERRUPTS) that latches each edge with its own timestamp into a small ring buffer from attachInterrupt(), independent of what the main loop is doing. check() replays buffered edges through the existing transition/multi-click logic afterwards, so click-duration and multi-click-window math still uses the edge's actual capture time rather than "now". Scoped to wio-tracker-l1-eink only for now: it's the one board this was reported against and the only GxEPDDisplay-class variant that currently builds clean on main. OLED boards refresh in ~20-30ms so polling already catches every press; analog-button boards (rak4631/rak3401) can't use attachInterrupt(CHANGE) at all. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,57 @@
|
|||||||
|
|
||||||
#define MULTI_CLICK_WINDOW_MS 280
|
#define MULTI_CLICK_WINDOW_MS 280
|
||||||
|
|
||||||
|
#ifdef BUTTON_USE_INTERRUPTS
|
||||||
|
#define ISR_DEBOUNCE_MS 5
|
||||||
|
|
||||||
|
MomentaryButton* MomentaryButton::_isr_table[MomentaryButton::MAX_ISR_BUTTONS] = { nullptr };
|
||||||
|
|
||||||
|
#define DEFINE_ISR_TRAMPOLINE(n) \
|
||||||
|
void MomentaryButton::isrTrampoline##n() { if (_isr_table[n]) _isr_table[n]->isrHandler(); }
|
||||||
|
DEFINE_ISR_TRAMPOLINE(0)
|
||||||
|
DEFINE_ISR_TRAMPOLINE(1)
|
||||||
|
DEFINE_ISR_TRAMPOLINE(2)
|
||||||
|
DEFINE_ISR_TRAMPOLINE(3)
|
||||||
|
DEFINE_ISR_TRAMPOLINE(4)
|
||||||
|
DEFINE_ISR_TRAMPOLINE(5)
|
||||||
|
DEFINE_ISR_TRAMPOLINE(6)
|
||||||
|
DEFINE_ISR_TRAMPOLINE(7)
|
||||||
|
#undef DEFINE_ISR_TRAMPOLINE
|
||||||
|
|
||||||
|
// attachInterrupt() takes a plain void(*)() with no user-data slot on the
|
||||||
|
// Adafruit nRF52 and ESP32 Arduino cores, so each button needs its own
|
||||||
|
// trampoline to know which instance to dispatch to.
|
||||||
|
static void (*const ISR_TRAMPOLINES[MomentaryButton::MAX_ISR_BUTTONS])() = {
|
||||||
|
MomentaryButton::isrTrampoline0, MomentaryButton::isrTrampoline1,
|
||||||
|
MomentaryButton::isrTrampoline2, MomentaryButton::isrTrampoline3,
|
||||||
|
MomentaryButton::isrTrampoline4, MomentaryButton::isrTrampoline5,
|
||||||
|
MomentaryButton::isrTrampoline6, MomentaryButton::isrTrampoline7,
|
||||||
|
};
|
||||||
|
|
||||||
|
void MomentaryButton::pushEdge(uint8_t level, uint32_t at) {
|
||||||
|
uint8_t next = (_edge_head + 1) % EDGE_BUF_SIZE;
|
||||||
|
if (next == _edge_tail) return; // full: drop rather than clobber older, unprocessed edges
|
||||||
|
_edge_level[_edge_head] = level;
|
||||||
|
_edge_time[_edge_head] = at;
|
||||||
|
_edge_head = next;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MomentaryButton::popEdge(uint8_t &level, uint32_t &at) {
|
||||||
|
if (_edge_tail == _edge_head) return false;
|
||||||
|
level = _edge_level[_edge_tail];
|
||||||
|
at = _edge_time[_edge_tail];
|
||||||
|
_edge_tail = (_edge_tail + 1) % EDGE_BUF_SIZE;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void MomentaryButton::isrHandler() {
|
||||||
|
uint32_t now = millis();
|
||||||
|
if (now - _last_isr_edge < ISR_DEBOUNCE_MS) return; // mechanical contact-bounce guard
|
||||||
|
_last_isr_edge = now;
|
||||||
|
pushEdge(digitalRead(_pin), now);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
MomentaryButton::MomentaryButton(int8_t pin, int long_press_millis, bool reverse, bool pulldownup, bool multiclick) {
|
MomentaryButton::MomentaryButton(int8_t pin, int long_press_millis, bool reverse, bool pulldownup, bool multiclick) {
|
||||||
_pin = pin;
|
_pin = pin;
|
||||||
_reverse = reverse;
|
_reverse = reverse;
|
||||||
@@ -35,6 +86,16 @@ MomentaryButton::MomentaryButton(int8_t pin, int long_press_millis, int analog_t
|
|||||||
void MomentaryButton::begin() {
|
void MomentaryButton::begin() {
|
||||||
if (_pin >= 0 && _threshold == 0) {
|
if (_pin >= 0 && _threshold == 0) {
|
||||||
pinMode(_pin, _pull ? (_reverse ? INPUT_PULLUP : INPUT_PULLDOWN) : INPUT);
|
pinMode(_pin, _pull ? (_reverse ? INPUT_PULLUP : INPUT_PULLDOWN) : INPUT);
|
||||||
|
#ifdef BUTTON_USE_INTERRUPTS
|
||||||
|
for (uint8_t i = 0; i < MAX_ISR_BUTTONS; i++) {
|
||||||
|
if (_isr_table[i] == nullptr) {
|
||||||
|
_isr_table[i] = this;
|
||||||
|
_isr_slot = i;
|
||||||
|
attachInterrupt(digitalPinToInterrupt(_pin), ISR_TRAMPOLINES[i], CHANGE);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,36 +123,53 @@ bool MomentaryButton::isPressed(int level) const {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void MomentaryButton::applyTransition(int btn, unsigned long at) {
|
||||||
|
if (btn == prev) return;
|
||||||
|
int event = BUTTON_EVENT_NONE; // kept for parity with the original inline logic below
|
||||||
|
if (isPressed(btn)) {
|
||||||
|
down_at = at;
|
||||||
|
} else {
|
||||||
|
// button UP
|
||||||
|
if (_long_millis > 0) {
|
||||||
|
if (down_at > 0 && (unsigned long)(at - down_at) < (unsigned long)_long_millis) { // only a CLICK if still within the long_press millis
|
||||||
|
_click_count++;
|
||||||
|
_last_click_time = at;
|
||||||
|
_pending_click = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_click_count++;
|
||||||
|
_last_click_time = at;
|
||||||
|
_pending_click = true;
|
||||||
|
}
|
||||||
|
if (event == BUTTON_EVENT_CLICK && cancel) {
|
||||||
|
event = BUTTON_EVENT_NONE;
|
||||||
|
_click_count = 0;
|
||||||
|
_last_click_time = 0;
|
||||||
|
_pending_click = false;
|
||||||
|
}
|
||||||
|
down_at = 0;
|
||||||
|
}
|
||||||
|
prev = btn;
|
||||||
|
}
|
||||||
|
|
||||||
int MomentaryButton::check(bool repeat_click) {
|
int MomentaryButton::check(bool repeat_click) {
|
||||||
if (_pin < 0) return BUTTON_EVENT_NONE;
|
if (_pin < 0) return BUTTON_EVENT_NONE;
|
||||||
|
|
||||||
int event = BUTTON_EVENT_NONE;
|
int event = BUTTON_EVENT_NONE;
|
||||||
int btn = _threshold > 0 ? (analogRead(_pin) < _threshold) : digitalRead(_pin);
|
int btn;
|
||||||
if (btn != prev) {
|
#ifdef BUTTON_USE_INTERRUPTS
|
||||||
if (isPressed(btn)) {
|
if (_isr_slot >= 0) {
|
||||||
down_at = millis();
|
// Replay edges the ISR caught while the main loop was off doing something
|
||||||
} else {
|
// blocking (e-ink refresh) — each keeps its own capture timestamp so
|
||||||
// button UP
|
// click-duration / multi-click-window math still lines up correctly.
|
||||||
if (_long_millis > 0) {
|
uint8_t lvl; uint32_t at;
|
||||||
if (down_at > 0 && (unsigned long)(millis() - down_at) < _long_millis) { // only a CLICK if still within the long_press millis
|
while (popEdge(lvl, at)) applyTransition(lvl, at);
|
||||||
_click_count++;
|
btn = prev; // edges already caught us up to the live level
|
||||||
_last_click_time = millis();
|
} else
|
||||||
_pending_click = true;
|
#endif
|
||||||
}
|
{
|
||||||
} else {
|
btn = _threshold > 0 ? (analogRead(_pin) < _threshold) : digitalRead(_pin);
|
||||||
_click_count++;
|
applyTransition(btn, millis());
|
||||||
_last_click_time = millis();
|
|
||||||
_pending_click = true;
|
|
||||||
}
|
|
||||||
if (event == BUTTON_EVENT_CLICK && cancel) {
|
|
||||||
event = BUTTON_EVENT_NONE;
|
|
||||||
_click_count = 0;
|
|
||||||
_last_click_time = 0;
|
|
||||||
_pending_click = false;
|
|
||||||
}
|
|
||||||
down_at = 0;
|
|
||||||
}
|
|
||||||
prev = btn;
|
|
||||||
}
|
}
|
||||||
if (!isPressed(btn) && cancel) { // always clear the pending 'cancel' once button is back in UP state
|
if (!isPressed(btn) && cancel) { // always clear the pending 'cancel' once button is back in UP state
|
||||||
cancel = 0;
|
cancel = 0;
|
||||||
|
|||||||
@@ -21,6 +21,41 @@ class MomentaryButton {
|
|||||||
bool _pending_click;
|
bool _pending_click;
|
||||||
|
|
||||||
bool isPressed(int level) const;
|
bool isPressed(int level) const;
|
||||||
|
void applyTransition(int btn, unsigned long at);
|
||||||
|
|
||||||
|
#ifdef BUTTON_USE_INTERRUPTS
|
||||||
|
// GPIO-IRQ edge capture: boards whose display blocks the main loop for a
|
||||||
|
// long time per refresh (e-ink) can miss an entire press+release cycle if
|
||||||
|
// check() only ever samples the live pin. The ISR latches edges with their
|
||||||
|
// own timestamp into a small ring buffer; check() replays them so no tap
|
||||||
|
// gets lost while the loop is stuck waiting on the panel.
|
||||||
|
int8_t _isr_slot = -1;
|
||||||
|
static const uint8_t EDGE_BUF_SIZE = 16;
|
||||||
|
volatile uint8_t _edge_level[EDGE_BUF_SIZE];
|
||||||
|
volatile uint32_t _edge_time[EDGE_BUF_SIZE];
|
||||||
|
volatile uint8_t _edge_head = 0, _edge_tail = 0;
|
||||||
|
volatile uint32_t _last_isr_edge = 0;
|
||||||
|
|
||||||
|
void pushEdge(uint8_t level, uint32_t at);
|
||||||
|
bool popEdge(uint8_t &level, uint32_t &at);
|
||||||
|
void isrHandler();
|
||||||
|
static MomentaryButton* _isr_table[8];
|
||||||
|
|
||||||
|
public:
|
||||||
|
// MAX_ISR_BUTTONS/isrTrampolineN must be public: attachInterrupt() needs a
|
||||||
|
// plain free-function pointer, taken from a file-scope table outside the
|
||||||
|
// class (see MomentaryButton.cpp), which needs both to size and fill itself.
|
||||||
|
static const uint8_t MAX_ISR_BUTTONS = 8;
|
||||||
|
static void isrTrampoline0();
|
||||||
|
static void isrTrampoline1();
|
||||||
|
static void isrTrampoline2();
|
||||||
|
static void isrTrampoline3();
|
||||||
|
static void isrTrampoline4();
|
||||||
|
static void isrTrampoline5();
|
||||||
|
static void isrTrampoline6();
|
||||||
|
static void isrTrampoline7();
|
||||||
|
private:
|
||||||
|
#endif
|
||||||
|
|
||||||
public:
|
public:
|
||||||
MomentaryButton(int8_t pin, int long_press_mills=0, bool reverse=false, bool pulldownup=false, bool multiclick=true);
|
MomentaryButton(int8_t pin, int long_press_mills=0, bool reverse=false, bool pulldownup=false, bool multiclick=true);
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ build_flags = ${nrf52_base.build_flags}
|
|||||||
-D MAX_GROUP_CHANNELS=40
|
-D MAX_GROUP_CHANNELS=40
|
||||||
-D OFFLINE_QUEUE_SIZE=256
|
-D OFFLINE_QUEUE_SIZE=256
|
||||||
-D DISPLAY_CLASS=GxEPDDisplay
|
-D DISPLAY_CLASS=GxEPDDisplay
|
||||||
|
-D BUTTON_USE_INTERRUPTS=1
|
||||||
-D UI_HAS_JOYSTICK=1
|
-D UI_HAS_JOYSTICK=1
|
||||||
-D UI_HAS_JOYSTICK_UPDOWN=1
|
-D UI_HAS_JOYSTICK_UPDOWN=1
|
||||||
-D PIN_BUZZER=12
|
-D PIN_BUZZER=12
|
||||||
|
|||||||
Reference in New Issue
Block a user