243 lines
7.6 KiB
C
243 lines
7.6 KiB
C
#include "ws_client.h"
|
|
#include "esp_websocket_client.h"
|
|
#include "esp_log.h"
|
|
#include "cJSON.h"
|
|
#include <string.h>
|
|
#include <freertos/FreeRTOS.h>
|
|
#include <freertos/semphr.h>
|
|
#include <freertos/timers.h>
|
|
|
|
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;
|
|
}
|