/** * DreamStack Thin Client — Waveshare ESP32-P4 10.1" Panel * * Firmware that turns the panel into a dumb pixel display * with touch input. All rendering happens on the source device. * * Flow: WiFi → WebSocket → receive delta frames → blit to display * Touch → encode event → send over WebSocket * * Dependencies (via ESP Component Registry): * - waveshare/esp_lcd_jd9365_10_1 (10.1" MIPI DSI display driver) * - espressif/esp_websocket_client (WebSocket client) */ #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 "esp_websocket_client.h" #include "ds_codec.h" #include "ds_protocol.h" static const char *TAG = "ds-panel"; // ─── Configuration (set via menuconfig or hardcode for POC) ─── #define PANEL_WIDTH 800 #define PANEL_HEIGHT 1280 #define PIXEL_BYTES 2 // RGB565 #define FB_SIZE (PANEL_WIDTH * PANEL_HEIGHT * PIXEL_BYTES) // ~2MB #define WIFI_SSID CONFIG_WIFI_SSID #define WIFI_PASS CONFIG_WIFI_PASS #define RELAY_URL CONFIG_RELAY_URL // e.g. "ws://192.168.1.100:9100/stream/home" // ─── Framebuffers (in PSRAM) ─── static uint8_t *framebuffer; // Current display state static uint8_t *scratch_buf; // Temp buffer for delta decode // ─── Display handle ─── static esp_lcd_panel_handle_t panel_handle = NULL; // ─── Touch state ─── static uint16_t input_seq = 0; // ─── WebSocket event handler ─── 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: // Full keyframe — copy directly to framebuffer 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: // Delta frame — RLE decode + XOR apply 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: // Respond with pong (same message back) 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; } } // ─── Send touch event over WebSocket ─── 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); } // ─── Touch polling task ─── // // TODO: Replace with actual GT9271 I2C touch driver. // The Waveshare BSP should provide touch reading functions. // This is a placeholder showing the integration pattern. // static void touch_task(void *arg) { esp_websocket_client_handle_t ws = (esp_websocket_client_handle_t)arg; while (1) { // TODO: Read from GT9271 touch controller via I2C // Example (pseudocode): // // gt9271_touch_data_t td; // if (gt9271_read(&td) == ESP_OK && td.num_points > 0) { // for (int i = 0; i < td.num_points; i++) { // send_touch_event(ws, td.points[i].id, // td.points[i].x, td.points[i].y, // td.points[i].phase); // } // } vTaskDelay(pdMS_TO_TICKS(10)); // 100Hz touch polling } } // ─── Display initialization ─── // // TODO: Initialize MIPI DSI display using Waveshare component. // Add `waveshare/esp_lcd_jd9365_10_1` to idf_component.yml // static esp_err_t display_init(void) { // TODO: Configure MIPI DSI bus and JD9365 panel driver // Example (pseudocode): // // esp_lcd_dsi_bus_config_t bus_cfg = { ... }; // esp_lcd_new_dsi_bus(&bus_cfg, &dsi_bus); // // esp_lcd_panel_dev_config_t panel_cfg = { // .reset_gpio_num = ..., // .rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB, // .bits_per_pixel = 16, // RGB565 // }; // esp_lcd_new_panel_jd9365_10_1(dsi_bus, &panel_cfg, &panel_handle); // esp_lcd_panel_init(panel_handle); 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(); esp_wifi_connect(); ESP_LOGI(TAG, "WiFi connecting to %s...", WIFI_SSID); } // ─── Main ─── void app_main(void) { ESP_LOGI(TAG, "DreamStack Thin Client v0.1"); ESP_LOGI(TAG, "Panel: %dx%d @ %d bpp = %d bytes", PANEL_WIDTH, PANEL_HEIGHT, PIXEL_BYTES * 8, FB_SIZE); // Initialize NVS (required for WiFi) nvs_flash_init(); // Allocate framebuffers in PSRAM 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; } ESP_LOGI(TAG, "Framebuffers allocated in PSRAM (%d MB each)", FB_SIZE / (1024 * 1024)); // Initialize display display_init(); // Initialize WiFi wifi_init(); vTaskDelay(pdMS_TO_TICKS(3000)); // Wait for WiFi connection // Connect WebSocket to relay esp_websocket_client_config_t ws_cfg = { .uri = RELAY_URL, .buffer_size = 64 * 1024, // 64KB receive buffer }; 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); // Start touch polling task xTaskCreate(touch_task, "touch", 4096, ws, 5, NULL); ESP_LOGI(TAG, "Thin client running. Waiting for frames..."); }