#include "ws_client.h" #include "esp_websocket_client.h" #include "esp_log.h" #include "cJSON.h" #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) { s_state = state; if (s_state_cb) { s_state_cb(state); } } static void parse_stats_json(const char *data, int len) { cJSON *root = cJSON_ParseWithLength(data, len); if (!root) { ESP_LOGW(TAG, "JSON parse failed"); return; } if (xSemaphoreTake(s_stats_mutex, pdMS_TO_TICKS(100)) == pdTRUE) { cJSON *item; item = cJSON_GetObjectItem(root, "cpu_pct"); if (item) s_stats.cpu_pct = (float)item->valuedouble; item = cJSON_GetObjectItem(root, "mem_pct"); if (item) s_stats.mem_pct = (float)item->valuedouble; item = cJSON_GetObjectItem(root, "mem_used_mb"); if (item) s_stats.mem_used_mb = (uint32_t)item->valuedouble; item = cJSON_GetObjectItem(root, "disk_pct"); if (item) s_stats.disk_pct = (float)item->valuedouble; item = cJSON_GetObjectItem(root, "cpu_temp"); if (item) s_stats.cpu_temp = (float)item->valuedouble; item = cJSON_GetObjectItem(root, "uptime_hrs"); if (item) s_stats.uptime_hrs = (float)item->valuedouble; item = cJSON_GetObjectItem(root, "net_rx_kbps"); if (item) s_stats.net_rx_kbps = (float)item->valuedouble; item = cJSON_GetObjectItem(root, "net_tx_kbps"); if (item) s_stats.net_tx_kbps = (float)item->valuedouble; item = cJSON_GetObjectItem(root, "timestamp"); if (item) s_stats.last_update = (uint32_t)item->valuedouble; cJSON *services = cJSON_GetObjectItem(root, "services"); if (cJSON_IsArray(services)) { int count = cJSON_GetArraySize(services); if (count > WS_MAX_SERVICES) count = WS_MAX_SERVICES; s_stats.service_count = count; for (int i = 0; i < count; i++) { cJSON *svc = cJSON_GetArrayItem(services, i); cJSON *name = cJSON_GetObjectItem(svc, "name"); cJSON *status = cJSON_GetObjectItem(svc, "status"); if (name && name->valuestring) { strncpy(s_stats.services[i].name, name->valuestring, WS_SERVICE_NAME_LEN - 1); s_stats.services[i].name[WS_SERVICE_NAME_LEN - 1] = '\0'; } if (status && status->valuestring) { if (strcmp(status->valuestring, "running") == 0) { s_stats.services[i].status = SVC_RUNNING; } else if (strcmp(status->valuestring, "warning") == 0) { s_stats.services[i].status = SVC_WARNING; } else { s_stats.services[i].status = SVC_STOPPED; } } } } /* Parse local_time object for RTC sync */ cJSON *lt = cJSON_GetObjectItem(root, "local_time"); if (cJSON_IsObject(lt)) { cJSON *y = cJSON_GetObjectItem(lt, "y"); cJSON *mo = cJSON_GetObjectItem(lt, "mo"); cJSON *d = cJSON_GetObjectItem(lt, "d"); cJSON *h = cJSON_GetObjectItem(lt, "h"); cJSON *m = cJSON_GetObjectItem(lt, "m"); cJSON *s = cJSON_GetObjectItem(lt, "s"); if (y && mo && d && h && m && s) { s_stats.time_year = (uint16_t)y->valuedouble; s_stats.time_month = (uint8_t)mo->valuedouble; s_stats.time_day = (uint8_t)d->valuedouble; s_stats.time_hour = (uint8_t)h->valuedouble; s_stats.time_minute = (uint8_t)m->valuedouble; s_stats.time_second = (uint8_t)s->valuedouble; s_stats.time_valid = true; } else { s_stats.time_valid = false; } } else { s_stats.time_valid = false; } s_stats.valid = true; xSemaphoreGive(s_stats_mutex); s_last_data_tick = xTaskGetTickCount(); if (s_data_cb) { s_data_cb(&s_stats); } } cJSON_Delete(root); } static void ws_event_handler(void *arg, esp_event_base_t event_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"); s_last_data_tick = xTaskGetTickCount(); set_state(WS_STATE_CONNECTED); break; case WEBSOCKET_EVENT_DISCONNECTED: ESP_LOGW(TAG, "WebSocket disconnected"); set_state(WS_STATE_DISCONNECTED); break; case WEBSOCKET_EVENT_DATA: if (data->op_code == 0x01 && data->data_len > 0) { // text frame ESP_LOGD(TAG, "Received %d bytes", data->data_len); parse_stats_json(data->data_ptr, data->data_len); } break; case WEBSOCKET_EVENT_ERROR: ESP_LOGE(TAG, "WebSocket error"); set_state(WS_STATE_ERROR); break; default: break; } } void ws_client_init(const char *uri) { s_stats_mutex = xSemaphoreCreateMutex(); esp_websocket_client_config_t config = {}; config.uri = uri; config.reconnect_timeout_ms = 5000; config.buffer_size = 2048; 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); } 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); } } void ws_client_reconnect(void) { if (!s_client) return; ESP_LOGI(TAG, "Manual WS reconnect triggered"); esp_websocket_client_close(s_client, pdMS_TO_TICKS(2000)); s_last_data_tick = xTaskGetTickCount(); } ws_state_t ws_client_get_state(void) { return s_state; } void ws_client_get_stats(pi_stats_t *out) { if (xSemaphoreTake(s_stats_mutex, pdMS_TO_TICKS(100)) == pdTRUE) { memcpy(out, &s_stats, sizeof(pi_stats_t)); xSemaphoreGive(s_stats_mutex); } } void ws_client_set_data_callback(ws_data_callback_t cb) { s_data_cb = cb; } void ws_client_set_state_callback(ws_state_callback_t cb) { s_state_cb = cb; }