385 lines
16 KiB
C
385 lines
16 KiB
C
#include <string.h>
|
|
#include "freertos/FreeRTOS.h"
|
|
#include "freertos/task.h"
|
|
#include "freertos/event_groups.h"
|
|
#include "esp_system.h"
|
|
#include "esp_log.h"
|
|
#include "esp_websocket_client.h"
|
|
#include "esp_crt_bundle.h"
|
|
#include "esp_wifi.h"
|
|
#include "esp_timer.h"
|
|
|
|
#include "socket_client.h"
|
|
#include "audio_driver.h"
|
|
#include "wifi_connect.h"
|
|
#include "state_manager.h"
|
|
#include "device_config.h"
|
|
#include "ota_manager.h"
|
|
|
|
static const char *TAG = "socket_client";
|
|
|
|
static EventGroupHandle_t s_ws_event_group;
|
|
#define WS_CONNECTED_BIT BIT0
|
|
|
|
static esp_websocket_client_handle_t s_client = NULL;
|
|
|
|
static void send_telemetry(void)
|
|
{
|
|
if (s_client == NULL || !(xEventGroupGetBits(s_ws_event_group) & WS_CONNECTED_BIT)) {
|
|
return;
|
|
}
|
|
|
|
device_state_t st;
|
|
state_get_current(&st);
|
|
|
|
int free_heap = esp_get_free_heap_size();
|
|
int uptime = esp_timer_get_time() / 1000000; // seconds
|
|
int rssi = 0;
|
|
|
|
wifi_ap_record_t ap_info;
|
|
if (esp_wifi_sta_get_ap_info(&ap_info) == ESP_OK) {
|
|
rssi = ap_info.rssi;
|
|
}
|
|
|
|
char payload[512];
|
|
int len = snprintf(payload, sizeof(payload),
|
|
"{\"type\":\"state_report\",\"system\":{\"model\":\"%s\"},\"health\":{\"free_heap_bytes\":%d,\"uptime_seconds\":%d},\"network\":{\"wifi_rssi\":%d},\"peripherals\":{\"audio\":{\"volume\":%d},\"led_ring\":{\"state_id\":%d,\"brightness_coef\":%.2f},\"haptic_intensity\":%d,\"ambient_light\":%d,\"touch_active\":%s}}",
|
|
DEVICE_MODEL, free_heap, uptime, rssi, st.volume, st.led_state, st.led_brightness,
|
|
st.haptic_intensity, st.ambient_light, st.touch_active ? "true" : "false");
|
|
|
|
if (len > 0 && len < sizeof(payload)) {
|
|
ESP_LOGI(TAG, "Sending state report: %s", payload);
|
|
esp_websocket_client_send_text(s_client, payload, len, portMAX_DELAY);
|
|
}
|
|
}
|
|
|
|
void trigger_telemetry_broadcast(void)
|
|
{
|
|
send_telemetry();
|
|
}
|
|
|
|
static char *extract_json_string(const char *json, const char *key)
|
|
{
|
|
char *key_pos = strstr(json, key);
|
|
if (!key_pos) return NULL;
|
|
|
|
// Find the colon after key
|
|
char *colon = strchr(key_pos + strlen(key), ':');
|
|
if (!colon) return NULL;
|
|
|
|
// Find the opening quote of the string value
|
|
char *start_quote = strchr(colon, '"');
|
|
if (!start_quote) return NULL;
|
|
|
|
// Find the closing quote, making sure we handle escaped quotes \"
|
|
char *end_ptr = start_quote + 1;
|
|
while (*end_ptr) {
|
|
if (*end_ptr == '"' && *(end_ptr - 1) != '\\') {
|
|
break;
|
|
}
|
|
end_ptr++;
|
|
}
|
|
if (*end_ptr != '"') return NULL;
|
|
|
|
size_t raw_len = end_ptr - start_quote - 1;
|
|
char *out = malloc(raw_len + 1);
|
|
if (!out) return NULL;
|
|
|
|
// Unescape the string while copying
|
|
size_t out_idx = 0;
|
|
for (size_t i = 0; i < raw_len; i++) {
|
|
if (start_quote[1 + i] == '\\' && i + 1 < raw_len) {
|
|
char next_char = start_quote[1 + i + 1];
|
|
if (next_char == 'n') {
|
|
out[out_idx++] = '\n';
|
|
i++;
|
|
} else if (next_char == 'r') {
|
|
out[out_idx++] = '\r';
|
|
i++;
|
|
} else if (next_char == 't') {
|
|
out[out_idx++] = '\t';
|
|
i++;
|
|
} else if (next_char == '\\' || next_char == '"' || next_char == '/') {
|
|
out[out_idx++] = next_char;
|
|
i++;
|
|
} else {
|
|
out[out_idx++] = '\\';
|
|
}
|
|
} else {
|
|
out[out_idx++] = start_quote[1 + i];
|
|
}
|
|
}
|
|
out[out_idx] = '\0';
|
|
return out;
|
|
}
|
|
|
|
static void websocket_event_handler(void *handler_args, 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 server!");
|
|
xEventGroupSetBits(s_ws_event_group, WS_CONNECTED_BIT);
|
|
state_set_websocket_connected(true);
|
|
break;
|
|
case WEBSOCKET_EVENT_DISCONNECTED:
|
|
ESP_LOGI(TAG, "Websocket disconnected!");
|
|
xEventGroupClearBits(s_ws_event_group, WS_CONNECTED_BIT);
|
|
state_set_websocket_connected(false);
|
|
break;
|
|
case WEBSOCKET_EVENT_DATA:
|
|
if (data->data_ptr != NULL && data->data_len > 0) {
|
|
ESP_LOGI(TAG, "Received message: %.*s", data->data_len, (char *)data->data_ptr);
|
|
|
|
char *msg_str = malloc(data->data_len + 1);
|
|
if (msg_str != NULL) {
|
|
memcpy(msg_str, data->data_ptr, data->data_len);
|
|
msg_str[data->data_len] = '\0';
|
|
|
|
if (strstr(msg_str, "\"set_volume\"") != NULL) {
|
|
char *val_ptr = strstr(msg_str, "\"value\"");
|
|
if (val_ptr != NULL) {
|
|
int level = -1;
|
|
if (sscanf(val_ptr, "\"value\"%*[^0-9]%d", &level) == 1) {
|
|
if (level >= 0 && level <= 100) {
|
|
ESP_LOGI(TAG, "Parsed set_volume command. Mutating state to: %d", level);
|
|
state_set_volume((uint8_t)level);
|
|
}
|
|
}
|
|
}
|
|
send_telemetry();
|
|
}
|
|
else if (strstr(msg_str, "\"set_led\"") != NULL) {
|
|
char *val_ptr = strstr(msg_str, "\"value\"");
|
|
if (val_ptr != NULL) {
|
|
if (strstr(val_ptr, "\"rainbow\"") != NULL) {
|
|
ESP_LOGI(TAG, "Parsed set_led command: rainbow");
|
|
state_set_led(LED_STATE_CONNECTED);
|
|
} else if (strstr(val_ptr, "\"connecting\"") != NULL) {
|
|
ESP_LOGI(TAG, "Parsed set_led command: connecting");
|
|
state_set_led(LED_STATE_SERVER_CONNECTING);
|
|
} else if (strstr(val_ptr, "\"failed\"") != NULL) {
|
|
ESP_LOGI(TAG, "Parsed set_led command: failed");
|
|
state_set_led(LED_STATE_SERVER_FAILED);
|
|
} else if (strstr(val_ptr, "\"idle\"") != NULL) {
|
|
ESP_LOGI(TAG, "Parsed set_led command: idle");
|
|
state_set_led(LED_STATE_OFF);
|
|
} else if (strstr(val_ptr, "\"updating\"") != NULL) {
|
|
ESP_LOGI(TAG, "Parsed set_led command: updating");
|
|
state_set_led(LED_STATE_UPDATING);
|
|
}
|
|
}
|
|
send_telemetry();
|
|
}
|
|
else if (strstr(msg_str, "\"play_audio\"") != NULL) {
|
|
char *val_ptr = strstr(msg_str, "\"value\"");
|
|
if (val_ptr != NULL) {
|
|
if (strstr(val_ptr, "\"startup\"") != NULL) {
|
|
ESP_LOGI(TAG, "Parsed play_audio command: startup");
|
|
Audio_Play_Music_To_End("file://spiffs/startup.mp3");
|
|
} else if (strstr(val_ptr, "\"wifi_connecting\"") != NULL) {
|
|
ESP_LOGI(TAG, "Parsed play_audio command: wifi_connecting");
|
|
Audio_Play_Music_To_End("file://spiffs/connecting_wifi.mp3");
|
|
} else if (strstr(val_ptr, "\"wifi_connected\"") != NULL) {
|
|
ESP_LOGI(TAG, "Parsed play_audio command: wifi_connected");
|
|
Audio_Play_Music_To_End("file://spiffs/connected_wifi.mp3");
|
|
}
|
|
}
|
|
send_telemetry();
|
|
}
|
|
else if (strstr(msg_str, "\"set_brightness\"") != NULL) {
|
|
char *val_ptr = strstr(msg_str, "\"value\"");
|
|
if (val_ptr != NULL) {
|
|
float brightness = -1.0f;
|
|
if (sscanf(val_ptr, "\"value\"%*[^0-9.-]%f", &brightness) == 1) {
|
|
if (brightness >= 0.0f && brightness <= 1.0f) {
|
|
ESP_LOGI(TAG, "Parsed set_brightness command. Mutating state to: %f", brightness);
|
|
state_set_brightness(brightness);
|
|
}
|
|
}
|
|
}
|
|
send_telemetry();
|
|
}
|
|
else if (strstr(msg_str, "\"set_haptics\"") != NULL) {
|
|
char *val_ptr = strstr(msg_str, "\"value\"");
|
|
if (val_ptr != NULL) {
|
|
int intensity = -1;
|
|
if (sscanf(val_ptr, "\"value\"%*[^0-9]%d", &intensity) == 1) {
|
|
if (intensity >= 0 && intensity <= 100) {
|
|
ESP_LOGI(TAG, "Parsed set_haptics command. Mutating state to: %d", intensity);
|
|
state_set_haptic_intensity((uint8_t)intensity);
|
|
}
|
|
}
|
|
}
|
|
send_telemetry();
|
|
}
|
|
else if (strstr(msg_str, "\"query_state\"") != NULL) {
|
|
ESP_LOGI(TAG, "Parsed query_state command. Dispatching telemetry update...");
|
|
send_telemetry();
|
|
}
|
|
else if (strstr(msg_str, "\"start_ota\"") != NULL) {
|
|
char *url = extract_json_string(msg_str, "\"url\"");
|
|
|
|
if (url != NULL) {
|
|
ESP_LOGI(TAG, "Parsed start_ota command. URL: %s", url);
|
|
ota_start(url);
|
|
free(url);
|
|
} else {
|
|
ESP_LOGE(TAG, "start_ota: missing url field");
|
|
}
|
|
}
|
|
free(msg_str);
|
|
}
|
|
}
|
|
break;
|
|
case WEBSOCKET_EVENT_ERROR:
|
|
ESP_LOGE(TAG, "Websocket error occurred!");
|
|
break;
|
|
}
|
|
}
|
|
|
|
static void socket_client_task(void *pvParameters)
|
|
{
|
|
bool was_connected = false;
|
|
|
|
while (1) {
|
|
// Wait until Wi-Fi is connected
|
|
while (!wifi_is_connected()) {
|
|
state_set_wifi_connected(false);
|
|
vTaskDelay(pdMS_TO_TICKS(1000));
|
|
}
|
|
state_set_wifi_connected(true);
|
|
|
|
EventBits_t bits = xEventGroupGetBits(s_ws_event_group);
|
|
if (!(bits & WS_CONNECTED_BIT)) {
|
|
// We are not connected!
|
|
if (was_connected) {
|
|
// We lost connection!
|
|
ESP_LOGW(TAG, "WebSocket connection lost! Playing alert...");
|
|
state_set_led(LED_STATE_SERVER_CONNECTING);
|
|
Audio_Play_Music_To_End("file://spiffs/disconnected_server.mp3");
|
|
was_connected = false;
|
|
}
|
|
|
|
ESP_LOGI(TAG, "Attempting WebSocket connection...");
|
|
state_set_led(LED_STATE_SERVER_CONNECTING);
|
|
|
|
// Play connecting audio: "Connecting to the dev server"
|
|
Audio_Play_Music_To_End("file://spiffs/connecting_server.mp3");
|
|
|
|
// Stop (if running) and start connection
|
|
esp_websocket_client_stop(s_client);
|
|
esp_websocket_client_start(s_client);
|
|
|
|
// Wait up to 10 seconds for connection
|
|
ESP_LOGI(TAG, "Waiting for websocket connection...");
|
|
bits = xEventGroupWaitBits(s_ws_event_group, WS_CONNECTED_BIT, pdFALSE, pdFALSE, pdMS_TO_TICKS(10000));
|
|
|
|
if (bits & WS_CONNECTED_BIT) {
|
|
ESP_LOGI(TAG, "WebSocket connected successfully.");
|
|
was_connected = true;
|
|
state_set_led(LED_STATE_CONNECTED);
|
|
Audio_Play_Music_To_End("file://spiffs/connected_server.mp3");
|
|
} else {
|
|
ESP_LOGE(TAG, "Failed to connect to dev server within 10 seconds!");
|
|
state_set_led(LED_STATE_SERVER_FAILED);
|
|
esp_websocket_client_stop(s_client);
|
|
Audio_Play_Music_To_End("file://spiffs/connect_server_fail.mp3");
|
|
|
|
// Play "Still not working, let me try again in a moment"
|
|
Audio_Play_Music_To_End("file://spiffs/retry_later.mp3");
|
|
|
|
// Wait 10 seconds before next connection attempt
|
|
vTaskDelay(pdMS_TO_TICKS(10000));
|
|
}
|
|
} else {
|
|
// Already connected! Keep monitoring
|
|
vTaskDelay(pdMS_TO_TICKS(1000));
|
|
}
|
|
}
|
|
}
|
|
|
|
void socket_client_init(void)
|
|
{
|
|
s_ws_event_group = xEventGroupCreate();
|
|
|
|
// Load WebSocket configuration from SPIFFS
|
|
char url_buf[128] = {0};
|
|
FILE *f = fopen("/spiffs/server_url.txt", "r");
|
|
if (f != NULL) {
|
|
if (fgets(url_buf, sizeof(url_buf), f) != NULL) {
|
|
// Strip trailing whitespace/newlines
|
|
size_t len = strlen(url_buf);
|
|
while (len > 0 && (url_buf[len - 1] == '\n' || url_buf[len - 1] == '\r' || url_buf[len - 1] == ' ')) {
|
|
url_buf[len - 1] = '\0';
|
|
len--;
|
|
}
|
|
ESP_LOGI(TAG, "Loaded WebSocket URI from SPIFFS: %s", url_buf);
|
|
}
|
|
fclose(f);
|
|
}
|
|
|
|
const char *ws_uri = "wss://example.com"; // Replace with your deployment endpoint
|
|
if (strlen(url_buf) > 0) {
|
|
ws_uri = url_buf;
|
|
} else {
|
|
ESP_LOGW(TAG, "No server_url.txt configuration found or empty, using default: %s", ws_uri);
|
|
}
|
|
|
|
esp_websocket_client_config_t websocket_cfg = {
|
|
.uri = ws_uri,
|
|
.buffer_size = 4096,
|
|
};
|
|
|
|
ESP_LOGI(TAG, "Initializing WebSocket client...");
|
|
s_client = esp_websocket_client_init(&websocket_cfg);
|
|
|
|
// Register event handler
|
|
ESP_ERROR_CHECK(esp_websocket_register_events(s_client, WEBSOCKET_EVENT_ANY, websocket_event_handler, NULL));
|
|
|
|
// Create the background connection task
|
|
ESP_LOGI(TAG, "Starting WebSocket monitor task...");
|
|
xTaskCreate(socket_client_task, "socket_client_task", 4096, NULL, 5, NULL);
|
|
}
|
|
|
|
/* ------------------------------------------------------------------
|
|
* OTA WebSocket message helpers
|
|
* Called from ota_manager.c to report OTA state back to the server.
|
|
* ------------------------------------------------------------------ */
|
|
|
|
void socket_send_ota_progress(int percent, int bytes_written, int bytes_total)
|
|
{
|
|
if (s_client == NULL || !(xEventGroupGetBits(s_ws_event_group) & WS_CONNECTED_BIT)) {
|
|
return;
|
|
}
|
|
char buf[128];
|
|
int len = snprintf(buf, sizeof(buf),
|
|
"{\"type\":\"ota_progress\",\"percent\":%d,\"bytes_written\":%d,\"bytes_total\":%d}",
|
|
percent, bytes_written, bytes_total);
|
|
if (len > 0 && len < (int)sizeof(buf)) {
|
|
esp_websocket_client_send_text(s_client, buf, len, portMAX_DELAY);
|
|
}
|
|
}
|
|
|
|
void socket_send_ota_complete(void)
|
|
{
|
|
if (s_client == NULL || !(xEventGroupGetBits(s_ws_event_group) & WS_CONNECTED_BIT)) {
|
|
return;
|
|
}
|
|
const char *msg = "{\"type\":\"ota_complete\"}";
|
|
esp_websocket_client_send_text(s_client, msg, strlen(msg), portMAX_DELAY);
|
|
}
|
|
|
|
void socket_send_ota_error(const char *reason)
|
|
{
|
|
if (s_client == NULL || !(xEventGroupGetBits(s_ws_event_group) & WS_CONNECTED_BIT)) {
|
|
return;
|
|
}
|
|
char buf[192];
|
|
int len = snprintf(buf, sizeof(buf),
|
|
"{\"type\":\"ota_error\",\"reason\":\"%s\"}", reason ? reason : "unknown");
|
|
if (len > 0 && len < (int)sizeof(buf)) {
|
|
esp_websocket_client_send_text(s_client, buf, len, portMAX_DELAY);
|
|
}
|
|
}
|