From 58948bdfb66853a4c92577d505b0e3531d66aa06 Mon Sep 17 00:00:00 2001 From: Patedam Date: Sun, 5 Apr 2026 19:34:45 -0400 Subject: [PATCH] Last piece to connect client to provider: actually downloading the image and displaying it --- Client/main/main.cpp | 79 ++++--- Client/main/provider.cpp | 96 ++++---- Client/main/provider.hpp | 5 +- Provider/main/api/devices/screen_bitmap.cpp | 235 ++++++++++++++++++++ Provider/main/api/devices/unity.cpp | 1 + Provider/main/http_server.cpp | 1 + components/epd/epd.cpp | 3 +- components/epd/epd.hpp | 2 + 8 files changed, 346 insertions(+), 76 deletions(-) create mode 100644 Provider/main/api/devices/screen_bitmap.cpp diff --git a/Client/main/main.cpp b/Client/main/main.cpp index 0c6d56c..1e6da4d 100644 --- a/Client/main/main.cpp +++ b/Client/main/main.cpp @@ -37,41 +37,43 @@ extern "C" void app_main() setup_led(); + static uint8 display_buffer[96000]; + bool received_from_provider = false; + // Connect to WiFi - if (false) + ESP_LOGI(TAG, "Initializing WiFi connection"); + initialize_network(); + + esp_err_t err = connect_wifi(CONFIG_CALENDINK_WIFI_SSID, + CONFIG_CALENDINK_WIFI_PASSWORD, false); + + if (err == ESP_OK) { - ESP_LOGI(TAG, "Initializing WiFi connection"); - initialize_network(); - - esp_err_t err = connect_wifi(CONFIG_CALENDINK_WIFI_SSID, - CONFIG_CALENDINK_WIFI_PASSWORD, false); - - if (err == ESP_OK) + uint8_t retries = 1; + do { - uint8_t retries = 1; - do + err = check_wifi_connection(retries); + if (err != ESP_OK) { - err = check_wifi_connection(retries); - if (err != ESP_OK) - { - ESP_LOGW(TAG, "WiFi connection check timeout, retrying... (%d)", - retries); - led_blink_number(3, 255, 0, 0); - } - retries++; - } while (err == ESP_ERR_TIMEOUT && - retries <= CONFIG_CALENDINK_WIFI_RETRIES); - } + ESP_LOGW(TAG, "WiFi connection check timeout, retrying... (%d)", + retries); + led_blink_number(3, 255, 0, 0); + } + retries++; + } while (err == ESP_ERR_TIMEOUT && + retries <= CONFIG_CALENDINK_WIFI_RETRIES); + } - if (err == ESP_OK) - { - ESP_LOGI(TAG, "Successfully connected to WiFi!"); - test_provider_communication(); - } - else - { - ESP_LOGE(TAG, "Failed to connect to WiFi."); - } + if (err == ESP_OK) + { + ESP_LOGI(TAG, "Successfully connected to WiFi!"); + ESP_LOGI(TAG, "Fetching screen from Provider..."); + received_from_provider = test_provider_communication(display_buffer, sizeof(display_buffer)); + ESP_LOGI(TAG, "Provider result: %s", received_from_provider ? "SUCCESS" : "FAILED"); + } + else + { + ESP_LOGE(TAG, "Failed to connect to WiFi."); } turn_off_led(); @@ -79,21 +81,28 @@ extern "C" void app_main() ESP_LOGI(TAG, "Initializing EPD"); epd_init(); epd_init_display(true); - // epd_clear(epd_color::WHITE); - epd_draw_bitmap_grayscale(epd_color::WHITE, gImage_4G1); + + if (received_from_provider) { + ESP_LOGI(TAG, "Drawing image from Provider"); + epd_draw_bitmap_grayscale(epd_color::WHITE, display_buffer); + } else { + ESP_LOGW(TAG, "Drawing fallback test image"); + epd_draw_bitmap_grayscale(epd_color::WHITE, gImage_4G1); + } + epd_refresh(); epd_shutdown_display(); - if (false) + if (err == ESP_OK) { disconnect_wifi(); shutdown_network(); } ESP_LOGI(TAG, "Waiting 5 seconds before deep sleep..."); - vTaskDelay(pdMS_TO_TICKS(5000)); + vTaskDelay(pdMS_TO_TICKS(15000)); ESP_LOGI(TAG, "Entering Deep Sleep for 60 seconds..."); - esp_sleep_enable_timer_wakeup(60ULL * 1000000ULL); + esp_sleep_enable_timer_wakeup(30ULL * 1000000ULL); esp_deep_sleep_start(); } diff --git a/Client/main/provider.cpp b/Client/main/provider.cpp index 82898e6..6ef2445 100644 --- a/Client/main/provider.cpp +++ b/Client/main/provider.cpp @@ -32,15 +32,19 @@ static bool resolve_provider_ip(char *out_ip, size_t out_ip_len) { esp_ip4_addr_t addr = {}; - err = mdns_query_a(CONFIG_CALENDINK_PROVIDER_MDNS_HOSTNAME, 5000, &addr); - if (err == ESP_OK) + constexpr int kMaxRetries = 3; + for (int attempt = 1; attempt <= kMaxRetries; attempt++) { - snprintf(out_ip, out_ip_len, IPSTR, IP2STR(&addr)); - ESP_LOGI(TAG, "Provider resolved: %s", out_ip); - return true; + err = mdns_query_a(CONFIG_CALENDINK_PROVIDER_MDNS_HOSTNAME, 5000, &addr); + if (err == ESP_OK) + { + snprintf(out_ip, out_ip_len, IPSTR, IP2STR(&addr)); + ESP_LOGI(TAG, "Provider resolved: %s (attempt %d)", out_ip, attempt); + return true; + } + ESP_LOGW(TAG, "mDNS attempt %d/%d failed: %s", attempt, kMaxRetries, + esp_err_to_name(err)); } - - ESP_LOGW(TAG, "mDNS resolution failed: %s", esp_err_to_name(err)); } fallback: @@ -57,13 +61,15 @@ fallback: // ── Provider Communication Test ───────────────────────────────────────────── -void test_provider_communication(void) +bool test_provider_communication(uint8 *out_buffer, size_t buffer_size) { + bool success = false; + // 1. Resolve Provider IP char provider_ip[16] = {}; if (!resolve_provider_ip(provider_ip, sizeof(provider_ip))) { - return; + return false; } uint16_t provider_port = CONFIG_CALENDINK_PROVIDER_PORT; @@ -74,7 +80,7 @@ void test_provider_communication(void) if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to get WiFi MAC: %s", esp_err_to_name(err)); - return; + return false; } char mac_str[18] = {}; @@ -84,59 +90,73 @@ void test_provider_communication(void) ESP_LOGI(TAG, "Client MAC: %s", mac_str); // 3. Register with Provider: POST /api/devices/register + // This may return "already_registered" — that's fine, we continue regardless. { char *url = http_build_url(provider_ip, provider_port, "/api/devices/register"); - if (url == nullptr) + if (url != nullptr) { - return; + char json_body[64] = {}; + snprintf(json_body, sizeof(json_body), "{\"mac\":\"%s\"}", mac_str); + + http_text_response_t resp = {}; + err = http_post_json(url, json_body, &resp); + + if (err == ESP_OK) + { + ESP_LOGI(TAG, "Register response (%d): %s", resp.status_code, + resp.body ? resp.body : "(empty)"); + } + else + { + ESP_LOGW(TAG, "Register request failed: %s (continuing anyway)", + esp_err_to_name(err)); + } + + free(resp.body); + free(url); } - - char json_body[64] = {}; - snprintf(json_body, sizeof(json_body), "{\"mac\":\"%s\"}", mac_str); - - http_text_response_t resp = {}; - err = http_post_json(url, json_body, &resp); - - if (err == ESP_OK) - { - ESP_LOGI(TAG, "Register response (%d): %s", resp.status_code, - resp.body ? resp.body : "(empty)"); - } - else - { - ESP_LOGE(TAG, "Register request failed: %s", esp_err_to_name(err)); - } - - free(resp.body); - free(url); } - // 4. Fetch screen image: GET /api/devices/screen.png?mac=XX:XX:XX:XX:XX:XX + // 4. Fetch screen bitmap: GET /api/devices/screen.bin?mac=XX:XX:XX:XX:XX:XX { char path[80] = {}; - snprintf(path, sizeof(path), "/api/devices/screen.png?mac=%s", mac_str); + snprintf(path, sizeof(path), "/api/devices/screen.bin?mac=%s", mac_str); char *url = http_build_url(provider_ip, provider_port, path); if (url == nullptr) { - return; + return false; } http_binary_response_t resp = {}; err = http_get_binary(url, &resp); - if (err == ESP_OK) + if (err == ESP_OK && resp.status_code == 200) { - ESP_LOGI(TAG, "Screen image response (%d): %zu bytes", resp.status_code, - resp.data_len); + ESP_LOGI(TAG, "Screen bitmap response: %zu bytes", resp.data_len); + if (resp.data != nullptr && resp.data_len > 0) + { + size_t copy_size = (resp.data_len < buffer_size) ? resp.data_len : buffer_size; + memcpy(out_buffer, resp.data, copy_size); + success = true; + + // Debug: log first 10 bytes (should be 0xFF for white top-left pixels) + ESP_LOGI(TAG, "First 10 bytes: %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X", + out_buffer[0], out_buffer[1], out_buffer[2], out_buffer[3], + out_buffer[4], out_buffer[5], out_buffer[6], out_buffer[7], + out_buffer[8], out_buffer[9]); + } } else { - ESP_LOGE(TAG, "Screen image request failed: %s", esp_err_to_name(err)); + ESP_LOGE(TAG, "Screen bitmap request failed: %s (status %d)", + esp_err_to_name(err), resp.status_code); } free(resp.data); free(url); } + + return success; } diff --git a/Client/main/provider.hpp b/Client/main/provider.hpp index 413f914..cce274e 100644 --- a/Client/main/provider.hpp +++ b/Client/main/provider.hpp @@ -1,5 +1,8 @@ #pragma once +#include "types.hpp" +#include + // Resolve the Provider's IP and run the device registration + screen fetch // test flow. Call once after WiFi is connected. -void test_provider_communication(void); +bool test_provider_communication(uint8 *out_buffer, size_t buffer_size); diff --git a/Provider/main/api/devices/screen_bitmap.cpp b/Provider/main/api/devices/screen_bitmap.cpp new file mode 100644 index 0000000..6184559 --- /dev/null +++ b/Provider/main/api/devices/screen_bitmap.cpp @@ -0,0 +1,235 @@ +// GET /api/devices/screen.bin?mac=XX — Render and return a raw 2bpp grayscale +// bitmap for the device's current screen. +// Uses LVGL to render the device's XML layout, quantizes to 4 grayscale levels, +// and packs into 2 bits per pixel (96,000 bytes for 800×480). + +#include "esp_heap_caps.h" +#include "esp_http_server.h" +#include "esp_log.h" +#include "lv_setup.hpp" +#include "lvgl.h" +#include + +#include "device.hpp" +#include "types.hpp" + +internal const char *kTagDeviceScreenBitmap = "API_DEV_SCREEN_BMP"; + +internal esp_err_t api_devices_screen_bitmap_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, "application/octet-stream"); + + // 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(kTagDeviceScreenBitmap, "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); + + // Setup the MAC address subject so the XML can bind to it + static lv_subject_t mac_subject; + static char mac_buf[18]; + static char mac_prev_buf[18]; + + strncpy(mac_buf, mac, sizeof(mac_buf)); + strncpy(mac_prev_buf, mac, sizeof(mac_prev_buf)); + + lv_subject_init_string(&mac_subject, mac_buf, mac_prev_buf, sizeof(mac_buf), + mac); + + // Register the subject in the global XML scope under the name "device_mac" + lv_xml_component_scope_t *global_scope = + lv_xml_component_get_scope("globals"); + if (global_scope) + { + lv_xml_register_subject(global_scope, "device_mac", &mac_subject); + ESP_LOGI(kTagDeviceScreenBitmap, + "Registered subject 'device_mac' with value: %s", mac); + } + + bool render_success = false; + + // 1. Prepare the XML payload + const char *xml_to_register = NULL; + + if (dev->xml_layout[0] == '\0') + { + ESP_LOGI(kTagDeviceScreenBitmap, "Device %s has no layout xml.", mac); + xSemaphoreGive(g_LvglMutex); + httpd_resp_send_err(req, HTTPD_404_NOT_FOUND, "No layout configured"); + return ESP_FAIL; + } + + if (strstr(dev->xml_layout, "xml_layout; + ESP_LOGI(kTagDeviceScreenBitmap, + "XML already contains , passing directly to parser."); + } + + // 2. Register the XML payload as a component + lv_result_t res = + lv_xml_register_component_from_data("current_device", xml_to_register); + + if (res == LV_RESULT_OK) + { + ESP_LOGI(kTagDeviceScreenBitmap, + "Successfully registered XML for device %s", mac); + + lv_obj_t *new_scr = lv_xml_create_screen("current_device"); + + if (new_scr) + { + lv_screen_load(new_scr); + scr = new_scr; + render_success = true; + } + else + { + ESP_LOGE(kTagDeviceScreenBitmap, + "lv_xml_create_screen failed for device %s", mac); + } + } + else + { + ESP_LOGE(kTagDeviceScreenBitmap, + "lv_xml_register_component_from_data failed for device %s", mac); + } + + // 3. Fallback if LVGL XML parsing or creation failed + if (!render_success) + { + ESP_LOGW(kTagDeviceScreenBitmap, + "XML render failed, falling back to raw text layout"); + lv_obj_t *label = lv_label_create(scr); + lv_label_set_text(label, "XML Parsing Error\nSee serial log"); + 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(kTagDeviceScreenBitmap, "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; + + // Output: 2 bits per pixel, 4 pixels per byte → width*height/4 bytes + constexpr uint32_t kBitmapSize = 96000; // 800 * 480 / 4 + uint8_t *bitmap = (uint8_t *)heap_caps_malloc(kBitmapSize, MALLOC_CAP_SPIRAM); + if (!bitmap) + { + bitmap = (uint8_t *)malloc(kBitmapSize); + if (!bitmap) + { + xSemaphoreGive(g_LvglMutex); + ESP_LOGE(kTagDeviceScreenBitmap, "Failed to allocate bitmap buffer"); + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, + "Out of memory"); + return ESP_FAIL; + } + } + + // LVGL renders into RGB565 (2 bytes per pixel). + // Quantize to 4 grayscale levels and pack 4 pixels per byte (2bpp). + // Pixel encoding (MSB first): + // 0b00 = BLACK (lum 0) + // 0b01 = DARK_GRAY (lum 85) + // 0b10 = LIGHT_GRAY (lum 170) + // 0b11 = WHITE (lum 255) + uint32_t bitmap_idx = 0; + 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)); + + for (uint32_t x = 0; x < width; x += 4) + { + uint8_t packed = 0; + for (int p = 0; p < 4; ++p) + { + uint16_t c = src_row[x + p]; + // 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 + 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); + + // Luminance → 2-bit quantized level (0..3) + uint8_t lum = (r * 77 + g * 150 + b * 29) >> 8; + uint8_t level = lum >> 6; // 0,1,2,3 + + packed |= (level << (6 - p * 2)); + } + bitmap[bitmap_idx++] = packed; + } + } + + xSemaphoreGive(g_LvglMutex); + + ESP_LOGI(kTagDeviceScreenBitmap, "Bitmap ready: %lu bytes. Sending...", + (unsigned long)bitmap_idx); + esp_err_t sendRes = httpd_resp_send(req, (const char *)bitmap, bitmap_idx); + + free(bitmap); + + return sendRes; +} + +internal const httpd_uri_t api_devices_screen_bitmap_uri = { + .uri = "/api/devices/screen.bin", + .method = HTTP_GET, + .handler = api_devices_screen_bitmap_handler, + .user_ctx = NULL}; diff --git a/Provider/main/api/devices/unity.cpp b/Provider/main/api/devices/unity.cpp index 498cf30..937d0b1 100644 --- a/Provider/main/api/devices/unity.cpp +++ b/Provider/main/api/devices/unity.cpp @@ -6,4 +6,5 @@ #include "api/devices/layout.cpp" #include "api/devices/screen.cpp" #include "api/devices/screen_image.cpp" +#include "api/devices/screen_bitmap.cpp" // clang-format on diff --git a/Provider/main/http_server.cpp b/Provider/main/http_server.cpp index 2622a95..7f9d3d0 100644 --- a/Provider/main/http_server.cpp +++ b/Provider/main/http_server.cpp @@ -308,6 +308,7 @@ internal httpd_handle_t start_webserver(void) httpd_register_uri_handler(server, &api_devices_layout_uri); httpd_register_uri_handler(server, &api_devices_screen_info_uri); httpd_register_uri_handler(server, &api_devices_screen_image_uri); + httpd_register_uri_handler(server, &api_devices_screen_bitmap_uri); // Populate dummy data for development (debug builds only) #ifndef NDEBUG diff --git a/components/epd/epd.cpp b/components/epd/epd.cpp index d0e44e3..f40acfe 100644 --- a/components/epd/epd.cpp +++ b/components/epd/epd.cpp @@ -435,8 +435,7 @@ void epd_draw_bitmap_grayscale(epd_color clearColor, const uint8 *bitmap) } } - // The demo code applies a bitwise NOT to the result - g_scratch_buffer[scratch_idx++] = ~output_byte; + g_scratch_buffer[scratch_idx++] = output_byte; if (scratch_idx >= sizeof(g_scratch_buffer)) { diff --git a/components/epd/epd.hpp b/components/epd/epd.hpp index a6caedd..a65cb4a 100644 --- a/components/epd/epd.hpp +++ b/components/epd/epd.hpp @@ -22,6 +22,8 @@ enum class epd_color : uint8 { BLACK = 0x00, + DARK_GRAY = 0x55, + LIGHT_GRAY = 0xAA, WHITE = 0xFF };