// GET /api/devices/screen.png?mac=XX — Render and return a PNG for the device's current screen // Uses LVGL to render the device's XML layout (or a fallback label) then encodes to PNG via lodepng. #include "lv_setup.hpp" #include "lodepng/lodepng.h" #include "esp_http_server.h" #include "esp_log.h" #include "esp_heap_caps.h" #include "lvgl.h" #include #include "lodepng_alloc.hpp" #include "types.hpp" #include "device.hpp" internal const char *kTagDeviceScreenImage = "API_DEV_SCREEN_IMG"; internal esp_err_t api_devices_screen_image_handler(httpd_req_t *req) { httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*"); httpd_resp_set_hdr(req, "Cache-Control", "no-cache, no-store, must-revalidate"); httpd_resp_set_type(req, "image/png"); // Extract mac query parameter char mac[18] = {}; size_t buf_len = httpd_req_get_url_query_len(req) + 1; if (buf_len > 1) { char query[64] = {}; if (httpd_req_get_url_query_str(req, query, sizeof(query)) == ESP_OK) { httpd_query_key_value(query, "mac", mac, sizeof(mac)); } } if (mac[0] == '\0') { httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Missing 'mac' query param"); return ESP_FAIL; } device_t *dev = find_device(mac); if (!dev) { httpd_resp_send_err(req, HTTPD_404_NOT_FOUND, "Device not registered"); return ESP_FAIL; } // --- LVGL rendering (mutex-protected) --- if (xSemaphoreTake(g_LvglMutex, pdMS_TO_TICKS(5000)) != pdTRUE) { ESP_LOGE(kTagDeviceScreenImage, "Failed to get LVGL mutex"); httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "LVGL busy"); return ESP_FAIL; } lv_obj_t *scr = lv_screen_active(); // Clear all children from the active screen lv_obj_clean(scr); // White background for grayscale lv_obj_set_style_bg_color(scr, lv_color_white(), LV_PART_MAIN); if (dev->xml_layout[0] != '\0') { // TODO: Use lv_xml_create() when LVGL XML runtime is verified working. // For now, show the XML as text to prove the pipeline works end to end. // Once we confirm LV_USE_XML compiles, we'll swap this for the real XML parser. lv_obj_t *label = lv_label_create(scr); lv_label_set_text(label, dev->xml_layout); lv_obj_set_style_text_color(label, lv_color_black(), LV_PART_MAIN); lv_obj_align(label, LV_ALIGN_TOP_LEFT, 10, 10); } else { // Fallback: render "Hello " lv_obj_t *label = lv_label_create(scr); char text[48]; snprintf(text, sizeof(text), "Hello %s", mac); lv_label_set_text(label, text); lv_obj_set_style_text_color(label, lv_color_black(), LV_PART_MAIN); lv_obj_align(label, LV_ALIGN_CENTER, 0, 0); } // Force LVGL to fully render the screen 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(kTagDeviceScreenImage, "No active draw buffer"); 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; // Handle stride != width uint8_t *packed_data = (uint8_t *)draw_buf->data; bool needs_free = false; if (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(kTagDeviceScreenImage, "Failed to allocate packed buffer"); 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); } } // Encode to PNG unsigned char *png = nullptr; size_t pngsize = 0; lodepng_allocator_reset(); ESP_LOGI(kTagDeviceScreenImage, "Encoding %lux%lu PNG for device %s", width, height, mac); 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(kTagDeviceScreenImage, "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(kTagDeviceScreenImage, "PNG ready: %zu bytes. Sending...", pngsize); esp_err_t res = httpd_resp_send(req, (const char *)png, pngsize); return res; } internal const httpd_uri_t api_devices_screen_image_uri = { .uri = "/api/devices/screen.png", .method = HTTP_GET, .handler = api_devices_screen_image_handler, .user_ctx = NULL};