# LVGL Image Generation Architecture **Authored by Antigravity** **Date:** 2026-03-14 --- ## 1. Goal Integrate the Light and Versatile Graphics Library (LVGL) into the Calendink Provider to generate UI images server-side. The ESP32-S3 will render screens into a memory buffer and serve the resulting image (PNG) directly over the HTTP REST API to clients. This allows the Provider to act as a "smart renderer" for dump clients like e-ink displays or web widgets that only have the capability to fetch and display static images. The display resolution (default 800x480) and color depth (4-level grayscale) are parameterized via Kconfig to support future hardware changes. ## 2. Chosen Approach ### 2.1. Headless LVGL Display Driver LVGL is typically used to drive physical displays via SPI/I2C/RGB interfaces. For this use case, we will configure a **virtual (headless) display**: - A display instance (`lv_display_t`) will be created without a physical flush callback (or a dummy one that does nothing). - We will allocate a full-screen frame buffer (e.g., 800x480 at 8-bit grayscale). While small enough to fit in SRAM (~384 KB), we will use **PSRAM** to avoid memory pressure on the system. - When an image is requested, we will force LVGL to update the screen (`lv_refr_now()`) and then read the raw pixel data directly from the draw buffer. ### 2.2. Image Encoding (PNG) Raw pixel data is not web-friendly. To serve the image over HTTP, we will encode it as a **PNG** image. - **Why PNG?** PNG is smaller over the network than BMP and natively supports lossless grayscale compression. Given the target of 4-level grayscale, a PNG will compress exceptionally well. - **Encoder:** We will use `lodepng`, a lightweight single-file C library, to compress the raw LVGL buffer into a PNG on the fly. - **Color Depth:** The target is **4-level grayscale**. We will map LVGL's output to a 2-bit grayscale PNG to minimize the payload size. - **Parameterization:** Resolution (e.g., 800 width, 480 height) is configurable via Kconfig (`CONFIG_DISPLAY_WIDTH`, `CONFIG_DISPLAY_HEIGHT`) so it can be easily changed for different e-ink displays. ### 2.3. Thread Safety and Concurrency LVGL is **not thread-safe**. Since the HTTP server runs its API handlers in different FreeRTOS tasks (from the `httpd` thread pool), we must protect LVGL with a **Mutex**. - All LVGL initialization, UI setup (`lv_obj_create`, etc.), and the periodic `lv_timer_handler()` will take the `g_LvglMutex`. - The `GET /api/display/image` API endpoint will acquire `g_LvglMutex`, draw the screen, encode the BMP header, transmit the buffer, and then release the mutex. ## 3. Architecture ### 3.1. File Structure ``` Provider/main/ ├── lv_setup.hpp # LVGL initialization and mutex declarations ├── lv_setup.cpp # Driver setup, PSRAM buffer allocation, LVGL tick task ├── api/display/ │ ├── image.cpp # GET /api/display/image handler (BMP streamer) │ └── unity.cpp # Aggregator for display endpoints ├── http_server.cpp # Modified to include api/display/unity.cpp └── main.cpp # Starts LVGL task before HTTP server ``` ### 3.2. Data Flow 1. **Client** makes a `GET /api/display/image` request. 2. **`image.cpp` handler** intercepts the request and takes the `g_LvglMutex`. 3. The handler forces LVGL to finish any pending rendering. 4. The handler uses `lodepng` to compress the raw frame buffer into a PNG payload. 5. The handler sends the PNG via `httpd_resp_send()`. 6. The handler frees the PNG payload buffer and releases `g_LvglMutex`. 7. The HTTP response completes. ## 4. Design Decisions & Trade-offs | Decision | Trade-off | Rationale | |---|---|---| | **PSRAM Allocation** | Slower access than SRAM | A full framebuffer for high-res displays exceeds internal SRAM. PSRAM is required, and since we only read/write it occasionally for HTTP, the speed penalty is negligible compared to network latency. | | **PNG over BMP** | More CPU overhead | PNG compression takes CPU cycles and temporary RAM, but significantly reduces network payload, which is better for web clients and saves transmission time. | | **Pull vs Push** | Requires polling | The client must actively request the image. This is standard for web, but means the client won't know instantly if the UI state changes unless using WebSockets/SSE (future scope). | | **Synchronous Render** | Blocks HTTP task | The HTTP handler waits for LVGL to finish drawing and PNG encoding to complete. Since LVGL renders very quickly in memory, this block should be acceptable. | ## 5. Next Steps 1. Add `lvgl/lvgl` to `idf_component.yml`. 2. Configure LVGL in `sdkconfig` to use custom memory allocation (routed to PSRAM). 3. Implement `lv_setup.cpp` and `api/display/image.cpp`. 4. Draw a sample UI on the virtual display. --- *Created by Antigravity - Last Updated: 2026-03-14*