diff --git a/keywords.txt b/keywords.txt index d81ce5f..58efb92 100644 --- a/keywords.txt +++ b/keywords.txt @@ -1,3 +1,4 @@ LvScreenshot KEYWORD1 lvScreenshotTake KEYWORD2 lvScreenshotAttachHttp KEYWORD2 +lvScreenshotAttachHttpRgb KEYWORD2 diff --git a/library.json b/library.json index d368fde..43d2ad2 100644 --- a/library.json +++ b/library.json @@ -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", diff --git a/library.properties b/library.properties index c3379c2..1273304 100644 --- a/library.properties +++ b/library.properties @@ -1,5 +1,5 @@ name=LvScreenshot -version=1.0.0 +version=1.1.0 author=xPablo.cz maintainer=xPablo.cz sentence=Board-independent LVGL 9 screen capture with an optional /screenshot web endpoint. diff --git a/src/LvScreenshotHttp.cpp b/src/LvScreenshotHttp.cpp index 684715c..3b0fc76 100644 --- a/src/LvScreenshotHttp.cpp +++ b/src/LvScreenshotHttp.cpp @@ -11,6 +11,8 @@ #include #include "LvScreenshot.hpp" #include +#include +#include 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(heap_caps_malloc(g_rgbLen, MALLOC_CAP_SPIRAM)); + if (!g_rgbSnap) g_rgbSnap = static_cast(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 diff --git a/src/LvScreenshotHttp.hpp b/src/LvScreenshotHttp.hpp index 439cfe8..76add3a 100644 --- a/src/LvScreenshotHttp.hpp +++ b/src/LvScreenshotHttp.hpp @@ -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 ). 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");