mirror of
https://github.com/arendst/Tasmota.git
synced 2026-10-26 22:11:22 +00:00
LVGL Panel improved click handling (#25171)
* LVGL Panel improved click handling * Fix auto-start
This commit is contained in:
1 parent
da51c9b1db
commit
5ce1f1204b
6 files changed
+88
-21
No files matched your search
+2
-1
@@ -19,7 +19,8 @@ All notable changes to this project will be documented in this file.
|
|||||||
- Several buffer overflows
|
- Several buffer overflows
|
||||||
- MiEL HVAC Modbus no response timeouts now serves the RS485 slave from its own task (#25157)
|
- 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)
|
- 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
|
### Removed
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
@@ -13,6 +13,7 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
|
touch-action: none;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<table style="width: 100%;">
|
<table style="width: 100%;">
|
||||||
@@ -39,8 +40,12 @@
|
|||||||
let abortController = null;
|
let abortController = null;
|
||||||
let isStreaming = false;
|
let isStreaming = false;
|
||||||
let dataBuf, readBuf, writeBuf, imageData, imgBuf;
|
let dataBuf, readBuf, writeBuf, imageData, imgBuf;
|
||||||
let touchPos = null;
|
const TOUCH_HEARTBEAT_MS = 250; // resend while held, the device releases after 1 s without update
|
||||||
let touchPressed = false;
|
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
|
// RGB565 to ARGB8888 (Little Endian: AABBGGRR) using bit replication
|
||||||
for (let p = 0; p < 0x10000; p++) {
|
for (let p = 0; p < 0x10000; p++) {
|
||||||
@@ -155,6 +160,7 @@
|
|||||||
if (abortController) {
|
if (abortController) {
|
||||||
abortController.abort();
|
abortController.abort();
|
||||||
}
|
}
|
||||||
|
if (touchId !== null) touchRelease();
|
||||||
if (imageData) {
|
if (imageData) {
|
||||||
imgBuf.fill(0);
|
imgBuf.fill(0);
|
||||||
ctx.putImageData(imageData, 0, 0);
|
ctx.putImageData(imageData, 0, 0);
|
||||||
@@ -162,28 +168,68 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendTouch(t) {
|
function queueTouch(t) {
|
||||||
fetch(`http://${getHost()}/lvgl_touch`, {
|
const ev = { ...touchPos, t };
|
||||||
method: "POST",
|
const last = touchQueue[touchQueue.length - 1];
|
||||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
if (last && last.t === t) {
|
||||||
body: new URLSearchParams({ ...touchPos, t }).toString()
|
touchQueue[touchQueue.length - 1] = ev; // keep only the latest position
|
||||||
}).catch(() => { });
|
} 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) {
|
function updatePos(e) {
|
||||||
const r = canvas.getBoundingClientRect();
|
const r = canvas.getBoundingClientRect();
|
||||||
|
const clamp = (v, max) => Math.min(max - 1, Math.max(0, Math.round(v)));
|
||||||
touchPos = {
|
touchPos = {
|
||||||
x: Math.round((e.clientX - r.left) * (canvas.width / r.width)),
|
x: clamp((e.clientX - r.left) * (canvas.width / r.width), canvas.width),
|
||||||
y: Math.round((e.clientY - r.top) * (canvas.height / r.height))
|
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); } });
|
// Pointer events cover mouse, touch and pen; capture keeps the release when it happens outside the canvas
|
||||||
canvas.addEventListener("mousemove", (e) => { e.preventDefault(); if (e.buttons === 1) updatePos(e); });
|
canvas.addEventListener("pointerdown", (e) => {
|
||||||
canvas.addEventListener("mouseup", (e) => { e.preventDefault(); if (e.button === 0) { touchPressed = false; sendTouch(0); } });
|
if (touchId !== null || e.button !== 0) return;
|
||||||
window.addEventListener("mouseup", (e) => { if (e.button === 0) touchPressed = false; });
|
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) => {
|
lvBtn.addEventListener('click', (e) => {
|
||||||
const host = getHost();
|
const host = getHost();
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ class lvgl_panel
|
|||||||
|
|
||||||
if tasmota.is_network_up()
|
if tasmota.is_network_up()
|
||||||
self.init_network()
|
self.init_network()
|
||||||
|
self.web_add_handler() # if init is called after the network is up, `web_add_handler` event is not fired
|
||||||
else
|
else
|
||||||
tasmota.when_network_up(/ -> self.init_network())
|
tasmota.when_network_up(/ -> self.init_network())
|
||||||
end
|
end
|
||||||
@@ -42,7 +43,10 @@ class lvgl_panel
|
|||||||
end
|
end
|
||||||
|
|
||||||
def unload()
|
def unload()
|
||||||
|
import webserver
|
||||||
|
webserver.remove_route('/lvgl_touch')
|
||||||
if (self.stream_cb != nil)
|
if (self.stream_cb != nil)
|
||||||
|
import cb
|
||||||
import introspect
|
import introspect
|
||||||
lv.set_stream_cb(introspect.toptr(0))
|
lv.set_stream_cb(introspect.toptr(0))
|
||||||
cb.free_cb(self.stream_cb)
|
cb.free_cb(self.stream_cb)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "LVGL Panel",
|
"name": "LVGL Panel",
|
||||||
"version": "0x1A030301",
|
"version": "0x1A0A1501",
|
||||||
"description": "Realtime mirror of the LVGL display in browser",
|
"description": "Realtime mirror of the LVGL display in browser",
|
||||||
"author": "Stephan Hadinger & Milko Daskalov",
|
"author": "Stephan Hadinger & Milko Daskalov",
|
||||||
"min_tasmota": "0x0F030002",
|
"min_tasmota": "0x0F030002",
|
||||||
|
|||||||
@@ -63,8 +63,15 @@ typedef struct TSGlobal_t {
|
|||||||
// multi-point is not yet supported
|
// multi-point is not yet supported
|
||||||
bool touched;
|
bool touched;
|
||||||
bool external_ts;
|
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;
|
} 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;
|
TSGlobal_t TSGlobal;
|
||||||
|
|
||||||
bool CST816S_found = false;
|
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) {
|
if (!force_no_convert) {
|
||||||
TS_RotConvert(&TSGlobal.touch_xp, &TSGlobal.touch_yp);
|
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
|
// 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)) {
|
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
|
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
|
#ifdef SIMPLE_RES_TOUCH
|
||||||
if (SRES_found) {
|
if (SRES_found) {
|
||||||
@@ -439,11 +455,10 @@ void Touch_Check(void(*rotconvert)(int16_t *x, int16_t *y)) {
|
|||||||
}
|
}
|
||||||
#endif // USE_XPT2046
|
#endif // USE_XPT2046
|
||||||
|
|
||||||
TSGlobal.touch_xp = TSGlobal.raw_touch_xp;
|
|
||||||
TSGlobal.touch_yp = TSGlobal.raw_touch_yp;
|
|
||||||
|
|
||||||
if (TSGlobal.touched) {
|
if (TSGlobal.touched) {
|
||||||
was_touched = true;
|
was_touched = true;
|
||||||
|
TSGlobal.touch_xp = TSGlobal.raw_touch_xp;
|
||||||
|
TSGlobal.touch_yp = TSGlobal.raw_touch_yp;
|
||||||
rotconvert(&TSGlobal.touch_xp, &TSGlobal.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);
|
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
|
#endif // USE_TOUCH_BUTTONS
|
||||||
|
|
||||||
} else {
|
} 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) {
|
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);
|
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;
|
was_touched = false;
|
||||||
|
|||||||
Reference in new issue
Block a user