Files
firmware/src/input/TouchScreenBase.cpp
T
31f05ab057 fix(touch): stop LONG_PRESS repeating when the suppression deadline wraps (#11829)
* fix(touch): stop LONG_PRESS repeating when the suppression deadline wraps

TouchScreenBase::_start was one field doing two incompatible jobs. It held the
press-down timestamp, and then the LONG_PRESS handler overwrote it with
`millis() + 30000` to stop the event repeating for the rest of the hold. Every read
was a hand-rolled signed subtraction on time_t, and suppression worked only because
`time_t(millis()) - _start` came out around -30000.

Where time_t is 64 bits - the portduino host - that uint32_t sum wraps to a small
number while millis() is still just under 0xFFFFFFFF. The subtraction then goes
hugely positive instead of negative, the threshold test passes on every 20ms poll,
and each pass re-arms to another wrapped value. It keeps firing until millis()
itself wraps, up to ~30 s later: about 1500 TOUCH_ACTION_LONG_PRESS events injected
into InputBroker for one finger that never moved. Modelling the old expression
across press-start offsets puts the worst case at exactly 1500 for a 60 s hold,
where three is correct. On a 32-bit time_t build the signed wrap happens to keep
suppressing, so this is host-and-variant dependent rather than universal.

The zero-dodging helpers in src/UptimeClock.h are no use here: they map 0 to 1, and
1 reads as "long ago" exactly as 0 does. The defect is the overload, not the zero,
so the field is split by what it is actually asked:

  _pressStartMs             a past event time - how long has the finger been down
  _longPressSuppressed      is repeat suppression armed
  _longPressSuppressUntilMs when it expires, read only while the bool is set

Two fields for the suppression rather than one, for the reason Throttle.h's
TODO(deadline-type) gives: armed has to stay a separate question from passed. No
single value can stand in for "unarmed" here either, since deadlinePassed() reads 0
as long past below ~24.8 days of uptime and as far future above it. Nothing new uses
0 as a sentinel, so bin/lint-unset-sentinel-millis.sh needs no entry.

All three comparisons now go through Throttle - hasElapsed() for the two
elapsed-since-press questions, which also buys the full ~49.7 day range that a
stored event time gets, and deadlinePassed() for the suppression window.

Behaviour is preserved deliberately, including the part that is easy to miss: the
old `+ 30000` made a held finger re-report LONG_PRESS once every 30 s, not once per
touch. A bool latch would have been simpler and quietly narrowed that, so the
window is kept as LONG_PRESS_REPEAT_SUPPRESS_MS. Old and new were compared across
five wrap scenarios and agree everywhere except the wrap window the old code got
wrong. The tap-on-release suppression the old write also provided is not needed: a
hold long enough to reach here has duration >= TIME_LONG_PRESS, so the tap branch
already takes its else and clears _tapped.

One guard added while here. The RAK14014 deferred-tap window is TIME_LONG_PRESS - 50
and that subtraction is unsigned now, so a variant lowering TIME_LONG_PRESS below 50
would underflow it into a ~49.7 day wait and the deferred TAP would never fire. The
only override in the tree is t5s3_epaper at 500; a static_assert fails the build
instead of the touch panel.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* style(touch): trim comments to the house limit

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: nomdetom <nomdetom@protonmail.com>
2026-09-14 09:34:31 +00:00

224 lines
7.5 KiB
C++

#include "TouchScreenBase.h"
#include "main.h"
#include "mesh/Throttle.h"
#if defined(RAK14014) && !defined(MESHTASTIC_EXCLUDE_CANNEDMESSAGES)
#include "modules/CannedMessageModule.h"
#endif
#ifndef TIME_LONG_PRESS
#define TIME_LONG_PRESS 400
#endif
// The deferred-tap window is `TIME_LONG_PRESS - 50`, unsigned: below 50 it underflows to ~49.7 days.
static_assert(TIME_LONG_PRESS >= 50, "TIME_LONG_PRESS must be at least 50ms: see the deferred-tap window below");
// How long a held finger stays suppressed after a LONG_PRESS is reported.
#define LONG_PRESS_REPEAT_SUPPRESS_MS 30000
// Touch sampling cadence (milliseconds).
// Can be overridden by board variants for faster touch panels.
#ifndef TOUCH_POLL_INTERVAL_IDLE
#define TOUCH_POLL_INTERVAL_IDLE 100
#endif
#ifndef TOUCH_POLL_INTERVAL_ACTIVE
#define TOUCH_POLL_INTERVAL_ACTIVE 20
#endif
#ifndef TOUCH_POLL_INTERVAL_RELEASE
#define TOUCH_POLL_INTERVAL_RELEASE 50
#endif
// Faster cadence used for keyboard-like tap-heavy UIs.
#ifndef TOUCH_POLL_INTERVAL_ACTIVE_FAST
#define TOUCH_POLL_INTERVAL_ACTIVE_FAST TOUCH_POLL_INTERVAL_ACTIVE
#endif
#ifndef TOUCH_POLL_INTERVAL_RELEASE_FAST
#define TOUCH_POLL_INTERVAL_RELEASE_FAST TOUCH_POLL_INTERVAL_RELEASE
#endif
// Ignore very short "finger lifted" glitches from noisy touch controllers.
// A release is only accepted once we've seen no-touch for at least this duration.
#ifndef TOUCH_RELEASE_GRACE_MS
#define TOUCH_RELEASE_GRACE_MS 35
#endif
// move a minimum distance over the screen to detect a "swipe"
#ifndef TOUCH_THRESHOLD_X
#define TOUCH_THRESHOLD_X 30
#endif
#ifndef TOUCH_THRESHOLD_Y
#define TOUCH_THRESHOLD_Y 20
#endif
TouchScreenBase::TouchScreenBase(const char *name, uint16_t width, uint16_t height)
: concurrency::OSThread(name), _display_width(width), _display_height(height), _first_x(0), _last_x(0), _first_y(0),
_last_y(0), _pressStartMs(0), _longPressSuppressed(false), _longPressSuppressUntilMs(0), _lastTouchSeenMs(0),
_tapped(false), _originName(name)
{
}
void TouchScreenBase::init(bool hasTouch)
{
if (hasTouch) {
LOG_INFO("TouchScreen initialized %d %d", TOUCH_THRESHOLD_X, TOUCH_THRESHOLD_Y);
this->setInterval(TOUCH_POLL_INTERVAL_IDLE);
} else {
disable();
this->setInterval(UINT_MAX);
}
}
int32_t TouchScreenBase::runOnce()
{
uint32_t nowMs = millis();
if (nowMs - _lastRun < 20) { // suppress too fast consecutive runOnce() executions
return 20;
}
_lastRun = nowMs;
TouchEvent e;
e.touchEvent = static_cast<char>(TOUCH_ACTION_NONE);
this->setInterval(TOUCH_POLL_INTERVAL_IDLE);
const bool fastTapMode = fastTapModeEnabled();
const bool allowLongPress = longPressEnabled();
// process touch events
int16_t x, y;
bool touched = getTouch(x, y);
if (x < 0 || y < 0) // T-deck can emit phantom touch events with a negative value when turning off the screen
touched = false;
if (touched) {
_lastTouchSeenMs = millis();
this->setInterval(fastTapMode ? TOUCH_POLL_INTERVAL_ACTIVE_FAST : TOUCH_POLL_INTERVAL_ACTIVE);
_last_x = x;
_last_y = y;
} else if (_touchedOld && ((uint32_t)millis() - _lastTouchSeenMs) < TOUCH_RELEASE_GRACE_MS) {
// Treat brief no-touch samples as continuous touch to preserve long-press detection.
touched = true;
}
if (touched != _touchedOld) {
if (touched) {
hapticFeedback();
_state = TOUCH_EVENT_OCCURRED;
_pressStartMs = nowMs;
_longPressSuppressed = false;
_first_x = x;
_first_y = y;
} else {
_state = TOUCH_EVENT_CLEARED;
uint32_t duration = nowMs - _pressStartMs;
x = _last_x;
y = _last_y;
this->setInterval(fastTapMode ? TOUCH_POLL_INTERVAL_RELEASE_FAST : TOUCH_POLL_INTERVAL_RELEASE);
// compute distance
int16_t dx = x - _first_x;
int16_t dy = y - _first_y;
uint16_t adx = abs(dx);
uint16_t ady = abs(dy);
// swipe horizontal
if (adx > ady && adx > TOUCH_THRESHOLD_X) {
if (0 > dx) { // swipe right to left
e.touchEvent = static_cast<char>(TOUCH_ACTION_LEFT);
LOG_DEBUG("action SWIPE: right to left");
} else { // swipe left to right
e.touchEvent = static_cast<char>(TOUCH_ACTION_RIGHT);
LOG_DEBUG("action SWIPE: left to right");
}
}
// swipe vertical
else if (ady > adx && ady > TOUCH_THRESHOLD_Y) {
if (0 > dy) { // swipe bottom to top
e.touchEvent = static_cast<char>(TOUCH_ACTION_UP);
LOG_DEBUG("action SWIPE: bottom to top");
} else { // swipe top to bottom
e.touchEvent = static_cast<char>(TOUCH_ACTION_DOWN);
LOG_DEBUG("action SWIPE: top to bottom");
}
}
// tap
else {
if (duration > 0 && (duration < TIME_LONG_PRESS || !allowLongPress)) {
if (_tapped) {
_tapped = false;
} else {
_tapped = true;
}
} else {
_tapped = false;
}
}
}
}
_touchedOld = touched;
#if defined RAK14014
// Speed up the processing speed of the keyboard in virtual keyboard mode
auto state = cannedMessageModule->getRunState();
if (state == CANNED_MESSAGE_RUN_STATE_FREETEXT) {
if (_tapped) {
_tapped = false;
e.touchEvent = static_cast<char>(TOUCH_ACTION_TAP);
LOG_DEBUG("action TAP(%d/%d)", _last_x, _last_y);
}
} else {
if (_tapped && Throttle::hasElapsed(_pressStartMs, TIME_LONG_PRESS - 50)) {
_tapped = false;
e.touchEvent = static_cast<char>(TOUCH_ACTION_TAP);
LOG_DEBUG("action TAP(%d/%d)", _last_x, _last_y);
}
}
#else
// fire TAP event when no 2nd tap occurred within time
if (_tapped) {
_tapped = false;
e.touchEvent = static_cast<char>(TOUCH_ACTION_TAP);
LOG_DEBUG("action TAP(%d/%d)", _last_x, _last_y);
}
#endif
// fire LONG_PRESS event without the need for release
// Armed and expired are asked separately; folding the deadline into the press stamp repeated
// LONG_PRESS every poll across the wrap on 64-bit time_t hosts.
const bool longPressSuppressed = _longPressSuppressed && !Throttle::deadlinePassed(_longPressSuppressUntilMs);
if (allowLongPress && touched && !longPressSuppressed && Throttle::hasElapsed(_pressStartMs, TIME_LONG_PRESS)) {
// A finger held past the window re-reports LONG_PRESS once per window, as before.
_longPressSuppressed = true;
_longPressSuppressUntilMs = nowMs + LONG_PRESS_REPEAT_SUPPRESS_MS;
e.touchEvent = static_cast<char>(TOUCH_ACTION_LONG_PRESS);
LOG_DEBUG("action LONG PRESS(%d/%d)", _last_x, _last_y);
}
if (e.touchEvent != TOUCH_ACTION_NONE) {
e.source = this->_originName;
e.x = _last_x;
e.y = _last_y;
onEvent(e);
}
return interval;
}
void TouchScreenBase::hapticFeedback()
{
#if defined(T_WATCH_S3) || defined(T_WATCH_ULTRA)
drv.setWaveform(0, 75);
drv.setWaveform(1, 0); // end waveform
drv.go();
#endif
}
bool TouchScreenBase::fastTapModeEnabled() const
{
return false;
}
bool TouchScreenBase::longPressEnabled() const
{
return true;
}