Commit Graph
12 Commits
Author SHA1 Message Date
MarekZegare4andClaude Opus 4.8 f31596da1a fix(eink): debounce edge capture + self-heal so one press isn't a double-tap
A single button press could surface as two CLICKs on the e-ink build, most
visibly as start+stop on the stopwatch. Two contact-bounce paths fed the
IRQ edge-capture machinery a phantom press/release pair:

- a bounce edge accepted just after a clean release was replayed as a real
  press — ISR_DEBOUNCE_MS (5 ms) was too short for the joystick switch; raised
  to 25 ms so the settling burst is swallowed.
- the live-pin self-heal reconciled prev against a single raw digitalRead,
  which can sample a bouncing contact mid-flap and synthesise a transition.
  It now acts only once the divergence has been stable for ISR_DEBOUNCE_MS, so
  a momentary read can't inject a click; a genuinely lost edge still heals
  (~25 ms later) so the button can't stick.

Both thresholds stay far below any human tap cadence (>100 ms), so rapid
multi-tap navigation still registers every tap.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 22:40:44 +02:00
MarekZegare4andClaude Opus 4.8 8c70acc3ef 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>
2026-06-30 22:10:18 +02:00
MarekZegare4andClaude Opus 4.8 57ebd80fa7 fix(eink): fall back to polling when no GPIOTE channel is free
begin() claimed the trampoline slot and set _isr_slot before calling
attachInterrupt(), so a button that lost the GPIOTE race (only 8 channels,
shared with the radio's DIO1) would take the ISR branch forever while its
interrupt never fired. attachInterrupt() returns 0 when channels are
exhausted — only commit the slot on success, else leave _isr_slot = -1 so
check() uses the polling path. Combined with the live-pin reconcile, an
exhausted button degrades cleanly to plain polling instead of misbehaving.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 12:21:30 +02:00
MarekZegare4andClaude Opus 4.8 d8c5fa83de fix(eink): self-heal button state against the live pin; drop dead code
The interrupt path trusted the replayed edge stream alone (btn = prev) and
never sampled the live pin. Any lost edge — buffer overflow, a debounce-
dropped settling edge, or a missed GPIOTE event — left prev diverged from
the hardware until the next captured edge, i.e. a stuck button. After
draining the queue, reconcile against digitalRead(): a no-op when the edge
stream already matches, a one-call self-heal when it doesn't.

Also remove dead code in applyTransition() (a local `event` that was always
BUTTON_EVENT_NONE, making its cancel-on-click branch unreachable) carried
over verbatim from the original inline logic. No behaviour change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 11:53:23 +02:00
MarekZegare4andClaude Sonnet 4.6 64a48f2aea 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>
2026-06-25 23:56:08 +02:00
Scott Powell cd920693ec * UITask: new UI_HAS_JOYSTICK
* MomentaryButton: new constructor 'multiclick' param
* WIoTrackerL1: now just use joystick, joystick press for KEY_ENTER, no multi-click for snappier UI
2025-10-16 17:33:22 +11:00
taco 9f185303b4 long press cancels multi click 2025-09-03 12:29:20 +10:00
taco 5de0dc1fd6 sliding multiclick window 2025-09-03 12:03:31 +10:00
taco ce31fd7c57 multi click support including buzzer toggle 2025-09-03 08:25:59 +10:00
Scott Powell 74722c24b8 * MomentaryButton: added support for analog button (with threshold)
* RAK: support for PIN_USER_BTN_ANA
2025-09-01 17:11:55 +10:00
Scott Powell e84e3066ff * MomentaryButton: pullupdown param moved to constructor 2025-08-12 10:01:35 +10:00
Scott Powell 4b95c981bb * UI revamp for companion radios 2025-08-08 20:01:31 +10:00