/** * DreamStack Thin Client — Waveshare ESP32-P4 10.1" Panel * * Dual transport firmware: * 1. ESP-NOW + Panel IR (primary) — sub-1ms signal delivery, LVGL native rendering * 2. WebSocket + pixel streaming (fallback) — for non-DreamStack content * * Build with -DDS_USE_ESPNOW=1 for ESP-NOW mode (default) * Build with -DDS_USE_ESPNOW=0 for WebSocket-only pixel mode */ #include #include #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "esp_log.h" #include "esp_wifi.h" #include "esp_event.h" #include "nvs_flash.h" #include "esp_lcd_panel_ops.h" #include "ds_codec.h" #include "ds_protocol.h" // ─── Transport selection ─── #ifndef DS_USE_ESPNOW #define DS_USE_ESPNOW 1 #endif #if DS_USE_ESPNOW #include "ds_espnow.h" #include "ds_runtime.h" #endif static const char *TAG = "ds-panel"; // ─── Configuration ─── #define PANEL_WIDTH 800 #define PANEL_HEIGHT 1280 #define PIXEL_BYTES 2 // RGB565 #define FB_SIZE (PANEL_WIDTH * PANEL_HEIGHT * PIXEL_BYTES) #define WIFI_SSID CONFIG_WIFI_SSID #define WIFI_PASS CONFIG_WIFI_PASS #if !DS_USE_ESPNOW #define RELAY_URL CONFIG_RELAY_URL #include "esp_websocket_client.h" #endif // ─── Framebuffers (in PSRAM, for pixel mode) ─── static uint8_t *framebuffer; static uint8_t *scratch_buf; // ─── Display handle ─── static esp_lcd_panel_handle_t panel_handle = NULL; // ─── Touch state ─── static uint16_t input_seq = 0; #if DS_USE_ESPNOW // ═══════════════════════════════════════════════════════ // ESP-NOW + Panel IR Mode // ═══════════════════════════════════════════════════════ static void on_signal(uint16_t signal_id, int32_t value) { // Feed signal updates to the Panel IR runtime ds_signal_update(signal_id, value); ESP_LOGD(TAG, "Signal %d = %d", signal_id, (int)value); } static void on_ir_push(const char *ir_json, size_t length) { // Build LVGL UI from Panel IR JSON ESP_LOGI(TAG, "IR push received (%zu bytes), building UI...", length); esp_err_t ret = ds_ui_build(ir_json, length); if (ret == ESP_OK) { ESP_LOGI(TAG, "UI built: %d signals", ds_signal_count()); } else { ESP_LOGE(TAG, "Failed to build UI from IR"); } } static void on_action(uint8_t node_id, uint8_t action_type) { // Forward widget actions to hub via ESP-NOW ds_espnow_send_action(node_id, action_type); } static void espnow_init_and_run(void) { // Initialize Panel IR runtime (LVGL must be ready) // TODO: replace NULL with lv_scr_act() once LVGL is initialized ds_runtime_init(NULL, on_action); // Initialize ESP-NOW transport ds_espnow_config_t config = { .hub_mac = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}, // broadcast .channel = DS_ESPNOW_CHANNEL, .on_signal = on_signal, .on_ir_push = on_ir_push, }; esp_err_t ret = ds_espnow_init(&config); if (ret != ESP_OK) { ESP_LOGE(TAG, "ESP-NOW init failed"); return; } // Send periodic pings so hub discovers us while (1) { ds_espnow_send_ping(); vTaskDelay(pdMS_TO_TICKS(5000)); } } #else // ═══════════════════════════════════════════════════════ // WebSocket Pixel Streaming Mode (Legacy) // ═══════════════════════════════════════════════════════ static void ws_event_handler(void *arg, esp_event_base_t base, int32_t event_id, void *event_data) { esp_websocket_event_data_t *data = (esp_websocket_event_data_t *)event_data; switch (event_id) { case WEBSOCKET_EVENT_CONNECTED: ESP_LOGI(TAG, "WebSocket connected to relay"); break; case WEBSOCKET_EVENT_DATA: if (data->data_len < DS_HEADER_SIZE) break; ds_header_t hdr; if (ds_parse_header((const uint8_t *)data->data_ptr, &hdr) != 0) break; const uint8_t *payload = (const uint8_t *)data->data_ptr + DS_HEADER_SIZE; size_t payload_len = data->data_len - DS_HEADER_SIZE; switch (hdr.frame_type) { case DS_FRAME_PIXELS: if (payload_len == FB_SIZE) { memcpy(framebuffer, payload, FB_SIZE); esp_lcd_panel_draw_bitmap(panel_handle, 0, 0, PANEL_WIDTH, PANEL_HEIGHT, framebuffer); ESP_LOGI(TAG, "Keyframe received (%zu bytes)", payload_len); } break; case DS_FRAME_DELTA: if (ds_apply_delta_rle(framebuffer, FB_SIZE, payload, payload_len, scratch_buf) == 0) { esp_lcd_panel_draw_bitmap(panel_handle, 0, 0, PANEL_WIDTH, PANEL_HEIGHT, framebuffer); } else { ESP_LOGW(TAG, "Delta decode failed (len=%zu)", payload_len); } break; case DS_FRAME_PING: break; case DS_FRAME_END: ESP_LOGI(TAG, "Stream ended"); break; } break; case WEBSOCKET_EVENT_DISCONNECTED: ESP_LOGW(TAG, "WebSocket disconnected, reconnecting..."); break; case WEBSOCKET_EVENT_ERROR: ESP_LOGE(TAG, "WebSocket error"); break; } } static void send_touch_event(esp_websocket_client_handle_t ws, uint8_t id, uint16_t x, uint16_t y, uint8_t phase) { uint8_t buf[DS_HEADER_SIZE + sizeof(ds_touch_event_t)]; ds_touch_event_t touch = { .id = id, .x = x, .y = y, .phase = phase }; size_t len = ds_encode_touch(buf, input_seq++, (uint32_t)(esp_timer_get_time() / 1000), &touch); esp_websocket_client_send_bin(ws, (const char *)buf, len, portMAX_DELAY); } #endif // DS_USE_ESPNOW // ─── Touch polling task ─── static void touch_task(void *arg) { while (1) { // TODO: Read from GT9271 touch controller via I2C // gt9271_touch_data_t td; // if (gt9271_read(&td) == ESP_OK && td.num_points > 0) { // #if DS_USE_ESPNOW // ds_espnow_send_touch(0, 0, td.points[0].x, td.points[0].y); // #else // send_touch_event(ws, ...); // #endif // } vTaskDelay(pdMS_TO_TICKS(10)); // 100Hz touch polling } } // ─── Display initialization ─── static esp_err_t display_init(void) { // TODO: Initialize MIPI DSI display using Waveshare component ESP_LOGI(TAG, "Display initialized (%dx%d RGB565)", PANEL_WIDTH, PANEL_HEIGHT); return ESP_OK; } // ─── WiFi initialization ─── static void wifi_init(void) { esp_netif_init(); esp_event_loop_create_default(); esp_netif_create_default_wifi_sta(); wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); esp_wifi_init(&cfg); wifi_config_t wifi_cfg = { .sta = { .ssid = WIFI_SSID, .password = WIFI_PASS, }, }; esp_wifi_set_mode(WIFI_MODE_STA); esp_wifi_set_config(WIFI_IF_STA, &wifi_cfg); esp_wifi_start(); #if DS_USE_ESPNOW // For ESP-NOW: don't need to connect to AP, just start WiFi ESP_LOGI(TAG, "WiFi started (ESP-NOW mode, no AP connection needed)"); #else esp_wifi_connect(); ESP_LOGI(TAG, "WiFi connecting to %s...", WIFI_SSID); #endif } // ─── Main ─── void app_main(void) { #if DS_USE_ESPNOW ESP_LOGI(TAG, "DreamStack Panel v0.2 (ESP-NOW + Panel IR)"); #else ESP_LOGI(TAG, "DreamStack Panel v0.2 (WebSocket + Pixel)"); #endif ESP_LOGI(TAG, "Display: %dx%d @ %d bpp = %d bytes", PANEL_WIDTH, PANEL_HEIGHT, PIXEL_BYTES * 8, FB_SIZE); nvs_flash_init(); // Allocate framebuffers in PSRAM (needed for pixel mode, optional for IR mode) framebuffer = heap_caps_calloc(1, FB_SIZE, MALLOC_CAP_SPIRAM); scratch_buf = heap_caps_calloc(1, FB_SIZE, MALLOC_CAP_SPIRAM); if (!framebuffer || !scratch_buf) { ESP_LOGE(TAG, "Failed to allocate framebuffers in PSRAM (%d bytes each)", FB_SIZE); return; } display_init(); wifi_init(); vTaskDelay(pdMS_TO_TICKS(2000)); #if DS_USE_ESPNOW // ESP-NOW mode: init transport + runtime, wait for IR push espnow_init_and_run(); #else // WebSocket mode: connect to relay, receive pixel frames esp_websocket_client_config_t ws_cfg = { .uri = RELAY_URL, .buffer_size = 64 * 1024, }; esp_websocket_client_handle_t ws = esp_websocket_client_init(&ws_cfg); esp_websocket_register_events(ws, WEBSOCKET_EVENT_ANY, ws_event_handler, NULL); esp_websocket_client_start(ws); ESP_LOGI(TAG, "WebSocket connecting to %s...", RELAY_URL); xTaskCreate(touch_task, "touch", 4096, ws, 5, NULL); #endif ESP_LOGI(TAG, "Panel running. Waiting for frames..."); }