1 Commits
Author SHA1 Message Date
pablo2048andClaude Opus 4.8 640f356754 feat: RGB-panel framebuffer-read path (lvScreenshotAttachHttpRgb) v1.1.0
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 16:50:09 +02:00
5 changed files with 86 additions and 3 deletions
+1
View File
@@ -1,3 +1,4 @@
LvScreenshot KEYWORD1
lvScreenshotTake KEYWORD2
lvScreenshotAttachHttp KEYWORD2
lvScreenshotAttachHttpRgb KEYWORD2
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "LvScreenshot",
"version": "1.0.0",
"version": "1.1.0",
"description": "Board-independent LVGL 9 screen capture (lv_snapshot_take -> tight RGB565) plus an optional AsyncWebServer /screenshot endpoint and a PC tool that saves a PNG.",
"keywords": "lvgl, screenshot, snapshot, capture, png, esp32",
"frameworks": "arduino",
+1 -1
View File
@@ -1,5 +1,5 @@
name=LvScreenshot
version=1.0.0
version=1.1.0
author=xPablo.cz
maintainer=xPablo.cz <https://git.xpablo.cz/xPablo.cz>
sentence=Board-independent LVGL 9 screen capture with an optional /screenshot web endpoint.
+60 -1
View File
@@ -11,6 +11,8 @@
#include <ESPAsyncWebServer.h>
#include "LvScreenshot.hpp"
#include <cstring>
#include <esp_lcd_panel_rgb.h>
#include <esp_heap_caps.h>
namespace {
// One shared capture buffer: the async response streams from it over ~1 s, so it must outlive the
@@ -57,7 +59,64 @@ void lvScreenshotAttachHttp(AsyncWebServer * www, LvScreenshotLockFn lock,
req->send(resp);
});
}
// ---- RGB-panel path: read the scan-out framebuffer directly (no lv_snapshot re-render) ----
namespace {
esp_lcd_panel_handle_t g_rgbPanel = nullptr;
uint8_t * g_rgbSnap = nullptr; // reused PSRAM copy; streamed until the next request
size_t g_rgbLen = 0;
int g_rgbW = 0;
int g_rgbH = 0;
LvScreenshotLockFn g_rgbLock = nullptr;
LvScreenshotUnlockFn g_rgbUnlock = nullptr;
}
void lvScreenshotAttachHttpRgb(AsyncWebServer * www, esp_lcd_panel_handle_t panelHandle,
int width, int height, LvScreenshotLockFn lock,
LvScreenshotUnlockFn unlock, const char * path) {
if (!www) return;
g_rgbPanel = panelHandle;
g_rgbW = width;
g_rgbH = height;
g_rgbLen = (size_t) width * (size_t) height * 2; // RGB565
g_rgbLock = lock;
g_rgbUnlock = unlock;
www->on(path ? path : "/screenshot", HTTP_GET, [](AsyncWebServerRequest * req) {
void * fb = nullptr;
const esp_err_t err = g_rgbPanel ? esp_lcd_rgb_panel_get_frame_buffer(g_rgbPanel, 1, &fb) : ESP_FAIL;
if (err != ESP_OK || !fb) { req->send(500, "text/plain", "screenshot: no framebuffer"); return; }
if (!g_rgbSnap) {
g_rgbSnap = static_cast<uint8_t *>(heap_caps_malloc(g_rgbLen, MALLOC_CAP_SPIRAM));
if (!g_rgbSnap) g_rgbSnap = static_cast<uint8_t *>(heap_caps_malloc(g_rgbLen, MALLOC_CAP_8BIT));
}
if (!g_rgbSnap) { req->send(500, "text/plain", "screenshot: alloc failed"); return; }
// Copy a single consistent frame under the LVGL lock (don't capture mid-flush).
if (g_rgbLock && g_rgbLock(-1)) {
std::memcpy(g_rgbSnap, fb, g_rgbLen);
if (g_rgbUnlock) g_rgbUnlock();
} else {
std::memcpy(g_rgbSnap, fb, g_rgbLen); // lock unavailable: best-effort
}
AsyncWebServerResponse * resp = req->beginResponse(
"application/octet-stream", g_rgbLen,
[](uint8_t * out, size_t maxLen, size_t index) -> size_t {
const size_t remaining = g_rgbLen - index;
const size_t n = remaining < maxLen ? remaining : maxLen;
std::memcpy(out, g_rgbSnap + index, n);
return n;
});
resp->addHeader("X-Width", String(g_rgbW));
resp->addHeader("X-Height", String(g_rgbH));
resp->addHeader("X-Format", "RGB565");
req->send(resp);
});
}
#else
// ESPAsyncWebServer not available in this project: no-op so the core LvScreenshot still builds.
// ESPAsyncWebServer not available in this project: no-ops so the core LvScreenshot still builds.
void lvScreenshotAttachHttp(AsyncWebServer *, LvScreenshotLockFn, LvScreenshotUnlockFn, const char *) {}
void lvScreenshotAttachHttpRgb(AsyncWebServer *, esp_lcd_panel_handle_t, int, int,
LvScreenshotLockFn, LvScreenshotUnlockFn, const char *) {}
#endif
+23
View File
@@ -25,3 +25,26 @@ typedef bool (*LvScreenshotUnlockFn)(void);
*/
void lvScreenshotAttachHttp(AsyncWebServer * www, LvScreenshotLockFn lock,
LvScreenshotUnlockFn unlock, const char * path = "/screenshot");
// esp_lcd panel handle (matches the ESP-IDF typedef in <esp_lcd_types.h>). Forward-declared so this
// header stays lightweight; the .cpp includes the real ESP-IDF headers.
typedef struct esp_lcd_panel_t * esp_lcd_panel_handle_t;
/**
* @brief RGB-panel variant of the /screenshot route. Instead of re-rendering the UI via lv_snapshot
* (which allocates a fresh width*height*2 buffer), this reads the RGB panel's scan-out
* framebuffer DIRECTLY — the pixels already on screen with every LVGL layer composited. On
* ESP32-S3 RGB panels this is both lighter on PSRAM (no extra render buffer; the framebuffer
* already exists) and captures exactly what is displayed, including overlays on lv_layer_top().
* Prefer this over lvScreenshotAttachHttp() whenever the display is an RGB panel.
* @param www the AsyncWebServer to attach to.
* @param panelHandle the RGB LCD panel handle (e.g. board->getLCD()->getRefreshPanelHandle()).
* @param width,height panel dimensions in pixels.
* @param lock/unlock LVGL-lock hooks; the framebuffer is copied while locked (not mid-flush).
* @param path route path (default "/screenshot").
*
* No-op if ESPAsyncWebServer is not available.
*/
void lvScreenshotAttachHttpRgb(AsyncWebServer * www, esp_lcd_panel_handle_t panelHandle,
int width, int height, LvScreenshotLockFn lock,
LvScreenshotUnlockFn unlock, const char * path = "/screenshot");