mirror of
https://github.com/arendst/Tasmota.git
synced 2026-07-28 04:15:53 +00:00
LVGL Panel optimizations (#24552)
* set no wait on connect * removed incremental draw; reduced data moving; reduced touch events * added set_nowait method to set TCP_NODELAY socket option * added last chunk indicator for the stream callback * writebytes optimizations
This commit is contained in:
Binary file not shown.
@@ -38,7 +38,9 @@
|
||||
|
||||
let abortController = null;
|
||||
let isStreaming = false;
|
||||
let imageData, argb, readBuf, readBuf16, updateBuffer;
|
||||
let dataBuf, readBuf, writeBuf, imageData, imgBuf;
|
||||
let touchPos = null;
|
||||
let touchPressed = false;
|
||||
|
||||
// RGB565 to ARGB8888 (Little Endian: AABBGGRR) using bit replication
|
||||
for (let p = 0; p < 0x10000; p++) {
|
||||
@@ -65,11 +67,11 @@
|
||||
border.style.height = (parentWidth * height / width) + 'px';
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
dataBuf = new Uint8Array((HEADER_WORDS + (width * height)) * 2);
|
||||
readBuf = new Uint16Array(dataBuf.buffer);
|
||||
writeBuf = new Uint16Array(width * height);
|
||||
imageData = ctx.createImageData(width, height);
|
||||
argb = new Uint32Array(imageData.data.buffer);
|
||||
readBuf = new Uint8Array(width * height * 2 + HEADER_WORDS * 2);
|
||||
readBuf16 = new Uint16Array(readBuf.buffer);
|
||||
updateBuffer = new Uint16Array(width * height);
|
||||
imgBuf = new Uint32Array(imageData.data.buffer);
|
||||
}
|
||||
isStreaming = true;
|
||||
} else {
|
||||
@@ -78,72 +80,67 @@
|
||||
return;
|
||||
}
|
||||
const reader = response.body.getReader();
|
||||
let available = 0;
|
||||
let expectedPixels, x, y, w, h;
|
||||
let readOffset, dIdx, dInc, uIdx;
|
||||
let drawnRows = 0;
|
||||
let frameStart = 0;
|
||||
let dataOffset = 0;
|
||||
let readOffset = 0;
|
||||
let headerParsed = false;
|
||||
let expectedPixels = 0;
|
||||
let writeOffset = 0;
|
||||
let x = 0, y = 0, w = 0, h = 0;
|
||||
while (isStreaming) {
|
||||
const { value, done } = await reader.read();
|
||||
if (done) break;
|
||||
readBuf.set(value, available * 2);
|
||||
available += value.length / 2;
|
||||
dataBuf.set(value, dataOffset);
|
||||
dataOffset += value.length;
|
||||
const totalWords = dataOffset >> 1;
|
||||
while (true) {
|
||||
if (!expectedPixels) {
|
||||
if (available - frameStart < HEADER_WORDS) break;
|
||||
[magic, x, y, w, h] = readBuf16.subarray(frameStart);
|
||||
if (!headerParsed) {
|
||||
if (totalWords - readOffset < HEADER_WORDS) break;
|
||||
[magic, x, y, w, h] = readBuf.subarray(readOffset);
|
||||
if (magic !== LV_MAGIC) {
|
||||
console.error("Sync Error: No magic found");
|
||||
stopStream();
|
||||
return;
|
||||
}
|
||||
expectedPixels = w * h;
|
||||
readOffset = frameStart + HEADER_WORDS;
|
||||
readOffset += HEADER_WORDS;
|
||||
writeOffset = 0;
|
||||
drawnRows = 0;
|
||||
dIdx = y * width + x;
|
||||
dInc = width - w;
|
||||
uIdx = 0;
|
||||
iIdx = y * width + x;
|
||||
iInc = width - w;
|
||||
wIdx = 0;
|
||||
headerParsed = true;
|
||||
}
|
||||
while (readOffset < available && writeOffset < expectedPixels) {
|
||||
const header = readBuf16[readOffset];
|
||||
while (readOffset < totalWords && writeOffset < expectedPixels) {
|
||||
const header = readBuf[readOffset];
|
||||
const isRun = header & 0x8000;
|
||||
const count = (header & 0x7FFF) + (isRun ? 2 : 1);
|
||||
if (available < readOffset + 1 + (isRun ? 1 : count)) break;
|
||||
if (totalWords < readOffset + 1 + (isRun ? 1 : count)) break;
|
||||
readOffset++;
|
||||
if (isRun) {
|
||||
updateBuffer.fill(readBuf16[readOffset++], writeOffset, writeOffset + count);
|
||||
writeBuf.fill(readBuf[readOffset++], writeOffset, writeOffset + count);
|
||||
} else {
|
||||
updateBuffer.set(readBuf16.subarray(readOffset, readOffset + count), writeOffset);
|
||||
writeBuf.set(readBuf.subarray(readOffset, readOffset + count), writeOffset);
|
||||
readOffset += count;
|
||||
}
|
||||
writeOffset += count;
|
||||
}
|
||||
const availableRows = Math.floor(writeOffset / w);
|
||||
if (availableRows > drawnRows) {
|
||||
const newRows = availableRows - drawnRows;
|
||||
for (let r = 0; r < newRows; r++, dIdx += dInc) {
|
||||
for (let c = 0; c < w; c++) {
|
||||
argb[dIdx++] = RGB565_LUT[updateBuffer[uIdx++]];
|
||||
}
|
||||
}
|
||||
ctx.putImageData(imageData, 0, 0, x, y + drawnRows, w, newRows);
|
||||
drawnRows = availableRows;
|
||||
}
|
||||
if (writeOffset < expectedPixels) break;
|
||||
frameStart = readOffset;
|
||||
expectedPixels = undefined;
|
||||
}
|
||||
if (frameStart > 0) {
|
||||
if (frameStart < available) {
|
||||
readBuf16.copyWithin(0, frameStart, available);
|
||||
readOffset -= frameStart;
|
||||
} else {
|
||||
readOffset = 0;
|
||||
for (let r = 0; r < h; r++, iIdx += iInc) {
|
||||
for (let c = 0; c < w; c++) {
|
||||
imgBuf[iIdx++] = RGB565_LUT[writeBuf[wIdx++]];
|
||||
}
|
||||
}
|
||||
available -= frameStart;
|
||||
frameStart = 0;
|
||||
ctx.putImageData(imageData, 0, 0, x, y, w, h);
|
||||
headerParsed = false;
|
||||
}
|
||||
if (readOffset === totalWords) {
|
||||
dataOffset = 0;
|
||||
} else {
|
||||
const consumedBytes = readOffset * 2;
|
||||
const remainingBytes = dataOffset - consumedBytes;
|
||||
dataBuf.copyWithin(0, consumedBytes, dataOffset);
|
||||
dataOffset = remainingBytes;
|
||||
}
|
||||
readOffset = 0;
|
||||
}
|
||||
} catch (err) {
|
||||
if (err.name !== 'AbortError') console.error("Stream Error: ", err);
|
||||
@@ -159,30 +156,34 @@
|
||||
abortController.abort();
|
||||
}
|
||||
if (imageData) {
|
||||
argb.fill(0);
|
||||
imgBuf.fill(0);
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
imageData = undefined
|
||||
}
|
||||
}
|
||||
|
||||
function sendTouchEvent(e) {
|
||||
e.preventDefault();
|
||||
if (e.type === "mousemove" && e.buttons !== 1) return;
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const x = Math.round((e.clientX - rect.left) * (canvas.width / rect.width));
|
||||
const y = Math.round((e.clientY - rect.top) * (canvas.height / rect.height));
|
||||
const t = e.type === "mouseup" ? 0 : 1;
|
||||
const host = getHost();
|
||||
fetch(`http://${host}/lvgl_touch`, {
|
||||
function sendTouch(t) {
|
||||
fetch(`http://${getHost()}/lvgl_touch`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: new URLSearchParams({ x, y, t }).toString()
|
||||
}).catch(err => console.error("Touch event error:", err));
|
||||
body: new URLSearchParams({ ...touchPos, t }).toString()
|
||||
}).catch(() => { });
|
||||
}
|
||||
|
||||
canvas.addEventListener("mousemove", sendTouchEvent);
|
||||
canvas.addEventListener("mouseup", sendTouchEvent);
|
||||
canvas.addEventListener("mousedown", sendTouchEvent);
|
||||
function updatePos(e) {
|
||||
const r = canvas.getBoundingClientRect();
|
||||
touchPos = {
|
||||
x: Math.round((e.clientX - r.left) * (canvas.width / r.width)),
|
||||
y: Math.round((e.clientY - r.top) * (canvas.height / r.height))
|
||||
};
|
||||
}
|
||||
|
||||
setInterval(() => { if (touchPressed) sendTouch(1); }, 50);
|
||||
|
||||
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; });
|
||||
|
||||
lvBtn.addEventListener('click', (e) => {
|
||||
const host = getHost();
|
||||
|
||||
@@ -71,6 +71,7 @@ class lvgl_panel
|
||||
var req = cnx.read()
|
||||
var w = lv.get_hor_res()
|
||||
var h = lv.get_ver_res()
|
||||
cnx.set_nowait(true)
|
||||
cnx.write(f"HTTP/1.1 200 OK\r\n"
|
||||
"Content-Type: application/octet-stream\r\n"
|
||||
"Screen-Size: {w}x{h}\r\n"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "LVGL Panel",
|
||||
"version": "0x1A030300",
|
||||
"version": "0x1A030301",
|
||||
"description": "Realtime mirror of the LVGL display in browser",
|
||||
"author": "Stephan Hadinger & Milko Daskalov",
|
||||
"min_tasmota": "0x0F030002",
|
||||
|
||||
@@ -212,6 +212,16 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
void set_nowait(bool nowait) {
|
||||
if (state == AsyncTCPState::CONNECTED) {
|
||||
int flag = nowait ? 1 : 0;
|
||||
int result = setsockopt(sockfd, IPPROTO_TCP, TCP_NODELAY, (char *)&flag, sizeof(int));
|
||||
if (result < 0) {
|
||||
AddLog(LOG_LEVEL_INFO, "BRY: Error: setsockopt failed on fd %d, errno: %d, \"%s\"", sockfd, errno, strerror(errno));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
size_t available(void) {
|
||||
_update_connected();
|
||||
if (state == AsyncTCPState::CONNECTED) {
|
||||
@@ -290,28 +300,20 @@ public:
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool writebytes(const char *buf, size_t size)
|
||||
{
|
||||
if (state != AsyncTCPState::CONNECTED) {
|
||||
return false;
|
||||
}
|
||||
bool writebytes(const char *buf, size_t size) {
|
||||
if (state != AsyncTCPState::CONNECTED) return false;
|
||||
size_t offset = 0;
|
||||
while (offset < size) {
|
||||
fd_set wfd, efd;
|
||||
FD_ZERO(&wfd);
|
||||
FD_ZERO(&efd);
|
||||
FD_SET(sockfd, &wfd);
|
||||
FD_SET(sockfd, &efd);
|
||||
struct timeval tv = {0, 1000};
|
||||
int sent = 0;
|
||||
int res = ::select(sockfd + 1, NULL, &wfd, &efd, &tv);
|
||||
if (res > 0 && FD_ISSET(sockfd, &wfd)) {
|
||||
sent = ::send(sockfd, buf + offset, size - offset, MSG_DONTWAIT);
|
||||
}
|
||||
if (sent > 0) { offset += sent; }
|
||||
else if ((res < 0) || (sent == 0) || FD_ISSET(sockfd, &efd))
|
||||
{
|
||||
if (errno == EAGAIN || errno == EWOULDBLOCK) { continue; }
|
||||
ssize_t sent = send(sockfd, buf + offset, size - offset, MSG_DONTWAIT);
|
||||
if (sent > 0) {
|
||||
offset += sent;
|
||||
} else if (sent < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) { // Buffer full
|
||||
fd_set wfd;
|
||||
FD_ZERO(&wfd);
|
||||
FD_SET(sockfd, &wfd);
|
||||
struct timeval tv = {0, 5000};
|
||||
select(sockfd + 1, NULL, &wfd, NULL, &tv);
|
||||
} else { // disconnection or a hard error
|
||||
stop();
|
||||
return false;
|
||||
}
|
||||
@@ -479,6 +481,19 @@ extern "C" {
|
||||
be_return_nil(vm);
|
||||
}
|
||||
|
||||
// tcp.set_nowait(bool) -> nil
|
||||
int32_t wc_tcpasync_set_nowait(struct bvm *vm);
|
||||
int32_t wc_tcpasync_set_nowait(struct bvm *vm) {
|
||||
int32_t argc = be_top(vm);
|
||||
if (argc >= 2 && be_isbool(vm, 2)) {
|
||||
AsyncTCPClient * tcp = wc_gettcpclientasync_p(vm);
|
||||
bool nowait = be_tobool(vm, 2);
|
||||
tcp->set_nowait(nowait);
|
||||
be_return_nil(vm);
|
||||
}
|
||||
be_raise(vm, kTypeError, nullptr);
|
||||
}
|
||||
|
||||
// tcp.available(void) -> int
|
||||
int32_t wc_tcpasync_available(struct bvm *vm);
|
||||
int32_t wc_tcpasync_available(struct bvm *vm) {
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
// callback type when a screen paint is done
|
||||
typedef void (*lv_paint_cb_t)(int32_t x1, int32_t y1, int32_t x2, int32_t y2, uint8_t *pixels);
|
||||
// callback type when a stream buffer is ready
|
||||
typedef void (*lv_stream_cb_t)(const uint8_t *buf, size_t len);
|
||||
typedef void (*lv_stream_cb_t)(const uint8_t *buf, size_t len, bool last);
|
||||
|
||||
struct LVGL_Glue {
|
||||
lv_display_t *lv_display = nullptr;
|
||||
@@ -149,7 +149,7 @@ void lv_process_stream(int32_t x, int32_t y, int32_t width, int32_t height, cons
|
||||
chunk[chunk_pos++] = val;
|
||||
if (chunk_pos >= CHUNK_SIZE) {
|
||||
chunk_pos = 0;
|
||||
lvgl_glue->stream_cb((const uint8_t *)chunk, CHUNK_SIZE * 2);
|
||||
lvgl_glue->stream_cb((const uint8_t *)chunk, CHUNK_SIZE * 2, false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -179,7 +179,7 @@ void lv_process_stream(int32_t x, int32_t y, int32_t width, int32_t height, cons
|
||||
}
|
||||
}
|
||||
if (chunk_pos > 0)
|
||||
lvgl_glue->stream_cb((const uint8_t *)chunk, chunk_pos * 2);
|
||||
lvgl_glue->stream_cb((const uint8_t *)chunk, chunk_pos * 2, true);
|
||||
}
|
||||
|
||||
/************************************************************
|
||||
|
||||
Reference in New Issue
Block a user