mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-08-03 10:46:12 +00:00
fix(eink): queue input so a burst of taps survives a blocking refresh
The joystick directions and Back were never begin()'d, so on the e-ink build they never claimed a GPIOTE channel — edges landing during a blocking panel refresh were lost and the IRQ-capture work didn't reach them. begin() them. Even with edges captured, rapid taps of one direction replayed into a single check() and collapsed into a double/triple-click event the navigation handler ignores, and the loop only ever dispatched one key per render. So: - MomentaryButton: for multiclick=false buttons emit one CLICK per completed release (one per check() call) instead of collapsing — each tap stays a discrete key. multiclick=true buttons (double/triple) are unchanged. - UITask: add a key FIFO; drain each direction fully into it, apply the whole queued burst, then redraw once. N taps captured during a refresh become N navigation steps at the cost of a single refresh. Also fixes losing a key when two buttons fire in the same loop iteration. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
57ebd80fa7
commit
8c70acc3ef
@@ -1395,6 +1395,21 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no
|
|||||||
#if defined(PIN_USER_BTN)
|
#if defined(PIN_USER_BTN)
|
||||||
user_btn.begin();
|
user_btn.begin();
|
||||||
#endif
|
#endif
|
||||||
|
#if UI_HAS_JOYSTICK
|
||||||
|
// The directional joystick + Back share the same MomentaryButton machinery as
|
||||||
|
// user_btn but were never begin()'d — they only worked because the pins
|
||||||
|
// default to INPUT and the board has external pulls. That left them on the
|
||||||
|
// polling path: with BUTTON_USE_INTERRUPTS (e-ink) they'd silently never
|
||||||
|
// attach a GPIOTE channel, so edges landing during a blocking panel refresh
|
||||||
|
// were lost. begin() sets pinMode and claims an IRQ slot for each.
|
||||||
|
joystick_left.begin();
|
||||||
|
joystick_right.begin();
|
||||||
|
back_btn.begin();
|
||||||
|
#if UI_HAS_JOYSTICK_UPDOWN
|
||||||
|
joystick_up.begin();
|
||||||
|
joystick_down.begin();
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
#if defined(PIN_USER_BTN_ANA)
|
#if defined(PIN_USER_BTN_ANA)
|
||||||
analog_btn.begin();
|
analog_btn.begin();
|
||||||
#endif
|
#endif
|
||||||
@@ -1877,8 +1892,22 @@ static void formatDashVal(uint8_t field, char* val, int val_len, uint16_t batt_m
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void UITask::enqueueKey(char c) {
|
||||||
|
if (c == 0) return;
|
||||||
|
uint8_t next = (_kq_head + 1) % KEY_QUEUE_SIZE;
|
||||||
|
if (next == _kq_tail) return; // full: drop newest rather than clobber unprocessed keys
|
||||||
|
_key_queue[_kq_head] = c;
|
||||||
|
_kq_head = next;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UITask::dequeueKey(char& c) {
|
||||||
|
if (_kq_tail == _kq_head) return false;
|
||||||
|
c = _key_queue[_kq_tail];
|
||||||
|
_kq_tail = (_kq_tail + 1) % KEY_QUEUE_SIZE;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
void UITask::loop() {
|
void UITask::loop() {
|
||||||
char c = 0;
|
|
||||||
// Background delivery: resend pending on-device DMs whose ACK timed out, and
|
// Background delivery: resend pending on-device DMs whose ACK timed out, and
|
||||||
// finalise the ✗ marker — runs regardless of which screen is active.
|
// finalise the ✗ marker — runs regardless of which screen is active.
|
||||||
((QuickMsgScreen*)quick_msg)->tickDmResends();
|
((QuickMsgScreen*)quick_msg)->tickDmResends();
|
||||||
@@ -1910,32 +1939,27 @@ void UITask::loop() {
|
|||||||
}
|
}
|
||||||
// eat the Enter — don't pass to curr
|
// eat the Enter — don't pass to curr
|
||||||
} else {
|
} else {
|
||||||
c = checkDisplayOn(KEY_ENTER);
|
enqueueKey(checkDisplayOn(KEY_ENTER));
|
||||||
}
|
}
|
||||||
} else if (ev == BUTTON_EVENT_LONG_PRESS) {
|
} else if (ev == BUTTON_EVENT_LONG_PRESS) {
|
||||||
c = handleLongPress(KEY_ENTER); // REVISIT: could be mapped to different key code
|
enqueueKey(handleLongPress(KEY_ENTER)); // REVISIT: could be mapped to different key code
|
||||||
}
|
}
|
||||||
|
// Drain each direction fully: a burst of taps captured during a blocking
|
||||||
|
// refresh replays as several CLICKs, queued here and applied before one
|
||||||
|
// redraw (see enqueueKey / the dispatch at the end of loop()).
|
||||||
#if UI_HAS_JOYSTICK_UPDOWN
|
#if UI_HAS_JOYSTICK_UPDOWN
|
||||||
ev = joystick_up.check();
|
while (joystick_up.check() == BUTTON_EVENT_CLICK)
|
||||||
if (ev == BUTTON_EVENT_CLICK) {
|
enqueueKey(checkDisplayOn(rotateJoystickKey(KEY_UP, joy_rot)));
|
||||||
c = checkDisplayOn(rotateJoystickKey(KEY_UP, joy_rot));
|
while (joystick_down.check() == BUTTON_EVENT_CLICK)
|
||||||
}
|
enqueueKey(checkDisplayOn(rotateJoystickKey(KEY_DOWN, joy_rot)));
|
||||||
ev = joystick_down.check();
|
|
||||||
if (ev == BUTTON_EVENT_CLICK) {
|
|
||||||
c = checkDisplayOn(rotateJoystickKey(KEY_DOWN, joy_rot));
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
ev = joystick_left.check();
|
while ((ev = joystick_left.check()) != BUTTON_EVENT_NONE) {
|
||||||
if (ev == BUTTON_EVENT_CLICK) {
|
if (ev == BUTTON_EVENT_CLICK) enqueueKey(checkDisplayOn(rotateJoystickKey(KEY_LEFT, joy_rot)));
|
||||||
c = checkDisplayOn(rotateJoystickKey(KEY_LEFT, joy_rot));
|
else { if (ev == BUTTON_EVENT_LONG_PRESS) enqueueKey(handleLongPress(rotateJoystickKey(KEY_LEFT, joy_rot))); break; }
|
||||||
} else if (ev == BUTTON_EVENT_LONG_PRESS) {
|
|
||||||
c = handleLongPress(rotateJoystickKey(KEY_LEFT, joy_rot));
|
|
||||||
}
|
}
|
||||||
ev = joystick_right.check();
|
while ((ev = joystick_right.check()) != BUTTON_EVENT_NONE) {
|
||||||
if (ev == BUTTON_EVENT_CLICK) {
|
if (ev == BUTTON_EVENT_CLICK) enqueueKey(checkDisplayOn(rotateJoystickKey(KEY_RIGHT, joy_rot)));
|
||||||
c = checkDisplayOn(rotateJoystickKey(KEY_RIGHT, joy_rot));
|
else { if (ev == BUTTON_EVENT_LONG_PRESS) enqueueKey(handleLongPress(rotateJoystickKey(KEY_RIGHT, joy_rot))); break; }
|
||||||
} else if (ev == BUTTON_EVENT_LONG_PRESS) {
|
|
||||||
c = handleLongPress(rotateJoystickKey(KEY_RIGHT, joy_rot));
|
|
||||||
}
|
}
|
||||||
if (_lock_seq_used && millis() - _lock_seq_ms > 5000) {
|
if (_lock_seq_used && millis() - _lock_seq_ms > 5000) {
|
||||||
_lock_seq_used = false; // safety reset if Back release event was missed
|
_lock_seq_used = false; // safety reset if Back release event was missed
|
||||||
@@ -1947,34 +1971,34 @@ void UITask::loop() {
|
|||||||
_lock_seq_count = 0;
|
_lock_seq_count = 0;
|
||||||
_lock_seq_used = false;
|
_lock_seq_used = false;
|
||||||
} else {
|
} else {
|
||||||
c = checkDisplayOn(KEY_CANCEL);
|
enqueueKey(checkDisplayOn(KEY_CANCEL));
|
||||||
}
|
}
|
||||||
} else if (ev == BUTTON_EVENT_TRIPLE_CLICK) {
|
} else if (ev == BUTTON_EVENT_TRIPLE_CLICK) {
|
||||||
if (!_locked) c = handleTripleClick(KEY_SELECT);
|
if (!_locked) enqueueKey(handleTripleClick(KEY_SELECT));
|
||||||
}
|
}
|
||||||
#elif defined(PIN_USER_BTN)
|
#elif defined(PIN_USER_BTN)
|
||||||
int ev = user_btn.check();
|
int ev = user_btn.check();
|
||||||
if (ev == BUTTON_EVENT_CLICK) {
|
if (ev == BUTTON_EVENT_CLICK) {
|
||||||
c = checkDisplayOn(KEY_NEXT);
|
enqueueKey(checkDisplayOn(KEY_NEXT));
|
||||||
} else if (ev == BUTTON_EVENT_LONG_PRESS) {
|
} else if (ev == BUTTON_EVENT_LONG_PRESS) {
|
||||||
c = handleLongPress(KEY_ENTER);
|
enqueueKey(handleLongPress(KEY_ENTER));
|
||||||
} else if (ev == BUTTON_EVENT_DOUBLE_CLICK) {
|
} else if (ev == BUTTON_EVENT_DOUBLE_CLICK) {
|
||||||
c = handleDoubleClick(KEY_PREV);
|
enqueueKey(handleDoubleClick(KEY_PREV));
|
||||||
} else if (ev == BUTTON_EVENT_TRIPLE_CLICK) {
|
} else if (ev == BUTTON_EVENT_TRIPLE_CLICK) {
|
||||||
c = handleTripleClick(KEY_SELECT);
|
enqueueKey(handleTripleClick(KEY_SELECT));
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
#if defined(PIN_USER_BTN_ANA)
|
#if defined(PIN_USER_BTN_ANA)
|
||||||
if (millis() - _analogue_pin_read_millis > 10) {
|
if (millis() - _analogue_pin_read_millis > 10) {
|
||||||
int ev = analog_btn.check();
|
int ev = analog_btn.check();
|
||||||
if (ev == BUTTON_EVENT_CLICK) {
|
if (ev == BUTTON_EVENT_CLICK) {
|
||||||
c = checkDisplayOn(KEY_NEXT);
|
enqueueKey(checkDisplayOn(KEY_NEXT));
|
||||||
} else if (ev == BUTTON_EVENT_LONG_PRESS) {
|
} else if (ev == BUTTON_EVENT_LONG_PRESS) {
|
||||||
c = handleLongPress(KEY_ENTER);
|
enqueueKey(handleLongPress(KEY_ENTER));
|
||||||
} else if (ev == BUTTON_EVENT_DOUBLE_CLICK) {
|
} else if (ev == BUTTON_EVENT_DOUBLE_CLICK) {
|
||||||
c = handleDoubleClick(KEY_PREV);
|
enqueueKey(handleDoubleClick(KEY_PREV));
|
||||||
} else if (ev == BUTTON_EVENT_TRIPLE_CLICK) {
|
} else if (ev == BUTTON_EVENT_TRIPLE_CLICK) {
|
||||||
c = handleTripleClick(KEY_SELECT);
|
enqueueKey(handleTripleClick(KEY_SELECT));
|
||||||
}
|
}
|
||||||
_analogue_pin_read_millis = millis();
|
_analogue_pin_read_millis = millis();
|
||||||
}
|
}
|
||||||
@@ -1991,17 +2015,21 @@ void UITask::loop() {
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
if (c != 0) {
|
if (_kq_head != _kq_tail) {
|
||||||
if (!_locked && curr) {
|
if (!_locked && curr) {
|
||||||
curr->handleInput(c);
|
// Apply the whole queued burst, then redraw once — N taps captured during
|
||||||
|
// a blocking refresh become N navigation steps at the cost of one refresh.
|
||||||
|
char k;
|
||||||
|
while (dequeueKey(k)) curr->handleInput(k);
|
||||||
{ uint32_t aoff = autoOffMillis(); if (aoff > 0) _auto_off = millis() + aoff; } // extend auto-off timer
|
{ uint32_t aoff = autoOffMillis(); if (aoff > 0) _auto_off = millis() + aoff; } // extend auto-off timer
|
||||||
// Note timing no longer depends on render cadence (TIMER1 IRQ advances
|
// Note timing no longer depends on render cadence (TIMER1 IRQ advances
|
||||||
// notes directly — see buzzer.cpp), so a redraw right after a keypress
|
// notes directly — see buzzer.cpp), so a redraw right after a keypress
|
||||||
// can't clip a note; no need to hold it back while buzzer.isPlaying().
|
// can't clip a note; no need to hold it back while buzzer.isPlaying().
|
||||||
_next_refresh = 100; // trigger refresh immediately
|
_next_refresh = 100; // trigger refresh immediately
|
||||||
} else if (_locked) {
|
} else {
|
||||||
// Locked: eat all keys — wake window is set only when display first turns on
|
_kq_head = _kq_tail = 0; // locked or no screen: eat all queued keys
|
||||||
_next_refresh = 0;
|
// Locked: wake window is set only when display first turns on
|
||||||
|
if (_locked) _next_refresh = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -149,6 +149,17 @@ class UITask : public AbstractUITask {
|
|||||||
char handleDoubleClick(char c);
|
char handleDoubleClick(char c);
|
||||||
char handleTripleClick(char c);
|
char handleTripleClick(char c);
|
||||||
|
|
||||||
|
// Key FIFO: a burst of taps captured during a blocking refresh (e-ink) is
|
||||||
|
// drained from the buttons into this queue, then all keys are applied before
|
||||||
|
// a single redraw — so rapid navigation steps neither get lost nor cost one
|
||||||
|
// slow refresh each. Also fixes losing a key when two buttons fire in the
|
||||||
|
// same loop iteration.
|
||||||
|
static const uint8_t KEY_QUEUE_SIZE = 16;
|
||||||
|
char _key_queue[KEY_QUEUE_SIZE];
|
||||||
|
uint8_t _kq_head = 0, _kq_tail = 0;
|
||||||
|
void enqueueKey(char c);
|
||||||
|
bool dequeueKey(char& c);
|
||||||
|
|
||||||
void setCurrScreen(UIScreen* c);
|
void setCurrScreen(UIScreen* c);
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
|||||||
@@ -202,7 +202,22 @@ int MomentaryButton::check(bool repeat_click) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_pending_click && (millis() - _last_click_time) >= _multi_click_window) {
|
if (_multi_click_window == 0) {
|
||||||
|
// No multi-click semantics: every completed press is an independent CLICK.
|
||||||
|
// When a burst of taps is captured during a blocking refresh (e-ink), all
|
||||||
|
// their edges replay into _click_count in a single check(); emit them one
|
||||||
|
// CLICK per call (decrementing) instead of collapsing into a lone
|
||||||
|
// double/triple event the caller would ignore — so each tap survives as a
|
||||||
|
// discrete key. Waits for release (down_at == 0) so a held button doesn't
|
||||||
|
// fire mid-press.
|
||||||
|
if (_pending_click && down_at == 0) {
|
||||||
|
event = BUTTON_EVENT_CLICK;
|
||||||
|
if (--_click_count == 0) {
|
||||||
|
_last_click_time = 0;
|
||||||
|
_pending_click = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (_pending_click && (millis() - _last_click_time) >= _multi_click_window) {
|
||||||
if (down_at > 0) {
|
if (down_at > 0) {
|
||||||
// still pressed - wait for button release before processing clicks
|
// still pressed - wait for button release before processing clicks
|
||||||
return event;
|
return event;
|
||||||
|
|||||||
Reference in New Issue
Block a user