236 lines
7.8 KiB
C
236 lines
7.8 KiB
C
|
|
/**
|
||
|
|
* 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 <stdio.h>
|
||
|
|
#include <string.h>
|
||
|
|
#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...");
|
||
|
|
}
|