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)

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)

#include <LvScreenshotHttp.hpp>
// 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

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).

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.

S
Description
Board-independent LVGL 9 screen capture (lv_snapshot -> RGB565) with an optional AsyncWebServer /screenshot endpoint and a PC PNG tool.
Readme MIT
39 KiB
Languages
C++ 79.9%
Python 20.1%