# LvScreenshot Board-independent **LVGL 9 screen capture** as a reusable PlatformIO library: grab the current screen into a tight **RGB565** buffer via `lv_snapshot_take()`, optionally serve it over HTTP, and save it as a PNG on your PC. Because it re-renders the LVGL object tree (not the panel framebuffer), it works with **any** display driver — partial-flush (LovyanGFX), full-frame (Arduino_GFX), or RGB panel. ## Requirements - LVGL 9 with **`LV_USE_SNAPSHOT 1`** in your `lv_conf.h` (the core warns at compile time otherwise). - **PSRAM must be enabled/available on ESP32** — see "Memory / PSRAM" below. This is essential. - The optional HTTP helper needs `ESPAsyncWebServer`; it self-disables (no-op) when that is absent. ## Memory / PSRAM (important) A full-frame snapshot is large — e.g. 320×480 RGB565 = **~300 KB**. LVGL's stock `lv_snapshot_take()` allocates that from LVGL's own heap, which with **`LV_USE_STDLIB_MALLOC = LV_STDLIB_BUILTIN`** (the default) is a small fixed pool (`LV_MEM_SIZE`). It cannot fit ~300 KB, so `lv_snapshot_take()` simply **returns null and the capture fails** — the symptom that must be worked around whenever capture goes through LVGL. This library avoids that: instead of letting LVGL allocate, it allocates the draw buffer itself from **PSRAM** (`heap_caps_malloc(MALLOC_CAP_SPIRAM)` on ESP32, plain `malloc` elsewhere) and renders into it via `lv_snapshot_take_to_draw_buf()`. Consequences for integrators: - **Enable PSRAM** in your build (e.g. `-DBOARD_HAS_PSRAM`, `board_build.arduino.memory_type` set to a `*_opi`/`*_qspi` PSRAM variant). Without a working PSRAM heap the two ~300 KB allocations (draw buffer + tight-packed copy) will fail and capture returns `false`. - You do **not** need to enlarge `LV_MEM_SIZE` or switch `LV_USE_STDLIB_MALLOC` — the library deliberately bypasses LVGL's allocator for the snapshot buffer for exactly this reason. - Peak transient usage is ~2× the frame (~600 KB) during a capture; both buffers are freed right after (the HTTP helper keeps one until the next request so it can stream it). ## Core API (`LvScreenshot.hpp`, LVGL-only) ```cpp LvScreenshot shot; if (lvgl_port_lock(-1)) { // the caller must hold the LVGL lock bool ok = lvScreenshotCaptureTop(&shot); // topmost overlay, else the active screen lvgl_port_unlock(); if (ok) { /* shot.data = RGB565, shot.width/height/length */ lvScreenshotFree(&shot); } } ``` `lvScreenshotCapture(obj, &shot)` captures a specific object. Buffers use PSRAM on ESP32. ## Optional HTTP endpoint (`LvScreenshotHttp.hpp`, needs ESPAsyncWebServer) ```cpp #include // in your web-server setup: lvScreenshotAttachHttp(www, lvgl_port_lock, lvgl_port_unlock); // registers GET /screenshot ``` The route returns the raw RGB565 body plus `X-Width` / `X-Height` / `X-Format` headers. ## PC tool ```bash uv run tools/screenshot.py --host diystickynotes.local --out shot.png # --swap-rb if colours look inverted (BGR panel) # --byteswap if the image is garbled (big-endian RGB565) ``` Converts the RGB565 response to a PNG (numpy + Pillow; deps auto-installed via uv / PEP 723). ## Gating it to dev builds (recommended) Keep production lean: flip `LV_USE_SNAPSHOT` on only under a build flag, and only call `lvScreenshotAttachHttp` under the same flag, so normal builds are byte-identical and expose no endpoint. See the DIYStickyNotes `[env:screenshot_*]` envs for an example. ## License MIT (code). Screenshots you capture are your own content.