Skip to content

Fix missed Xteink navigation during rapid taps and redraws - #446

Merged
bmorcelli merged 6 commits into
bmorcelli:mainfrom
LittleBidet:feat/xteink-rapid-button-input
Oct 15, 2026
Merged

bmorcelli merged 6 commits into
bmorcelli:mainfrom
LittleBidet:feat/xteink-rapid-button-input

Conversation

@LittleBidet

@LittleBidet LittleBidet commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Quick Xteink X3/X4 navigation taps can be missed for two reasons: the board handler stopped sampling for 200 ms after any press, and the shared input task wiped every unread flag after about 75 ms, even while loopTask was blocked in an e-paper panel refresh. A tap landing in that window was raised, wiped, and never seen — the "second press does nothing" report on the X3.

Board handler (boards/xteink-x3-x4/interface.cpp). Read both ladders and the power button on every poll. A rung counts once two consecutive samples agree: releasing Right or Down sweeps the ADC node up through the other bands on its way to idle, and a single transient sample must not fire a stray Prev/Sel/Esc/Up, nor end a hold. A stable idle rearms the ladder; a stable different button is a new press. Each ladder repeats independently at the existing 200 ms interval; LongPress bypasses the gate as before. Costs one 10 ms sample of latency on the first press.

Shared input task (src/main.cpp, E_PAPER_DISPLAY only). While any of the six button flags is unread and LongPress is not set, skip the per-cycle wipe of those flags; touch and keystroke state are still cleared each cycle. Pending is judged on the flags themselves, not AnyKeyPress, because check() drops AnyKeyPress on the first flag it consumes and the others have to survive that. Only one unread event is held at a time: the flags carry no order, so a press stacked on an unread one would be applied in whatever order the UI polls them (move-then-confirm when the user did confirm-then-move). While something is pending, whatever a poll newly raises is cleared, as every poll did before; a flag the handler cleared is never restored, so a press check() consumed meanwhile cannot be put back. Bounded at 1500 ms so an event nobody reads cannot ghost into a later screen. Handlers that assign false while idle (hal_buttons_poll_3, T-Deck Pro) still drop their flags, so those boards keep their previous behaviour. Not while LongPress: display.cpp, mykeyboard.cpp and launcherSelectHeld() poll the raw flags and need them to drop on release. LCD boards are unchanged.

Main menu. On e-paper boards, consume Back/Escape as a no-op on the main menu. Otherwise its unread flag occupies the retained event slot and discards navigation or Confirm presses for up to 1500 ms.

Board handler, measured. A hold repeats only after 400 ms (then every 200 ms), and only for Left/Right/Up/Down. Confirm and Back fire once per press; their 200 ms repeat used to be wiped before the UI saw it, but with retention a half-second Confirm produced a second Sel that landed in the menu the first one opened. Ordinary taps measured up to ~240 ms, so a 200 ms first repeat double-stepped 2 of 20 taps. Main menu consumes an unread Escape so it cannot block navigation for the retention window.

Validation. pio run -e xteink-x3-x4 passes (application image 1,376,688 bytes); seeedstudio-reterminal-e1001 and lilygo-t-deck-pro also build. A temporary host harness mirroring the task-loop block covered retention across 75 ms polls, a second flag surviving the first check(), confirm-then-move dropping the move, a check() racing InputHandler() not restoring the consumed press, the 1500 ms cap, LongPress live state, idle-assigning handlers, and timer wrap.

On an X3 (UC8253 panel), with a temporary trace printing both ladder ADCs on every 10 ms poll and every raised flag, over two runs and 112 press runs: correct button on every press, 0 stray flags on release, 0 release sweeps with an intermediate band held for two consecutive samples (one Right release showed a single Back-band sample at 3854, rejected as intended), hold-Right repeats at 400 ms then 200–240 ms with a clean release and no trailing event, a page tap registered once while Right kept repeating, a half-second Confirm produced exactly one Sel, 21 taps at normal pace produced 21 events. Band centres on this unit: Back 3564, Confirm 2727, Left 1500, Right 0; page 2267 and 5. A press landing during the new screen's first refresh is applied to that screen. git diff --check passes. No test framework is added.

Note for a separate change: this env does not define ARDUINO_USB_CDC_ON_BOOT, so Serial is UART0 on GPIO20/21 (I2C SDA and panel CS here) and the console is unreachable over the X3's USB port. The trace above was captured with that flag set in the environment, not in the repo.

Draft. Remaining UI-level checks are listed in the review thread. Known limits: a second press while one is unread is dropped (same as before this PR); multiple taps on one direction during a refresh coalesce; taps that fall entirely between polls, refreshes longer than 1500 ms, and the explicit resetGlobals() after some redraws can still discard navigation.

