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:
Milko Daskalov
2026-03-14 15:42:09 +01:00
committed by GitHub
parent 3d57fe57f2
commit ff0609d932
7 changed files with 105 additions and 86 deletions
Binary file not shown.
+63 -62
View File
@@ -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) {
+3 -3
View File
@@ -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);
}
/************************************************************