fix(tdeck-pro): harden touch and space holds

This commit is contained in:
Michael A. Cojocari committed 2026-09-17 10:34:18 -04:00
1 parent e83bb7582e
commit 4b7882ca44
4 files changed
+112 -15

No files matched your search

+5
View File
@@ -14,6 +14,11 @@ int heltecV4CapTouchCheck();
bool heltecV4CapTouchPopTap(uint16_t* x, uint16_t* y);
/** True while finger is down; returns latest mapped display coordinates. */
bool heltecV4CapTouchGetLive(uint16_t* x, uint16_t* y);
#if defined(HAS_TDECK_PRO)
/** Physical hold duration from the Pro touch driver, or 0 unless the latest
* hardware sample still reports a finger. */
uint32_t heltecV4CapTouchHeldMs();
#endif
/**
* Pop pending swipe gesture in display coordinates.
* x_dir: -1 left, +1 right, 0 none
+89 -13
View File
@@ -137,7 +137,7 @@ static_assert(ChannelSenderSplit::kMaxWireName >= (size_t)UITask::MAX_SENDER_NAM
#if defined(HAS_TOUCH_UI)
#include <lvgl.h>
#include <helpers/input/HeltecV4CapTouch.h>
#include "../helpers/input/HeltecV4CapTouch.h"
#if CAP_TRACKBALL
#include <helpers/input/TDeckTrackball.h>
#endif
@@ -8652,10 +8652,8 @@ static void chatDeleteApply() {
g_lv.task->showAlert(is_channel ? TR("Channel removed") : TR("Chat removed"), 1000);
}
static void threadLongPressCb(lv_event_t* e) {
if (lv_event_get_code(e) != LV_EVENT_LONG_PRESSED || !g_lv.task) return;
auto* ctx = static_cast<LvThreadButtonCtx*>(lv_event_get_user_data(e));
if (!ctx) return;
static void openThreadActionSheetFromCtx(LvThreadButtonCtx* ctx) {
if (!ctx || !g_lv.task) return;
bool ch; uint16_t unread; uint32_t ts;
char name[UITask::MAX_THREAD_NAME + 1] = "";
if (!g_lv.task->getThreadInfo(ctx->idx, ch, unread, ts, name, sizeof(name))) return;
@@ -8673,6 +8671,44 @@ static void threadLongPressCb(lv_event_t* e) {
openThreadActionSheet(ctx->idx, name, ch);
}
#if defined(HAS_TDECK_PRO)
static lv_obj_t* s_thread_hold_row = nullptr;
static bool s_thread_hold_opened = false;
static constexpr uint32_t kTDeckProThreadHoldMs = 800;
#endif
static void threadLongPressCb(lv_event_t* e) {
#if defined(HAS_TDECK_PRO)
const lv_event_code_t code = lv_event_get_code(e);
lv_obj_t* row = lv_event_get_current_target(e);
if (code == LV_EVENT_PRESSED) {
s_thread_hold_row = row;
s_thread_hold_opened = false;
return;
}
if (code == LV_EVENT_RELEASED || code == LV_EVENT_PRESS_LOST) {
if (s_thread_hold_row == row) {
s_thread_hold_row = nullptr;
s_thread_hold_opened = false;
}
return;
}
if (code != LV_EVENT_PRESSING || s_thread_hold_row != row ||
s_thread_hold_opened || heltecV4CapTouchHeldMs() < kTDeckProThreadHoldMs)
return;
// E-paper refreshes can delay LVGL's release sample long enough to satisfy
// its software timer after a physical tap has ended. Use the touch driver's
// hardware-sampled duration instead; debounce misses never count toward it.
if (heltecV4CapTouchIsSwiping()) return;
s_thread_hold_opened = true;
#else
if (lv_event_get_code(e) != LV_EVENT_LONG_PRESSED) return;
#endif
auto* ctx = static_cast<LvThreadButtonCtx*>(lv_event_get_user_data(e));
openThreadActionSheetFromCtx(ctx);
}
// Per-row gear: opens the SAME thread-settings sheet as a long-press (and as the in-chat cog).
static void threadGearCb(lv_event_t* e) {
if (lv_event_get_code(e) != LV_EVENT_CLICKED || !g_lv.task) return;
@@ -39157,7 +39193,14 @@ static void refreshChatList(LvChatPanel& p) {
lv_obj_add_flag(btn, NAV_THREAD_ROW_FLAG);
lv_obj_set_user_data(btn, reinterpret_cast<void*>(static_cast<intptr_t>(idxs[i] + 1)));
lv_obj_add_event_cb(btn, threadSelectCb, LV_EVENT_CLICKED, &p.ctx_store[i]);
#if defined(HAS_TDECK_PRO)
lv_obj_add_event_cb(btn, threadLongPressCb, LV_EVENT_PRESSED, &p.ctx_store[i]);
lv_obj_add_event_cb(btn, threadLongPressCb, LV_EVENT_PRESSING, &p.ctx_store[i]);
lv_obj_add_event_cb(btn, threadLongPressCb, LV_EVENT_RELEASED, &p.ctx_store[i]);
lv_obj_add_event_cb(btn, threadLongPressCb, LV_EVENT_PRESS_LOST, &p.ctx_store[i]);
#else
lv_obj_add_event_cb(btn, threadLongPressCb, LV_EVENT_LONG_PRESSED, &p.ctx_store[i]);
#endif
}
// Put the scroll back where it was so a data-driven rebuild doesn't snap to the top.
lv_obj_update_layout(p.list_cont);
@@ -40163,6 +40206,7 @@ static void updatePagerAltBackspaceChord() {
// isn't a real hold).
static lv_obj_t* s_pager_locking_popup = nullptr;
static lv_obj_t* s_pager_locking_bar = nullptr;
static bool s_pager_space_text_press = false;
static void pagerLockingPopupHide() {
if (s_pager_locking_popup) { popupClose(&s_pager_locking_popup); s_pager_locking_bar = nullptr; }
@@ -40206,19 +40250,37 @@ static void pagerLockingPopupShow() {
}
static void updatePagerSpaceHold(unsigned long now) {
// Never engage mid-typing (space just types normally there) or once already
// locked/off (nothing left to do -- updatePagerBackspaceUnlockHold owns the
// reverse direction).
if ((g_lv.task && (g_lv.task->isScreenOff() || g_lv.task->isManualLock())) || navFocusedTextarea()) {
pagerLockingPopupHide();
return;
}
const bool held = pagerKeyboardSpaceHeld();
static constexpr uint32_t kLongPressMs = 1000;
static bool s_was_held = false;
static uint32_t s_press_start = 0;
static bool s_long_fired = false;
bool text_input_active = navFocusedTextarea() != nullptr;
#if defined(HAS_TDECK_PRO)
// Pro typing follows the textarea bound to the hidden LVGL keyboard. Focus
// can temporarily move to another chat control while that composer remains
// the active edit target, so navFocusedTextarea() alone is not authoritative.
lv_obj_t* bound_ta = g_lv.keyboard ? lv_keyboard_get_textarea(g_lv.keyboard) : nullptr;
text_input_active = bound_ta && lv_obj_is_valid(bound_ta) &&
(!s_kbd_nav || s_nav_ta_editing);
#endif
// Never engage mid-typing (space just types normally there) or once already
// locked/off (nothing left to do -- updatePagerBackspaceUnlockHold owns the
// reverse direction). Once a press has belonged to an editor, suppress that
// whole physical press through key-up even if focus changes in the meantime.
if (text_input_active && held) s_pager_space_text_press = true;
if ((g_lv.task && (g_lv.task->isScreenOff() || g_lv.task->isManualLock())) ||
text_input_active || s_pager_space_text_press) {
pagerLockingPopupHide();
if (!held) s_pager_space_text_press = false;
s_was_held = held;
s_press_start = 0;
s_long_fired = false;
return;
}
if (held && !s_was_held) {
s_press_start = now;
s_long_fired = false;
@@ -42940,6 +43002,12 @@ if (g_lv.task && g_lv.task->isManualLock()) {
}
return;
}
#if defined(HAS_PAGER_KEYBOARD)
// This is the authoritative classification for the Space press: it reached
// the same textarea path that will insert the character below. Keep that
// physical press ineligible for lock even if focus changes before key-up.
if (key == ' ') s_pager_space_text_press = true;
#endif
txtMenuHide(); // any keypress while editing dismisses an open edit menu
#if CAP_KEYPAD_NAV
// Accent-variant / @-mention popups (issues #22, #42) are otherwise
@@ -56738,7 +56806,11 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no
::display.setBusyHook([] {
static uint32_t last_poll = 0;
const uint32_t now = millis();
if (now - last_poll >= 8) { last_poll = now; pagerKeyboardPoll(); }
if (now - last_poll >= 8) {
last_poll = now;
(void)heltecV4CapTouchCheck();
pagerKeyboardPoll();
}
delay(1);
});
#endif
@@ -59724,6 +59796,10 @@ void UITask::loop() {
// GPIO47/48 but use different driver locks. Keep every transaction on
// loopTask so the touch poll cannot interrupt an expander/ADC transfer.
pushDiagLine("touch inline (shared I2C)");
#elif defined(HAS_TDECK_PRO)
// The e-paper BUSY callback keeps this inline driver polling while the
// panel blocks the UI task, so physical releases cannot remain stale.
pushDiagLine("touch inline + e-paper busy");
#else
// Once hardware is up, hand the chsc6x poll off to a pinned task on
// core 0 so it runs at a fixed ~125 Hz independent of LVGL render and
+4 -2
View File
@@ -53,8 +53,10 @@ the controller's ten-event FIFO. The Pro matrix order is covered by
`test/test_tdeck_pro_keyboard_state.cpp`.
Touch probes CST3530 first and falls back to CST328. It is polled inline with
the UI to keep the shared I2C bus single-owner. While asleep, touch and keyboard
events are drained but do not wake the display; GPIO0 is the wake control.
the UI and from the panel BUSY callback during e-paper refreshes, keeping the
shared I2C bus single-owner while still observing releases during a blocked
refresh. While asleep, touch and keyboard events are drained but do not wake the
display; GPIO0 is the wake control.
## Validation status
@@ -19,10 +19,13 @@ bool swiping = false;
uint16_t x = 0, y = 0, start_x = 0, start_y = 0, tap_x = 0, tap_y = 0;
int8_t swipe_x = 0, swipe_y = 0;
uint32_t down_at = 0;
uint8_t release_misses = 0;
constexpr uint8_t RELEASE_DEBOUNCE = 2;
void poll() {
uint16_t next_x = 0, next_y = 0;
if (display.getTouchPoint(next_x, next_y)) {
release_misses = 0;
x = next_x;
y = next_y;
live = true;
@@ -39,6 +42,11 @@ void poll() {
return;
}
if (down && release_misses < RELEASE_DEBOUNCE) {
++release_misses;
return;
}
release_misses = 0;
live = false;
if (!down) return;
down = false;
@@ -76,6 +84,12 @@ bool heltecV4CapTouchGetLive(uint16_t* out_x, uint16_t* out_y) {
if (out_y) *out_y = y;
return true;
}
uint32_t heltecV4CapTouchHeldMs() {
// A debounce miss keeps LVGL's pointer stable, but must not extend a short
// physical tap into a hold. Only a fresh positive controller sample counts.
if (!down || !live || release_misses != 0 || down_at == 0) return 0;
return millis() - down_at;
}
bool heltecV4CapTouchPopSwipe(int8_t* out_x, int8_t* out_y) {
if (!swipe_pending) return false;
swipe_pending = false;