diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5290be90a..0bf67761c 100755
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -19,7 +19,8 @@ All notable changes to this project will be documented in this file.
- Several buffer overflows
- MiEL HVAC Modbus no response timeouts now serves the RS485 slave from its own task (#25157)
- ESP32 `PWMFrequency` < 8 result in WDT exception after restart (#25155)
-- LVGL Mirror extension crash (#25165)
+- LVGL Panel extension crash (#25165)
+- LVGL Panel improved click handling
### Removed
diff --git a/tasmota/berry/extensions/LVGL_Panel.tapp b/tasmota/berry/extensions/LVGL_Panel.tapp
index 527c8ae1e..1a24999db 100644
Binary files a/tasmota/berry/extensions/LVGL_Panel.tapp and b/tasmota/berry/extensions/LVGL_Panel.tapp differ
diff --git a/tasmota/berry/extensions/LVGL_Panel/lvgl.html b/tasmota/berry/extensions/LVGL_Panel/lvgl.html
index 711ff423f..872bf0046 100644
--- a/tasmota/berry/extensions/LVGL_Panel/lvgl.html
+++ b/tasmota/berry/extensions/LVGL_Panel/lvgl.html
@@ -13,6 +13,7 @@
width: 100%;
height: 100%;
object-fit: contain;
+ touch-action: none;
}
@@ -39,8 +40,12 @@
let abortController = null;
let isStreaming = false;
let dataBuf, readBuf, writeBuf, imageData, imgBuf;
- let touchPos = null;
- let touchPressed = false;
+ const TOUCH_HEARTBEAT_MS = 250; // resend while held, the device releases after 1 s without update
+ let touchPos = null; // last position {x, y} in screen pixels
+ let touchId = null; // pointerId of the active press, null when released
+ let touchTimer = null; // heartbeat while pressed
+ let touchQueue = []; // pending events, consecutive events of the same type are merged
+ let touchBusy = false; // one request at a time keeps events in order and avoids flooding
// RGB565 to ARGB8888 (Little Endian: AABBGGRR) using bit replication
for (let p = 0; p < 0x10000; p++) {
@@ -155,6 +160,7 @@
if (abortController) {
abortController.abort();
}
+ if (touchId !== null) touchRelease();
if (imageData) {
imgBuf.fill(0);
ctx.putImageData(imageData, 0, 0);
@@ -162,28 +168,68 @@
}
}
- function sendTouch(t) {
- fetch(`http://${getHost()}/lvgl_touch`, {
- method: "POST",
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
- body: new URLSearchParams({ ...touchPos, t }).toString()
- }).catch(() => { });
+ function queueTouch(t) {
+ const ev = { ...touchPos, t };
+ const last = touchQueue[touchQueue.length - 1];
+ if (last && last.t === t) {
+ touchQueue[touchQueue.length - 1] = ev; // keep only the latest position
+ } else {
+ touchQueue.push(ev); // never drop a press or a release
+ }
+ if (!touchBusy) sendTouch();
+ }
+
+ async function sendTouch() {
+ touchBusy = true;
+ while (touchQueue.length) {
+ const ev = touchQueue.shift();
+ try {
+ await fetch(`http://${getHost()}/lvgl_touch`, {
+ method: "POST",
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
+ body: new URLSearchParams(ev).toString(),
+ signal: AbortSignal.timeout ? AbortSignal.timeout(2000) : undefined
+ });
+ } catch (err) { }
+ }
+ touchBusy = false;
}
function updatePos(e) {
const r = canvas.getBoundingClientRect();
+ const clamp = (v, max) => Math.min(max - 1, Math.max(0, Math.round(v)));
touchPos = {
- x: Math.round((e.clientX - r.left) * (canvas.width / r.width)),
- y: Math.round((e.clientY - r.top) * (canvas.height / r.height))
+ x: clamp((e.clientX - r.left) * (canvas.width / r.width), canvas.width),
+ y: clamp((e.clientY - r.top) * (canvas.height / r.height), canvas.height)
};
}
- setInterval(() => { if (touchPressed) sendTouch(1); }, 50);
+ function touchRelease(e) {
+ touchId = null;
+ clearInterval(touchTimer);
+ if (e && e.type === "pointerup") updatePos(e);
+ queueTouch(0);
+ }
- canvas.addEventListener("mousedown", (e) => { e.preventDefault(); if (e.button === 0) { touchPressed = true; updatePos(e); } });
- canvas.addEventListener("mousemove", (e) => { e.preventDefault(); if (e.buttons === 1) updatePos(e); });
- canvas.addEventListener("mouseup", (e) => { e.preventDefault(); if (e.button === 0) { touchPressed = false; sendTouch(0); } });
- window.addEventListener("mouseup", (e) => { if (e.button === 0) touchPressed = false; });
+ // Pointer events cover mouse, touch and pen; capture keeps the release when it happens outside the canvas
+ canvas.addEventListener("pointerdown", (e) => {
+ if (touchId !== null || e.button !== 0) return;
+ e.preventDefault();
+ touchId = e.pointerId;
+ canvas.setPointerCapture(e.pointerId);
+ updatePos(e);
+ queueTouch(1);
+ touchTimer = setInterval(() => queueTouch(1), TOUCH_HEARTBEAT_MS);
+ });
+ canvas.addEventListener("pointermove", (e) => {
+ if (e.pointerId !== touchId) return;
+ updatePos(e);
+ queueTouch(1);
+ });
+ for (const type of ["pointerup", "pointercancel", "lostpointercapture"]) {
+ canvas.addEventListener(type, (e) => { if (e.pointerId === touchId) touchRelease(e); });
+ }
+ canvas.addEventListener("contextmenu", (e) => e.preventDefault()); // long press on mobile
lvBtn.addEventListener('click', (e) => {
const host = getHost();
diff --git a/tasmota/berry/extensions/LVGL_Panel/lvgl_panel.be b/tasmota/berry/extensions/LVGL_Panel/lvgl_panel.be
index 94e56e9fa..d96d60b05 100644
--- a/tasmota/berry/extensions/LVGL_Panel/lvgl_panel.be
+++ b/tasmota/berry/extensions/LVGL_Panel/lvgl_panel.be
@@ -30,6 +30,7 @@ class lvgl_panel
if tasmota.is_network_up()
self.init_network()
+ self.web_add_handler() # if init is called after the network is up, `web_add_handler` event is not fired
else
tasmota.when_network_up(/ -> self.init_network())
end
@@ -42,7 +43,10 @@ class lvgl_panel
end
def unload()
+ import webserver
+ webserver.remove_route('/lvgl_touch')
if (self.stream_cb != nil)
+ import cb
import introspect
lv.set_stream_cb(introspect.toptr(0))
cb.free_cb(self.stream_cb)
diff --git a/tasmota/berry/extensions/LVGL_Panel/manifest.json b/tasmota/berry/extensions/LVGL_Panel/manifest.json
index 9c1a124f3..94e14a5ff 100644
--- a/tasmota/berry/extensions/LVGL_Panel/manifest.json
+++ b/tasmota/berry/extensions/LVGL_Panel/manifest.json
@@ -1,6 +1,6 @@
{
"name": "LVGL Panel",
- "version": "0x1A030301",
+ "version": "0x1A0A1501",
"description": "Realtime mirror of the LVGL display in browser",
"author": "Stephan Hadinger & Milko Daskalov",
"min_tasmota": "0x0F030002",
diff --git a/tasmota/tasmota_xdrv_driver/xdrv_55_touch.ino b/tasmota/tasmota_xdrv_driver/xdrv_55_touch.ino
index 7f6934b70..f16c4a0e0 100644
--- a/tasmota/tasmota_xdrv_driver/xdrv_55_touch.ino
+++ b/tasmota/tasmota_xdrv_driver/xdrv_55_touch.ino
@@ -63,8 +63,15 @@ typedef struct TSGlobal_t {
// multi-point is not yet supported
bool touched;
bool external_ts;
+ bool ext_pressed; // press injected by display.touch_update(), ex: LVGL Panel in the browser
+ uint32_t ext_last; // millis() of the last injected update
} TSGlobal_t;
+// An injected press overrides the touch controller until it is released or not refreshed for this time
+#ifndef TOUCH_EXT_TIMEOUT_MS
+#define TOUCH_EXT_TIMEOUT_MS 1000
+#endif
+
TSGlobal_t TSGlobal;
bool CST816S_found = false;
@@ -92,6 +99,8 @@ void Touch_SetStatus(uint8_t touches, uint16_t raw_x, uint16_t raw_y, uint8_t ge
if (!force_no_convert) {
TS_RotConvert(&TSGlobal.touch_xp, &TSGlobal.touch_yp);
}
+ TSGlobal.ext_pressed = TSGlobal.touched;
+ TSGlobal.ext_last = millis();
}
// return true if succesful, false if not configured
@@ -386,6 +395,13 @@ int16_t XPT2046_y() {
void Touch_Check(void(*rotconvert)(int16_t *x, int16_t *y)) {
static bool was_touched = false; // flag used to log the data sent when the screen was just released
+ // An injected press (display.touch_update) is kept until released, otherwise
+ // polling the controller would release it within 50 ms
+ if (TSGlobal.ext_pressed) {
+ if (!TimeReached(TSGlobal.ext_last + TOUCH_EXT_TIMEOUT_MS)) { return; }
+ TSGlobal.ext_pressed = false; // no refresh from the source, release it
+ TSGlobal.touched = false;
+ }
#ifdef SIMPLE_RES_TOUCH
if (SRES_found) {
@@ -439,11 +455,10 @@ void Touch_Check(void(*rotconvert)(int16_t *x, int16_t *y)) {
}
#endif // USE_XPT2046
- TSGlobal.touch_xp = TSGlobal.raw_touch_xp;
- TSGlobal.touch_yp = TSGlobal.raw_touch_yp;
-
if (TSGlobal.touched) {
was_touched = true;
+ TSGlobal.touch_xp = TSGlobal.raw_touch_xp;
+ TSGlobal.touch_yp = TSGlobal.raw_touch_yp;
rotconvert(&TSGlobal.touch_xp, &TSGlobal.touch_yp);
AddLog(LOG_LEVEL_DEBUG_MORE, "TS : touched x=%i y=%i gest=0x%02x (raw x=%i y=%i)", TSGlobal.touch_xp, TSGlobal.touch_yp, TSGlobal.gesture, TSGlobal.raw_touch_xp, TSGlobal.raw_touch_yp);
@@ -452,7 +467,8 @@ void Touch_Check(void(*rotconvert)(int16_t *x, int16_t *y)) {
#endif // USE_TOUCH_BUTTONS
} else {
- rotconvert(&TSGlobal.touch_xp, &TSGlobal.touch_yp); // still do rot convert if not TSGlobal.touched
+ // keep the last pressed point; converting raw again would break an injected point
+ // (already in screen coordinates) on a rotated display
if (was_touched) {
AddLog(LOG_LEVEL_DEBUG_MORE, "TS : released x=%i y=%i (raw x=%i y=%i)", TSGlobal.touch_xp, TSGlobal.touch_yp, TSGlobal.raw_touch_xp, TSGlobal.raw_touch_yp);
was_touched = false;