Updated LVGL Panel using binary stream (#24436)

* updated lvgl panel using binary stream

* fixed partial buffer write using optional offset and len

* using async client and tcp write with offset
This commit is contained in:
Milko Daskalov
2026-02-15 10:38:21 +01:00
committed by GitHub
parent 44ed6da7cf
commit afcfe19fc6
6 changed files with 309 additions and 1045 deletions
Binary file not shown.
@@ -1,6 +1,6 @@
# rm LVGL_Panel.tapp; zip -j -0 LVGL_Panel.tapp LVGL_Panel/autoexec.be LVGL_Panel/lvgl_panel.be LVGL_Panel/manifest.json
# rm LVGL_Panel.tapp; zip -j -0 LVGL_Panel.tapp LVGL_Panel/autoexec.be LVGL_Panel/lvgl_panel.be LVGL_Panel/lvgl.html LVGL_Panel/manifest.json
do # embed in `do` so we don't add anything to global namespace
import introspect
var leds_panel = introspect.module('lvgl_panel', true) # load module but don't cache
tasmota.add_extension(leds_panel)
var lvgl_panel = introspect.module('lvgl_panel', true) # load module but don't cache
tasmota.add_extension(lvgl_panel)
end
@@ -0,0 +1,187 @@
<style>
.border {
display: none;
margin-top: 5px;
padding: 1px;
border: 1px solid var(--c_txt);
background-color: black;
box-sizing: border-box;
}
#canvas {
display: block;
width: 100%;
height: 100%;
object-fit: contain;
image-rendering: pixelated;
}
</style>
<table style="width: 100%;">
<tbody>
<tr>
<td>
<button id="lvBtn">LVGL screen mirroring</button>
<div class="border">
<canvas id="canvas"></canvas>
</div>
</td>
</tr>
</tbody>
</table>
<script>
const HEADER_BYTES = 10; // 10 bytes: 2(LV) + 2(x) + 2(y) + 2(w) + 2(h)
const RGB565_LUT = new Uint32Array(65536);
const lvBtn = document.getElementById('lvBtn');
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext("2d");
let abortController = null;
let isStreaming = false;
let imageData, argb, sharedBuffer, buf8, buf16;
// RGB565 to ARGB8888 (Little Endian: AABBGGRR) using bit replication
for (let p = 0; p < 0x10000; p++) {
RGB565_LUT[p] = 0xFF000000 | // Alpha (opaque)
(((p & 0xF800) >>> 8) | ((p & 0xE000) >>> 13)) | // R 5-bit abcde -> abcdeabc
(((p & 0x07E0) << 5) | ((p & 0x0600) >>> 1)) | // G 6-bit abcdef -> abcdefab
(((p & 0x001F) << 19) | ((p & 0x001C) << 14)); // B 5-bit abcde -> abcdeabc
}
async function startStream(url) {
const border = canvas.parentElement;
border.style.display = 'block';
abortController = new AbortController();
var width, height;
try {
const response = await fetch(url, { signal: abortController.signal });
const screenSize = response.headers.get('Screen-Size');
if (screenSize) {
[width, height] = screenSize.split('x').map(Number);
if (!imageData) {
const parentWidth = border.parentElement.clientWidth;
border.style.width = parentWidth + 'px';
border.style.height = (parentWidth * height / width) + 'px';
canvas.width = width;
canvas.height = height;
imageData = ctx.createImageData(width, height);
argb = new Uint32Array(imageData.data.buffer);
sharedBuffer = new ArrayBuffer(HEADER_BYTES + width * height * 2);
buf8 = new Uint8Array(sharedBuffer);
buf16 = new Uint16Array(sharedBuffer);
}
isStreaming = true;
} else {
console.error('No screen size header');
stopStream();
return;
}
const reader = response.body.getReader();
let bufEnd = 0;
let updateBytes, magic, x, y, w, h;
let sIdx, dIdx, dInc;
let rowBytes, rowsRemaining;
while (isStreaming) {
const { value, done } = await reader.read();
if (done) break;
buf8.set(value, bufEnd);
bufEnd += value.length;
if (!updateBytes) {
if (bufEnd < HEADER_BYTES) break;
[magic, x, y, w, h] = buf16
if (magic !== 0x4C56) { // LV
console.error("No magic found");
stopStream();
return;
}
updateBytes = HEADER_BYTES + w * h * 2;
sIdx = HEADER_BYTES / 2;
dIdx = x + y * width;
dInc = width - w;
rowBytes = w * 2;
rowsRemaining = h;
}
const pixelData = bufEnd - sIdx * 2;
const rowsToProcess = Math.min(Math.floor(pixelData / rowBytes), rowsRemaining);
if (rowsToProcess === 0) continue;
for (let r = 0; r < rowsToProcess; r++, dIdx += dInc) {
for (let c = 0; c < w; c++) {
argb[dIdx++] = RGB565_LUT[buf16[sIdx++]];
}
}
rowsRemaining -= rowsToProcess;
if (rowsRemaining > 0) continue;
ctx.putImageData(imageData, 0, 0, x, y, w, h);
bufEnd -= updateBytes;
if (bufEnd > 0) {
buf8.copyWithin(0, updateBytes, updateBytes + bufEnd);
}
updateBytes = undefined;
}
} catch (err) {
if (err.name !== 'AbortError') console.error(err);
} finally {
stopStream();
}
}
function stopStream() {
canvas.parentElement.style.display = 'none';
isStreaming = false;
if (abortController) {
abortController.abort();
}
if (imageData) {
argb.fill(0);
ctx.putImageData(imageData, 0, 0);
imageData = undefined
}
}
function getCanvasCoordinates(event) {
const rect = canvas.getBoundingClientRect();
const x = Math.round((event.clientX - rect.left) * (canvas.width / rect.width));
const y = Math.round((event.clientY - rect.top) * (canvas.height / rect.height));
return { x, y };
}
canvas.addEventListener("mousedown", (e) => {
e.preventDefault();
const host = getHost();
fetch(`http://${host}/lvgl_touch`, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams(getCanvasCoordinates(e)).toString()
}).catch(err => {
console.error("Touch event error:", err);
});
});
lvBtn.addEventListener('click', (e) => {
const host = getHost();
isStreaming ? stopStream() : startStream(`http://${host}:8881`);
});
function getHost() {
return window.location.hostname || 'localhost';
}
</script>
File diff suppressed because it is too large Load Diff
@@ -1,8 +1,8 @@
{
"name": "LVGL Panel",
"version": "0x190A0100",
"description": "Realtime display of the LVGL display in browser",
"author": "Stephan Hadinger",
"version": "0x1A020200",
"description": "Realtime mirror of the LVGL display in browser",
"author": "Stephan Hadinger & Milko Daskalov",
"min_tasmota": "0x0F010002",
"features": "display"
}
@@ -252,7 +252,7 @@ public:
}
return false;
}
size_t write(const char *buffer, size_t size) {
_update_connected();
if (state == AsyncTCPState::CONNECTED) {
@@ -263,7 +263,7 @@ public:
FD_SET(sockfd, &set); // adds FD to the set
tv.tv_sec = 0;
tv.tv_usec = 0; // non-blocking
if (select(sockfd + 1, NULL, &set, NULL, &tv) < 0) {
return 0; // error TODO close connection?
}
@@ -536,9 +536,7 @@ extern "C" {
else if (offset + len > buf_len) { len = buf_len - offset; } // len is too long, adjust
if (len > 0) {
// now adjust the buffer with offset
buf_len += offset;
size_t bw = tcp->write(buf, buf_len);
size_t bw = tcp->write(buf + offset, len);
be_pushint(vm, bw);
} else {
be_pushint(vm, 0); // nothing to send