#include "lv_setup.hpp" #include "esp_heap_caps.h" #include "esp_log.h" #include "esp_timer.h" #include "freertos/task.h" #include "types.hpp" internal const char *kTagLvgl = "LVGL"; SemaphoreHandle_t g_LvglMutex = nullptr; lv_display_t *g_LvglDisplay = nullptr; uint8_t *g_LvglDrawBuffer = nullptr; internal void lvgl_tick_task(void *arg) { while (true) { vTaskDelay(pdMS_TO_TICKS(10)); if (xSemaphoreTake(g_LvglMutex, portMAX_DELAY) == pdTRUE) { lv_timer_handler(); xSemaphoreGive(g_LvglMutex); } } } internal uint32_t my_tick_get_cb() { return (uint32_t)(esp_timer_get_time() / 1000); } internal void lv_dummy_flush_cb(lv_display_t *disp, const lv_area_t *area, uint8_t *px_map) { // Headless display, so we don't actually flush to SPI/I2C. // We just tell LVGL that the "flush" is completed so it unblocks wait_for_flushing. lv_display_flush_ready(disp); } internal void lv_draw_sample_ui() { lv_obj_t *scr = lv_screen_active(); // Default background to white for the grayscale PNG lv_obj_set_style_bg_color(scr, lv_color_white(), 0); lv_obj_t *label = lv_label_create(scr); lv_label_set_text(label, "Calendink Provider\nLVGL Headless Renderer"); lv_obj_align(label, LV_ALIGN_CENTER, 0, 0); } void setup_lvgl() { ESP_LOGI(kTagLvgl, "Initializing LVGL"); g_LvglMutex = xSemaphoreCreateMutex(); lv_init(); lv_tick_set_cb(my_tick_get_cb); uint32_t width = CONFIG_CALENDINK_DISPLAY_WIDTH; uint32_t height = CONFIG_CALENDINK_DISPLAY_HEIGHT; // Create a virtual display g_LvglDisplay = lv_display_create(width, height); lv_display_set_flush_cb(g_LvglDisplay, lv_dummy_flush_cb); // Initialize LodePNG custom bump allocator lodepng_allocator_init(); // Allocate draw buffers in PSRAM // Using LV_COLOR_FORMAT_L8 (1 byte per pixel) size_t buf_size = LV_DRAW_BUF_SIZE(width, height, LV_COLOR_FORMAT_L8); // Fallback to MALLOC_CAP_DEFAULT if we can't get SPIRAM (for debugging without it) void *buf1 = heap_caps_malloc(buf_size, MALLOC_CAP_SPIRAM); if (!buf1) { ESP_LOGW(kTagLvgl, "Failed to allocate LVGL draw buffer in PSRAM, falling back to internal RAM"); buf1 = heap_caps_malloc(buf_size, MALLOC_CAP_DEFAULT); } if (!buf1) { ESP_LOGE(kTagLvgl, "Failed to allocate LVGL draw buffer entirely."); return; } g_LvglDrawBuffer = (uint8_t *)buf1; lv_display_set_buffers(g_LvglDisplay, buf1, nullptr, buf_size, LV_DISPLAY_RENDER_MODE_FULL); // Explicitly set the color format of the display if it's set in sdkconfig/driver lv_display_set_color_format(g_LvglDisplay, LV_COLOR_FORMAT_L8); // Create the background task for the LVGL timer xTaskCreate(lvgl_tick_task, "LVGL Tick", 4096, nullptr, 5, nullptr); // Draw the sample UI if (xSemaphoreTake(g_LvglMutex, portMAX_DELAY) == pdTRUE) { lv_draw_sample_ui(); xSemaphoreGive(g_LvglMutex); } ESP_LOGI(kTagLvgl, "LVGL fully initialized. Display %lux%lu created.", width, height); }