diff --git a/Provider/main/api/devices/screen_image.cpp b/Provider/main/api/devices/screen_image.cpp index 4a61a64..eaae90e 100644 --- a/Provider/main/api/devices/screen_image.cpp +++ b/Provider/main/api/devices/screen_image.cpp @@ -91,7 +91,8 @@ internal esp_err_t api_devices_screen_image_handler(httpd_req_t *req) // 1. Prepare the XML payload const char *xml_to_register = NULL; - static char xml_buffer[DEVICE_XML_MAX + 100]; // static buffer to avoid stack overflow + static char + xml_buffer[DEVICE_XML_MAX + 100]; // static buffer to avoid stack overflow if (dev->xml_layout[0] == '\0') { @@ -106,16 +107,6 @@ internal esp_err_t api_devices_screen_image_handler(httpd_req_t *req) ESP_LOGI(kTagDeviceScreenImage, "XML already contains , passing directly to parser."); } - else - { - // Backwards compatibility for early setups - wrap it in screen and view - snprintf(xml_buffer, sizeof(xml_buffer), - "\n\n%s\n\n", - dev->xml_layout); - xml_to_register = xml_buffer; - ESP_LOGI(kTagDeviceScreenImage, - "Legacy XML without detected. Wrapped automatically."); - } // 2. Register the XML payload as a component lv_result_t res = @@ -131,7 +122,8 @@ internal esp_err_t api_devices_screen_image_handler(httpd_req_t *req) if (new_scr) { - // We must load this newly created screen to make it active before rendering + // We must load this newly created screen to make it active before + // rendering lv_screen_load(new_scr); scr = new_scr; // Update local pointer since active screen changed render_success = true; @@ -175,14 +167,13 @@ internal esp_err_t api_devices_screen_image_handler(httpd_req_t *req) uint32_t width = CONFIG_CALENDINK_DISPLAY_WIDTH; uint32_t height = CONFIG_CALENDINK_DISPLAY_HEIGHT; - // Handle stride != width - uint8_t *packed_data = (uint8_t *)draw_buf->data; - bool needs_free = false; - - if (draw_buf->header.stride != width) + // Allocate bounding memory for quantizing RGB565 buffer into tightly packed + // 8-bit PNG data. + uint8_t *packed_data = + (uint8_t *)heap_caps_malloc(width * height, MALLOC_CAP_SPIRAM); + if (!packed_data) { - packed_data = - (uint8_t *)heap_caps_malloc(width * height, MALLOC_CAP_SPIRAM); + packed_data = (uint8_t *)malloc(width * height); if (!packed_data) { xSemaphoreGive(g_LvglMutex); @@ -191,11 +182,33 @@ internal esp_err_t api_devices_screen_image_handler(httpd_req_t *req) "Out of memory"); return ESP_FAIL; } - needs_free = true; - for (uint32_t y = 0; y < height; ++y) + } + + // LVGL renders into RGB565 (2 bytes per pixel). + // Parse pixels, extract luminance, and quantize to 4 levels (0, 85, 170, 255). + for (uint32_t y = 0; y < height; ++y) + { + const uint16_t *src_row = (const uint16_t *)((const uint8_t *)draw_buf->data + (y * draw_buf->header.stride)); + uint8_t *dst_row = packed_data + (y * width); + + for (uint32_t x = 0; x < width; ++x) { - memcpy(packed_data + (y * width), - (uint8_t *)draw_buf->data + (y * draw_buf->header.stride), width); + uint16_t c = src_row[x]; + // Expand 5/6/5 components + uint8_t r_5 = (c >> 11) & 0x1F; + uint8_t g_6 = (c >> 5) & 0x3F; + uint8_t b_5 = c & 0x1F; + + // Unpack to 8-bit true values + uint8_t r = (r_5 << 3) | (r_5 >> 2); + uint8_t g = (g_6 << 2) | (g_6 >> 4); + uint8_t b = (b_5 << 3) | (b_5 >> 2); + + // Simple luminance + uint8_t lum = (r * 77 + g * 150 + b * 29) >> 8; + + // 4-level linear quantization (0, 85, 170, 255) + dst_row[x] = (lum >> 6) * 85; } } @@ -210,10 +223,7 @@ internal esp_err_t api_devices_screen_image_handler(httpd_req_t *req) unsigned error = lodepng_encode_memory(&png, &pngsize, packed_data, width, height, LCT_GREY, 8); - if (needs_free) - { - free(packed_data); - } + free(packed_data); xSemaphoreGive(g_LvglMutex); diff --git a/Provider/main/api/display/image.cpp b/Provider/main/api/display/image.cpp index e9d08c3..9997b3e 100644 --- a/Provider/main/api/display/image.cpp +++ b/Provider/main/api/display/image.cpp @@ -1,113 +1,145 @@ -#include "../../lv_setup.hpp" #include "../../lodepng/lodepng.h" +#include "../../lodepng_alloc.hpp" +#include "../../lv_setup.hpp" +#include "esp_heap_caps.h" #include "esp_http_server.h" #include "esp_log.h" -#include "esp_heap_caps.h" #include "esp_random.h" #include "lvgl.h" #include -#include "../../lodepng_alloc.hpp" internal const char *kTagDisplayImage = "API_DISPLAY_IMAGE"; internal esp_err_t api_display_image_handler(httpd_req_t *req) { - httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*"); - - // We are generating PNG on the fly, don't let it be cached locally immediately - httpd_resp_set_hdr(req, "Cache-Control", "no-cache, no-store, must-revalidate"); - httpd_resp_set_type(req, "image/png"); + httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*"); - if (xSemaphoreTake(g_LvglMutex, pdMS_TO_TICKS(5000)) != pdTRUE) - { - ESP_LOGE(kTagDisplayImage, "Failed to get LVGL mutex"); - httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "LVGL busy"); - return ESP_FAIL; - } + // We are generating PNG on the fly, don't let it be cached locally + // immediately + httpd_resp_set_hdr(req, "Cache-Control", + "no-cache, no-store, must-revalidate"); + httpd_resp_set_type(req, "image/png"); - // Change the background color securely to a random grayscale value - // esp_random() returns 32 bits, we just take the lowest 8. - uint8_t rand_gray = esp_random() & 0xFF; - lv_obj_t* active_screen = lv_screen_active(); - lv_obj_set_style_bg_color(active_screen, lv_color_make(rand_gray, rand_gray, rand_gray), LV_PART_MAIN); + if (xSemaphoreTake(g_LvglMutex, pdMS_TO_TICKS(5000)) != pdTRUE) + { + ESP_LOGE(kTagDisplayImage, "Failed to get LVGL mutex"); + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "LVGL busy"); + return ESP_FAIL; + } - // Force a screen refresh to get the latest rendered frame - lv_refr_now(g_LvglDisplay); + // Change the background color securely to a random grayscale value + // esp_random() returns 32 bits, we just take the lowest 8. + uint8_t rand_gray = esp_random() & 0xFF; + lv_obj_t *active_screen = lv_screen_active(); + // lv_obj_set_style_bg_color(active_screen, lv_color_make(rand_gray, + // rand_gray, rand_gray), LV_PART_MAIN); - lv_draw_buf_t *draw_buf = lv_display_get_buf_active(g_LvglDisplay); - if (!draw_buf) - { - xSemaphoreGive(g_LvglMutex); - ESP_LOGE(kTagDisplayImage, "No active draw buffer available"); - httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Display uninitialized"); - return ESP_FAIL; - } - - uint32_t width = CONFIG_CALENDINK_DISPLAY_WIDTH; - uint32_t height = CONFIG_CALENDINK_DISPLAY_HEIGHT; - - // LodePNG expects tightly packed data without stride padding. - // Ensure we copy the data if stride differs from width. - uint8_t *packed_data = (uint8_t *)draw_buf->data; - bool needs_free = false; - - if (draw_buf->header.stride != width) - { - ESP_LOGI(kTagDisplayImage, "Stride %lu differs from width %lu. Repacking buffer...", draw_buf->header.stride, width); - packed_data = (uint8_t *)heap_caps_malloc(width * height, MALLOC_CAP_SPIRAM); - if (!packed_data) - { - xSemaphoreGive(g_LvglMutex); - ESP_LOGE(kTagDisplayImage, "Failed to allocate packed buffer in PSRAM"); - httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Out of memory"); - return ESP_FAIL; - } - needs_free = true; - - for (uint32_t y = 0; y < height; ++y) - { - memcpy(packed_data + (y * width), (uint8_t *)draw_buf->data + (y * draw_buf->header.stride), width); - } - } - - // Convert LVGL 8-bit L8 buffer to 8-bit grayscale PNG using LodePNG. - // LCT_GREY = 0, bitdepth = 8 - unsigned char *png = nullptr; - size_t pngsize = 0; - - // We are about to start a huge memory operation inside LodePNG. - // We reset our 2MB PSRAM bump allocator to 0 bytes used. - lodepng_allocator_reset(); - - ESP_LOGI(kTagDisplayImage, "Encoding %lux%lu frame to PNG...", width, height); - unsigned error = lodepng_encode_memory(&png, &pngsize, packed_data, width, height, LCT_GREY, 8); - - if (needs_free) - { - free(packed_data); - } + // Force a screen refresh to get the latest rendered frame + lv_refr_now(g_LvglDisplay); + lv_draw_buf_t *draw_buf = lv_display_get_buf_active(g_LvglDisplay); + if (!draw_buf) + { xSemaphoreGive(g_LvglMutex); + ESP_LOGE(kTagDisplayImage, "No active draw buffer available"); + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, + "Display uninitialized"); + return ESP_FAIL; + } - if (error) + uint32_t width = CONFIG_CALENDINK_DISPLAY_WIDTH; + uint32_t height = CONFIG_CALENDINK_DISPLAY_HEIGHT; + + // We allocate a new buffer for the tightly packed 8-bit PNG grayscale data. + // Converting RGB565 frame to 4-level grayscale (quantized to 0, 85, 170, 255). + uint8_t *packed_data = + (uint8_t *)heap_caps_malloc(width * height, MALLOC_CAP_SPIRAM); + if (!packed_data) + { + packed_data = (uint8_t *)malloc(width * height); // Fallback + if (!packed_data) { - ESP_LOGE(kTagDisplayImage, "PNG encoding error %u: %s", error, lodepng_error_text(error)); - httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "PNG generation failed"); - return ESP_FAIL; + xSemaphoreGive(g_LvglMutex); + ESP_LOGE(kTagDisplayImage, "Failed to allocate packed buffer"); + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, + "Out of memory"); + return ESP_FAIL; } + } - ESP_LOGI(kTagDisplayImage, "Prepared PNG, size: %zu bytes. Sending to client...", pngsize); - esp_err_t res = httpd_resp_send(req, (const char *)png, pngsize); + // LVGL renders into RGB565 (2 bytes per pixel). + // Iterating to create an 8-bit grayscale PNG using 4 specific values. + for (uint32_t y = 0; y < height; ++y) + { + const uint16_t *src_row = + (const uint16_t *)((const uint8_t *)draw_buf->data + + (y * draw_buf->header.stride)); + uint8_t *dst_row = packed_data + (y * width); - // No need to free(png) because it is managed by our bump allocator - // which automatically resets the entire 2MB buffer to 0 next time - // lodepng_allocator_reset() is called. + for (uint32_t x = 0; x < width; ++x) + { + uint16_t c = src_row[x]; + // Note: LVGL may use swapped bytes for SPI rendering depending on config, + // but in memory RGB565 is standard if no SWAP is active. Usually standard + // RGB565 format: R(5) G(6) B(5) + uint8_t r_5 = (c >> 11) & 0x1F; + uint8_t g_6 = (c >> 5) & 0x3F; + uint8_t b_5 = c & 0x1F; - return res; + // Expand to 8 bits + uint8_t r = (r_5 << 3) | (r_5 >> 2); + uint8_t g = (g_6 << 2) | (g_6 >> 4); + uint8_t b = (b_5 << 3) | (b_5 >> 2); + + // Simple luminance calculation (fast) + uint8_t lum = (r * 77 + g * 150 + b * 29) >> 8; + + // Quantize to 4 levels (0..3) + uint8_t level = lum >> 6; + + // Expand level back to 8-bit for PNG: 0, 85, 170, 255 + dst_row[x] = level * 85; + } + } + + // Convert LVGL 8-bit L8 buffer to 8-bit grayscale PNG using LodePNG. + // LCT_GREY = 0, bitdepth = 8 + unsigned char *png = nullptr; + size_t pngsize = 0; + + // We are about to start a huge memory operation inside LodePNG. + // We reset our 3MB PSRAM bump allocator to 0 bytes used. + lodepng_allocator_reset(); + + ESP_LOGI(kTagDisplayImage, "Encoding %lux%lu frame to PNG...", width, height); + unsigned error = lodepng_encode_memory(&png, &pngsize, packed_data, width, + height, LCT_GREY, 8); + + free(packed_data); + + xSemaphoreGive(g_LvglMutex); + + if (error) + { + ESP_LOGE(kTagDisplayImage, "PNG encoding error %u: %s", error, + lodepng_error_text(error)); + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, + "PNG generation failed"); + return ESP_FAIL; + } + + ESP_LOGI(kTagDisplayImage, + "Prepared PNG, size: %zu bytes. Sending to client...", pngsize); + esp_err_t res = httpd_resp_send(req, (const char *)png, pngsize); + + // No need to free(png) because it is managed by our bump allocator + // which automatically resets the entire 2MB buffer to 0 next time + // lodepng_allocator_reset() is called. + + return res; } -httpd_uri_t api_display_image_uri = { - .uri = "/api/display/image.png", - .method = HTTP_GET, - .handler = api_display_image_handler, - .user_ctx = NULL}; +httpd_uri_t api_display_image_uri = {.uri = "/api/display/image.png", + .method = HTTP_GET, + .handler = api_display_image_handler, + .user_ctx = NULL}; diff --git a/Provider/main/lodepng_alloc.cpp b/Provider/main/lodepng_alloc.cpp index aca6404..275068f 100644 --- a/Provider/main/lodepng_alloc.cpp +++ b/Provider/main/lodepng_alloc.cpp @@ -4,149 +4,160 @@ #include // LVGL's LodePNG memory optimization -// Instead of standard heap allocations which fragment quickly and crash on the ESP32, -// we allocate a single massive buffer in PSRAM and just bump a pointer during encode! +// Instead of standard heap allocations which fragment quickly and crash on the +// ESP32, we allocate a single massive buffer in PSRAM and just bump a pointer +// during encode! -static const char* kTagLodeAlloc = "LODE_ALLOC"; +static const char *kTagLodeAlloc = "LODE_ALLOC"; -// 2MB buffer for LodePNG encoding intermediate state. -// A typical 800x480 grayscale PNG should compress to ~50-100KB, but the dynamic window -// matching and filtering algorithms need a good amount of scratch space. -// We can tune this down to 1MB if 2MB is too aggressive, but PSRAM provides 8MB. -#define LODEPNG_ALLOC_POOL_SIZE (2 * 1024 * 1024) +// 2MB buffer for LodePNG encoding intermediate state. +// A typical 800x480 grayscale PNG should compress to ~50-100KB, but the dynamic +// window matching and filtering algorithms need a good amount of scratch space. +// We can tune this down to 1MB if 2MB is too aggressive, but PSRAM provides +// 8MB. +#define LODEPNG_ALLOC_POOL_SIZE (1 * 1024 * 1024) -static uint8_t* s_lodepng_pool = nullptr; +static uint8_t *s_lodepng_pool = nullptr; static size_t s_lodepng_pool_used = 0; void lodepng_allocator_init() { - if (s_lodepng_pool != nullptr) return; + if (s_lodepng_pool != nullptr) + return; - ESP_LOGI(kTagLodeAlloc, "Allocating %d bytes in PSRAM for LodePNG bump allocator...", LODEPNG_ALLOC_POOL_SIZE); - - // SPIRAM fallback to internal if someone tests without a PSRAM chip - s_lodepng_pool = (uint8_t*)heap_caps_malloc(LODEPNG_ALLOC_POOL_SIZE, MALLOC_CAP_SPIRAM); - if (!s_lodepng_pool) - { - s_lodepng_pool = (uint8_t*)heap_caps_malloc(LODEPNG_ALLOC_POOL_SIZE, MALLOC_CAP_DEFAULT); - } + ESP_LOGI(kTagLodeAlloc, + "Allocating %d bytes in PSRAM for LodePNG bump allocator...", + LODEPNG_ALLOC_POOL_SIZE); - if (!s_lodepng_pool) - { - ESP_LOGE(kTagLodeAlloc, "CRITICAL: Failed to allocate LodePNG PSRAM pool!"); - } + // SPIRAM fallback to internal if someone tests without a PSRAM chip + s_lodepng_pool = + (uint8_t *)heap_caps_malloc(LODEPNG_ALLOC_POOL_SIZE, MALLOC_CAP_SPIRAM); + if (!s_lodepng_pool) + { + s_lodepng_pool = (uint8_t *)heap_caps_malloc(LODEPNG_ALLOC_POOL_SIZE, + MALLOC_CAP_DEFAULT); + } + + if (!s_lodepng_pool) + { + ESP_LOGE(kTagLodeAlloc, "CRITICAL: Failed to allocate LodePNG PSRAM pool!"); + } } -void lodepng_allocator_reset() -{ - s_lodepng_pool_used = 0; -} +void lodepng_allocator_reset() { s_lodepng_pool_used = 0; } void lodepng_allocator_free() { - if (s_lodepng_pool) - { - free(s_lodepng_pool); - s_lodepng_pool = nullptr; - } - s_lodepng_pool_used = 0; + if (s_lodepng_pool) + { + free(s_lodepng_pool); + s_lodepng_pool = nullptr; + } + s_lodepng_pool_used = 0; } // ---------------------------------------------------- // Custom Allocators injected into lodepng.c // ---------------------------------------------------- -// To support realloc properly, we prefix each allocation with an 8-byte header storing the size. -struct AllocHeader { - size_t size; +// To support realloc properly, we prefix each allocation with an 8-byte header +// storing the size. +struct AllocHeader +{ + size_t size; }; -void* lodepng_custom_malloc(size_t size) +void *lodepng_custom_malloc(size_t size) { - if (!s_lodepng_pool) - { - ESP_LOGE(kTagLodeAlloc, "lodepng_malloc called before lodepng_allocator_init!"); - return nullptr; - } + if (!s_lodepng_pool) + { + ESP_LOGE(kTagLodeAlloc, + "lodepng_malloc called before lodepng_allocator_init!"); + return nullptr; + } - // Align size to 8 bytes to avoid unaligned access faults - size_t aligned_size = (size + 7) & ~7; - size_t total_alloc = sizeof(AllocHeader) + aligned_size; + // Align size to 8 bytes to avoid unaligned access faults + size_t aligned_size = (size + 7) & ~7; + size_t total_alloc = sizeof(AllocHeader) + aligned_size; - if (s_lodepng_pool_used + total_alloc > LODEPNG_ALLOC_POOL_SIZE) - { - ESP_LOGE(kTagLodeAlloc, "LodePNG pool exhausted! Requested: %zu, Used: %zu, Total: %d", size, s_lodepng_pool_used, LODEPNG_ALLOC_POOL_SIZE); - return nullptr; - } + if (s_lodepng_pool_used + total_alloc > LODEPNG_ALLOC_POOL_SIZE) + { + ESP_LOGE(kTagLodeAlloc, + "LodePNG pool exhausted! Requested: %zu, Used: %zu, Total: %d", + size, s_lodepng_pool_used, LODEPNG_ALLOC_POOL_SIZE); + return nullptr; + } - // Grab pointer and bump - uint8_t* ptr = s_lodepng_pool + s_lodepng_pool_used; - s_lodepng_pool_used += total_alloc; + // Grab pointer and bump + uint8_t *ptr = s_lodepng_pool + s_lodepng_pool_used; + s_lodepng_pool_used += total_alloc; - // Write header - AllocHeader* header = (AllocHeader*)ptr; - header->size = size; // We store exact size for realloc memcpy bounds + // Write header + AllocHeader *header = (AllocHeader *)ptr; + header->size = size; // We store exact size for realloc memcpy bounds - // Return pointer right after header - return ptr + sizeof(AllocHeader); + // Return pointer right after header + return ptr + sizeof(AllocHeader); } -void* lodepng_custom_realloc(void* ptr, size_t new_size) +void *lodepng_custom_realloc(void *ptr, size_t new_size) { - if (!ptr) + if (!ptr) + { + return lodepng_custom_malloc(new_size); + } + + if (new_size == 0) + { + lodepng_custom_free(ptr); + return nullptr; + } + + // Get original header + uint8_t *orig_ptr = (uint8_t *)ptr - sizeof(AllocHeader); + AllocHeader *header = (AllocHeader *)orig_ptr; + + size_t old_size = header->size; + if (new_size <= old_size) + { + // Don't shrink to save time, bump allocator can't reclaim it easily anyway. + return ptr; + } + + // Let's see if this ptr was the *very last* allocation. + // If so, we can just expand it in place! + size_t old_aligned_size = (old_size + 7) & ~7; + if (orig_ptr + sizeof(AllocHeader) + old_aligned_size == + s_lodepng_pool + s_lodepng_pool_used) + { + // We are at the end! Just bump further! + size_t new_aligned_size = (new_size + 7) & ~7; + size_t size_diff = new_aligned_size - old_aligned_size; + + if (s_lodepng_pool_used + size_diff > LODEPNG_ALLOC_POOL_SIZE) { - return lodepng_custom_malloc(new_size); - } - - if (new_size == 0) - { - lodepng_custom_free(ptr); - return nullptr; + ESP_LOGE(kTagLodeAlloc, + "LodePNG pool exhausted during in-place realloc!"); + return nullptr; } - // Get original header - uint8_t* orig_ptr = (uint8_t*)ptr - sizeof(AllocHeader); - AllocHeader* header = (AllocHeader*)orig_ptr; + s_lodepng_pool_used += size_diff; + header->size = new_size; + return ptr; + } - size_t old_size = header->size; - if (new_size <= old_size) - { - // Don't shrink to save time, bump allocator can't reclaim it easily anyway. - return ptr; - } + // Otherwise, we have to copy into a new block + void *new_ptr = lodepng_custom_malloc(new_size); + if (new_ptr) + { + memcpy(new_ptr, ptr, old_size); + } - // Let's see if this ptr was the *very last* allocation. - // If so, we can just expand it in place! - size_t old_aligned_size = (old_size + 7) & ~7; - if (orig_ptr + sizeof(AllocHeader) + old_aligned_size == s_lodepng_pool + s_lodepng_pool_used) - { - // We are at the end! Just bump further! - size_t new_aligned_size = (new_size + 7) & ~7; - size_t size_diff = new_aligned_size - old_aligned_size; - - if (s_lodepng_pool_used + size_diff > LODEPNG_ALLOC_POOL_SIZE) - { - ESP_LOGE(kTagLodeAlloc, "LodePNG pool exhausted during in-place realloc!"); - return nullptr; - } - - s_lodepng_pool_used += size_diff; - header->size = new_size; - return ptr; - } - - // Otherwise, we have to copy into a new block - void* new_ptr = lodepng_custom_malloc(new_size); - if (new_ptr) - { - memcpy(new_ptr, ptr, old_size); - } - - return new_ptr; + return new_ptr; } -void lodepng_custom_free(void* ptr) +void lodepng_custom_free(void *ptr) { - // No-op! The bump pointer will just reset to 0 once the API endpoint is done! - (void)ptr; + // No-op! The bump pointer will just reset to 0 once the API endpoint is done! + (void)ptr; } diff --git a/Provider/main/lv_setup.cpp b/Provider/main/lv_setup.cpp index b532bac..6e1235e 100644 --- a/Provider/main/lv_setup.cpp +++ b/Provider/main/lv_setup.cpp @@ -1,8 +1,11 @@ #include "lv_setup.hpp" + #include "esp_heap_caps.h" #include "esp_log.h" #include "esp_timer.h" #include "freertos/task.h" +#include "lodepng_alloc.hpp" + #include "types.hpp" internal const char *kTagLvgl = "LVGL"; @@ -13,94 +16,117 @@ uint8_t *g_LvglDrawBuffer = nullptr; internal void lvgl_tick_task(void *arg) { - while (true) - { - vTaskDelay(pdMS_TO_TICKS(10)); + while (true) + { + vTaskDelay(pdMS_TO_TICKS(10)); - if (xSemaphoreTake(g_LvglMutex, portMAX_DELAY) == pdTRUE) - { - lv_timer_handler(); - xSemaphoreGive(g_LvglMutex); - } + 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); + 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) +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); + // 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 *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); + 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); + + static lv_style_t style; + lv_style_init(&style); + + lv_style_set_line_color(&style, lv_palette_main(LV_PALETTE_GREY)); + lv_style_set_line_width(&style, 6); + lv_style_set_line_rounded(&style, true); + + /*Create an object with the new style*/ + lv_obj_t *obj = lv_line_create(scr); + lv_obj_add_style(obj, &style, 0); + + static lv_point_precise_t p[] = {{10, 30}, {30, 50}, {100, 0}}; + lv_line_set_points(obj, p, 3); + + lv_obj_center(obj); } void setup_lvgl() { - ESP_LOGI(kTagLvgl, "Initializing LVGL"); + ESP_LOGI(kTagLvgl, "Initializing LVGL"); - g_LvglMutex = xSemaphoreCreateMutex(); + g_LvglMutex = xSemaphoreCreateMutex(); - lv_init(); - lv_tick_set_cb(my_tick_get_cb); + lv_init(); + lv_tick_set_cb(my_tick_get_cb); - uint32_t width = CONFIG_CALENDINK_DISPLAY_WIDTH; - uint32_t height = CONFIG_CALENDINK_DISPLAY_HEIGHT; + 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); + // 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(); + // 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; - } + // Allocate draw buffers in PSRAM + // Using LV_COLOR_FORMAT_RGB565 (2 bytes per pixel) + size_t buf_size = LV_DRAW_BUF_SIZE(width, height, LV_COLOR_FORMAT_RGB565); - g_LvglDrawBuffer = (uint8_t *)buf1; + // 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); + } - 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); + if (!buf1) + { + ESP_LOGE(kTagLvgl, "Failed to allocate LVGL draw buffer entirely."); + return; + } - // Create the background task for the LVGL timer - xTaskCreate(lvgl_tick_task, "LVGL Tick", 4096, nullptr, 5, nullptr); + g_LvglDrawBuffer = (uint8_t *)buf1; - // Draw the sample UI - if (xSemaphoreTake(g_LvglMutex, portMAX_DELAY) == pdTRUE) - { - lv_draw_sample_ui(); - xSemaphoreGive(g_LvglMutex); - } + // Explicitly set the color format of the display FIRST + // so that stride and byte-per-pixel calculations align with our buffer. + lv_display_set_color_format(g_LvglDisplay, LV_COLOR_FORMAT_RGB565); - ESP_LOGI(kTagLvgl, "LVGL fully initialized. Display %lux%lu created.", width, height); + lv_display_set_buffers(g_LvglDisplay, buf1, nullptr, buf_size, + LV_DISPLAY_RENDER_MODE_FULL); + + // 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); } diff --git a/Provider/sdkconfig.defaults b/Provider/sdkconfig.defaults index f466c57..edcaf06 100644 --- a/Provider/sdkconfig.defaults +++ b/Provider/sdkconfig.defaults @@ -33,9 +33,10 @@ CONFIG_SPIRAM_FETCH_INSTRUCTIONS=y CONFIG_SPIRAM_RODATA=y # LVGL Configuration -CONFIG_LV_COLOR_DEPTH_8=y +CONFIG_LV_COLOR_DEPTH_16=y CONFIG_LV_USE_SYSMON=n CONFIG_LV_USE_OBJ_NAME=y +CONFIG_LV_ATTRIBUTE_FAST_MEM_USE_IRAM=y # LVGL Memory Allocator (Use ESP-IDF Heap instead of internal 64kB BSS pool!) CONFIG_LV_USE_BUILTIN_MALLOC=n @@ -51,19 +52,19 @@ CONFIG_LV_BUILD_EXAMPLES=n CONFIG_LV_BUILD_DEMOS=n # Disable unused software drawing color formats (Only L8 and A8 matter for grayscale) -CONFIG_LV_DRAW_SW_SUPPORT_RGB565=n +CONFIG_LV_DRAW_SW_SUPPORT_RGB565=y CONFIG_LV_DRAW_SW_SUPPORT_RGB565A8=n CONFIG_LV_DRAW_SW_SUPPORT_RGB888=n CONFIG_LV_DRAW_SW_SUPPORT_XRGB8888=n CONFIG_LV_DRAW_SW_SUPPORT_ARGB8888=n CONFIG_LV_DRAW_SW_SUPPORT_ARGB8888_PREMULTIPLIED=n -CONFIG_LV_DRAW_SW_SUPPORT_L8=y +CONFIG_LV_DRAW_SW_SUPPORT_L8=n CONFIG_LV_DRAW_SW_SUPPORT_AL88=n -CONFIG_LV_DRAW_SW_SUPPORT_A8=y +CONFIG_LV_DRAW_SW_SUPPORT_A8=n CONFIG_LV_DRAW_SW_SUPPORT_I1=n -# Disable complex drawing features to save memory (no shadows, no complex gradients) -CONFIG_LV_DRAW_SW_COMPLEX=n +# Enable complex drawing features (required for lines thicker than 1px, rounded lines, arcs, and gradients) +CONFIG_LV_DRAW_SW_COMPLEX=y # Disable unneeded widgets for a simple static screen generator CONFIG_LV_USE_CHART=n