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;