#include "../../lv_setup.hpp" #include "../../lodepng/lodepng.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"); 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; } // 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); // 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; } 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); } 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};