diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 8bce4f3..bced40d 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -1,8 +1,9 @@ { - "outputStyle": "iseri", "permissions": { "allow": [ "Bash(echo:*)" ] - } + }, + "outputStyle": "iseri", + "prefersReducedMotion": false } diff --git a/README.md b/README.md new file mode 100644 index 0000000..4fa9cf3 --- /dev/null +++ b/README.md @@ -0,0 +1,49 @@ +# Pi Dashboard + +Raspberry Pi system monitor running on a Waveshare ESP32-S3 RLCD 4.2" board. Connects to a Pi over WebSocket, parses JSON stats, and renders a live dashboard on a 400x300 1-bit monochrome reflective LCD using LVGL. + +## Hardware + +- **Board:** Waveshare ESP32-S3 RLCD 4.2" +- **Display:** 400x300 reflective LCD (1-bit monochrome, no backlight needed) +- **Sensors:** SHTC3 temp/humidity (I2C 0x70), PCF85063 RTC (I2C 0x51) + +## Architecture + +``` +Pi (server) ESP32-S3 (client) +stats_server.py --WS/JSON--> ws_client --> dashboard_ui (LVGL) + + local sensors (SHTC3) +``` + +The Pi runs a WebSocket server that pushes system stats (CPU, memory, disk, temperature, network, services) as JSON every 2 seconds. The ESP32 parses the JSON and updates LVGL widgets. A data staleness watchdog forces reconnection if the server goes silent. + +## Configuration + +Edit `components/esp_wifi_bsp/wifi_config.h`: + +```c +#define WIFI_SSID "your_ssid" +#define WIFI_PASSWORD "your_password" +#define WS_SERVER_URI "ws://192.168.x.x:8765" +``` + +## Build and Flash + +Requires ESP-IDF v5.5+. + +```bash +idf.py build +idf.py flash monitor +``` + +## Mock Server + +Test without a real Pi using the mock server: + +```bash +pip install -r pi/requirements.txt +python pi/mock_server.py +``` + +Sends randomized stats on `ws://0.0.0.0:8765` every 2 seconds. diff --git a/components/ws_client/ws_client.c b/components/ws_client/ws_client.c index 2532da6..0723293 100644 --- a/components/ws_client/ws_client.c +++ b/components/ws_client/ws_client.c @@ -5,15 +5,32 @@ #include #include #include +#include static const char *TAG = "ws_client"; +#define WS_WATCHDOG_PERIOD_MS 5000 +#define WS_DATA_TIMEOUT_MS 10000 + static esp_websocket_client_handle_t s_client = NULL; static pi_stats_t s_stats = {}; static SemaphoreHandle_t s_stats_mutex = NULL; static ws_state_t s_state = WS_STATE_DISCONNECTED; static ws_data_callback_t s_data_cb = NULL; static ws_state_callback_t s_state_cb = NULL; +static TickType_t s_last_data_tick = 0; +static TimerHandle_t s_watchdog_timer = NULL; + +static void watchdog_callback(TimerHandle_t timer) +{ + (void)timer; + if (s_state == WS_STATE_CONNECTED && + (xTaskGetTickCount() - s_last_data_tick) > pdMS_TO_TICKS(WS_DATA_TIMEOUT_MS)) { + ESP_LOGW(TAG, "WS watchdog: no data for %ds, forcing reconnect", + WS_DATA_TIMEOUT_MS / 1000); + esp_websocket_client_close(s_client, pdMS_TO_TICKS(2000)); + } +} static void set_state(ws_state_t state) { @@ -83,6 +100,8 @@ static void parse_stats_json(const char *data, int len) s_stats.valid = true; xSemaphoreGive(s_stats_mutex); + s_last_data_tick = xTaskGetTickCount(); + if (s_data_cb) { s_data_cb(&s_stats); } @@ -99,6 +118,7 @@ static void ws_event_handler(void *arg, esp_event_base_t event_base, switch (event_id) { case WEBSOCKET_EVENT_CONNECTED: ESP_LOGI(TAG, "WebSocket connected"); + s_last_data_tick = xTaskGetTickCount(); set_state(WS_STATE_CONNECTED); break; case WEBSOCKET_EVENT_DISCONNECTED: @@ -132,6 +152,9 @@ void ws_client_init(const char *uri) s_client = esp_websocket_client_init(&config); esp_websocket_register_events(s_client, WEBSOCKET_EVENT_ANY, ws_event_handler, NULL); + s_watchdog_timer = xTimerCreate("ws_wd", pdMS_TO_TICKS(WS_WATCHDOG_PERIOD_MS), + pdTRUE, NULL, watchdog_callback); + ESP_LOGI(TAG, "WS client initialized: %s", uri); } @@ -140,12 +163,18 @@ void ws_client_start(void) if (s_client) { set_state(WS_STATE_CONNECTING); esp_websocket_client_start(s_client); + if (s_watchdog_timer) { + xTimerStart(s_watchdog_timer, 0); + } } } void ws_client_stop(void) { if (s_client) { + if (s_watchdog_timer) { + xTimerStop(s_watchdog_timer, 0); + } esp_websocket_client_stop(s_client); set_state(WS_STATE_DISCONNECTED); }