/* WebSocket link to the DeskLock gateway. * Protocol: docs/architecture.md "WebSocket protocol" — keep in sync. */ #include #include "cJSON.h" #include "esp_app_desc.h" #include "esp_log.h" #include "esp_websocket_client.h" #include "desklock.h" #include "secrets.h" static const char *TAG = "gw"; static esp_websocket_client_handle_t s_ws; static volatile bool s_connected; static void handle_text(const char *data, int len) { cJSON *root = cJSON_ParseWithLength(data, len); if (root == NULL) { return; } const cJSON *type = cJSON_GetObjectItem(root, "type"); if (!cJSON_IsString(type)) { cJSON_Delete(root); return; } face_activity(); if (strcmp(type->valuestring, "state") == 0) { const cJSON *value = cJSON_GetObjectItem(root, "value"); if (cJSON_IsString(value)) { if (strcmp(value->valuestring, "thinking") == 0) { face_set(FACE_EFFORT); } else if (strcmp(value->valuestring, "idle") == 0 && !audio_is_playing()) { face_set(FACE_IDLE); } } } else if (strcmp(type->valuestring, "transcript") == 0) { const cJSON *text = cJSON_GetObjectItem(root, "text"); ESP_LOGI(TAG, "transcript: %s", cJSON_IsString(text) ? text->valuestring : "?"); } else if (strcmp(type->valuestring, "reply_text") == 0) { const cJSON *text = cJSON_GetObjectItem(root, "text"); ESP_LOGI(TAG, "tatlock: %s", cJSON_IsString(text) ? text->valuestring : "?"); } else if (strcmp(type->valuestring, "audio_start") == 0) { audio_playback_begin(); face_set(FACE_SPEAKING); } else if (strcmp(type->valuestring, "audio_end") == 0) { audio_playback_end(); } else if (strcmp(type->valuestring, "error") == 0) { face_set(FACE_RAGE); } else if (strcmp(type->valuestring, "command") == 0) { const cJSON *action = cJSON_GetObjectItem(root, "action"); const cJSON *level = cJSON_GetObjectItem(root, "level"); if (cJSON_IsString(action)) { face_volume_command(action->valuestring, cJSON_IsNumber(level) ? level->valueint : 0); } } cJSON_Delete(root); } static void ws_event(void *arg, esp_event_base_t base, int32_t id, void *data) { (void)arg; (void)base; esp_websocket_event_data_t *ev = (esp_websocket_event_data_t *)data; switch (id) { case WEBSOCKET_EVENT_CONNECTED: { ESP_LOGI(TAG, "gateway connected"); s_connected = true; char hello[128]; snprintf(hello, sizeof(hello), "{\"type\":\"hello\",\"device\":\"desklock-p4\",\"fw\":\"%s\"}", esp_app_get_description()->version); gw_send_text(hello); face_set(FACE_IDLE); #if SDIO_TX_SELFTEST extern void sdio_tx_selftest_kick(void); sdio_tx_selftest_kick(); #endif break; } case WEBSOCKET_EVENT_DISCONNECTED: case WEBSOCKET_EVENT_ERROR: if (s_connected) { ESP_LOGW(TAG, "gateway lost"); } s_connected = false; audio_capture_stop(); app_on_disconnect(); /* Stay calm: the rain draining away signals "unreachable". A sustained * outage escalates to the x_x error face in face.c (clock_cb). */ face_set(FACE_IDLE); break; case WEBSOCKET_EVENT_DATA: if (ev->op_code == 0x01) { /* text */ handle_text(ev->data_ptr, ev->data_len); } else if (ev->op_code == 0x02 && ev->data_len > 0) { /* binary PCM */ audio_playback_feed((const uint8_t *)ev->data_ptr, ev->data_len); } break; default: break; } } void gw_start(void) { if (s_ws != NULL) { return; } esp_websocket_client_config_t cfg = { .uri = GATEWAY_WS_URI, .buffer_size = 8192, .reconnect_timeout_ms = 5000, .network_timeout_ms = 10000, .ping_interval_sec = 8, /* keep the link visibly alive through long Tatlock turns */ }; s_ws = esp_websocket_client_init(&cfg); esp_websocket_register_events(s_ws, WEBSOCKET_EVENT_ANY, ws_event, NULL); esp_websocket_client_start(s_ws); ESP_LOGI(TAG, "connecting to %s", GATEWAY_WS_URI); } bool gw_connected(void) { return s_connected; } void gw_send_text(const char *json) { if (s_connected) { esp_websocket_client_send_text(s_ws, json, strlen(json), pdMS_TO_TICKS(1500)); } } void gw_send_bin(const uint8_t *data, size_t len) { if (s_connected) { esp_websocket_client_send_bin(s_ws, (const char *)data, len, pdMS_TO_TICKS(1500)); } }