Initial sanitized SmartSpeaker snapshot
This commit is contained in:
@@ -0,0 +1,384 @@
|
||||
#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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user