@LittleBidet
LittleBidet force-pushed the feat/xteink-rapid-button-input branch from ba99796 to eb0c47f Compare October 9, 2026 10:31
@LittleBidet
LittleBidet force-pushed the feat/xteink-rapid-button-input branch from eb0c47f to b8f817b Compare October 9, 2026 16:58
@LittleBidet LittleBidet changed the title Fix Xteink rapid presses lost during hold-repeat cooldown Fix missed Xteink navigation during rapid taps and redraws Oct 9, 2026
Since the per-ladder edge detection landed, a different decoded button
counted as a new press on a single sample. Releasing Right or Down drags
the ADC node back up through the other buttons' bands on its way to idle,
so one transient sample could fire a stray Prev/Sel/Esc/Up. The old
200 ms blanking happened to hide this; 10 ms sampling exposes it.

Require the same decode on two consecutive samples before it can start
a press. A stable idle rearms the ladder, which also covers the single
idle-sample glitch mid-hold that the separate 20 ms release rearm was
for, so that state goes away (13 statics -> 9). Costs one 10 ms sample
of latency on the first press. Power gets the same LongPress bypass as
the ladders.
taskInputHandler wiped every flag with resetGlobals() at the top of each
poll cycle, so an event the UI had not read yet lived at most ~75 ms. On
LCD boards loopTask is back at check() within milliseconds, so that never
showed. On e-paper, loopTask is blocked in the panel refresh for hundreds
of ms after each click, and a tap landing in that window was raised,
wiped, and never seen - the "second press does nothing" complaint on the
Xteink X3.

Replace the direction-only replay latch from the previous commit with a
simpler rule in the task loop, gated on E_PAPER_DISPLAY: while
AnyKeyPress is still set (check() clears it) and LongPress is not, skip
the wipe of the six button flags and only clear touch/keystroke state.
Bounded at 1500 ms so an event nobody reads cannot ghost into a later
screen. Not while LongPress: display.cpp, mykeyboard.cpp and
launcherSelectHeld() poll the raw flags and need them to drop on release.

Covers Sel and Esc too (navigate-then-confirm during a refresh was still
lost), needs no replay step and therefore no mutex inside check() and
resetGlobals(), and drops the board-specific XTEINK_NAV_INPUT_LATCH
flag; globals.h is back to its previous form.

Built xteink-x3-x4 and lilygo-t-deck-pro; xteink-x3-x4 is 464 bytes
smaller than before.
…Press

Two holes in the previous commit's retention rule:

check() clears AnyKeyPress on the first flag it consumes, and the rule
was gated on AnyKeyPress. With two flags unread (nav + page in one
poll, or a tap on each side of a refresh), consuming the first made the
next input cycle see AnyKeyPress == false, take the resetGlobals() path
and wipe the second. Judge pending on the six flags themselves.

Flags carry no order. A press stacked on an unread one was applied in
whatever order the UI polls them: main menu and loopOptions both handle
the move before Sel, so confirm-then-move during a refresh ran the newly
highlighted row, and Right-then-Left cancelled. Hold one unread event at
a time: while a flag is pending, clear whatever this poll newly raised,
which is what every poll did before retention. Only newly raised flags
are cleared, never restored, so a press check() consumed while
InputHandler was running is not put back and cannot double-step.

This also stops a latched press riding alongside a later Esc chord on
hal_buttons_poll_3 (reTerminal E1001/Sticky). Handlers that assign
false while idle still drop their flags, so those boards keep their
pre-retention behaviour.

Built xteink-x3-x4 (1,376,576 bytes), seeedstudio-reterminal-e1001 and
lilygo-t-deck-pro.
Measured on an X3 with a ladder trace at the 10 ms poll:

Confirm and Back repeated at 200 ms like the navigation buttons. Before
the e-paper input task retained unread flags that second Sel was wiped
within 75 ms; now it survives into whatever menu the first Confirm
opened and selects its first row. A half-second Confirm produced two
Sel events in the trace. Only Left/Right/Up/Down repeat now.

Ordinary taps ran up to ~240 ms, so a first repeat at 200 ms
double-stepped 2 of 20 taps. A hold now starts repeating after 400 ms
(first repeat landed at exactly 400 ms on hardware), then every 200 ms.
LongPress still reports live state every poll.

Also confirmed on hardware with the same trace: 0 stray flags over 61
press runs on both ladders, 0 release sweeps with an intermediate band
held for two consecutive samples (one Right release showed a single
Back-band sample at 3854, rejected as intended), clean release with no
trailing event, ladders independent while one is held. Band centres on
this unit: Back 3564, Confirm 2727, Left 1500, Right 0; page 2267 / 5.
@LittleBidet
LittleBidet marked this pull request as ready for review October 13, 2026 22:55
@bmorcelli
bmorcelli merged commit 6d54ce1 into bmorcelli:main Oct 15, 2026
93 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants