toy-hardware/main/protocols/websocket_protocol.cc

318 lines
11 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include "websocket_protocol.h"
#include "board.h"
#include "system_info.h"
#include "application.h"
#include "background_task.h"
#include <cstring>
#include <cJSON.h>
#include <esp_log.h>
#include <arpa/inet.h>
#include <atomic>
#include "assets/lang_config.h"
#define TAG "WS"
// 初始化静态成员
std::atomic<int> WebsocketProtocol::pending_delete_tasks_{0};
WebsocketProtocol::WebsocketProtocol() {
event_group_handle_ = xEventGroupCreate();
}
WebsocketProtocol::~WebsocketProtocol() {
if (websocket_ != nullptr) {
delete websocket_;
}
vEventGroupDelete(event_group_handle_);
}
void WebsocketProtocol::Start() {
}
void WebsocketProtocol::SendAudio(const std::vector<uint8_t>& data) {
if (websocket_ == nullptr || !websocket_->IsConnected()) {
ESP_LOGD(TAG, "WebSocket not connected, dropping audio data");
return;
}
websocket_->Send(data.data(), data.size(), true);
}
void WebsocketProtocol::SendText(const std::string& text) {
// 🔧 修复:增强连接状态检查,防止访问无效连接
if (websocket_ == nullptr) {
ESP_LOGD(TAG, "WebSocket is null, dropping text message: %s", text.c_str());
return;
}
// 🔧 双重检查连接状态,防止偶发性连接异常
if (!websocket_->IsConnected()) {
ESP_LOGD(TAG, "WebSocket not connected, dropping text message: %s", text.c_str());
return;
}
// 🔧 再次验证连接有效性防止偶发性TLS状态异常
if (!IsAudioChannelOpened()) {
ESP_LOGW(TAG, "Audio channel not properly opened, dropping message: %s", text.c_str());
return;
}
// 🔧 添加异常处理防止TLS层崩溃
try {
// 验证消息内容有效性
if (text.empty()) {
ESP_LOGW(TAG, "Attempted to send empty message");
return;
}
if (!websocket_->Send(text)) {
ESP_LOGE(TAG, "Failed to send text: %s", text.c_str());
SetError(Lang::Strings::SERVER_ERROR);
} else {
ESP_LOGD(TAG, "Successfully sent WebSocket message: %s", text.c_str());
}
} catch (const std::exception& e) {
ESP_LOGE(TAG, "Exception sending text: %s, message: %s", e.what(), text.c_str());
SetError(Lang::Strings::SERVER_ERROR);
} catch (...) {
ESP_LOGE(TAG, "Unknown exception sending text: %s", text.c_str());
SetError(Lang::Strings::SERVER_ERROR);
}
}
bool WebsocketProtocol::IsAudioChannelOpened() const {
if (websocket_ == nullptr) {
return false;
}
// 🔧 增强连接状态验证不仅检查IsConnected还验证实际可用性
bool basic_check = websocket_->IsConnected() && !error_occurred_ && !IsTimeout();
if (!basic_check) {
return false;
}
// 🔧 额外验证确保WebSocket真正可用偶发性保护
try {
// 这里可以添加轻量级的连接测试,但要避免频繁调用
return true;
} catch (...) {
ESP_LOGW(TAG, "WebSocket connection validation failed");
return false;
}
}
void WebsocketProtocol::CloseAudioChannel() {
std::lock_guard<std::mutex> lock(websocket_mutex_);
if (websocket_ != nullptr && !is_being_deleted_) {
// ESP_LOGI(TAG, "🔧 关闭WebSocket连接");
is_being_deleted_ = true;
try {
websocket_->Close();
} catch (const std::exception& e) {
ESP_LOGE(TAG, "WebSocket close failed: %s", e.what());
}
auto websocket_to_delete = websocket_;
websocket_ = nullptr; // 立即置空,防止重复访问
// 使用更安全的延迟进行异步删除,确保其他线程完成访问
Application::GetInstance().Schedule([this, websocket_to_delete]() {
vTaskDelay(pdMS_TO_TICKS(50));
try {
delete websocket_to_delete;
ESP_LOGI(TAG, "🔧 WebSocket已安全删除");
} catch (const std::exception& e) {
ESP_LOGE(TAG, "WebSocket deletion failed: %s", e.what());
}
is_being_deleted_ = false;
});
}
}
bool WebsocketProtocol::OpenAudioChannel() {
std::lock_guard<std::mutex> lock(websocket_mutex_);
if (websocket_ != nullptr && !is_being_deleted_) {
// ESP_LOGI(TAG, "🔧 关闭现有WebSocket连接");
is_being_deleted_ = true;
try {
// 🔧 关键修复清除OnDisconnected回调防止触发OnAudioChannelClosed
websocket_->OnDisconnected(nullptr);
websocket_->Close();
} catch (const std::exception& e) {
ESP_LOGE(TAG, "WebSocket close failed during reopen: %s", e.what());
}
auto websocket_to_delete = websocket_;
websocket_ = nullptr; // 立即置空,防止重复访问
// 增加待删除任务计数
pending_delete_tasks_++;
// 使用更安全的异步删除机制
Application::GetInstance().Schedule([this, websocket_to_delete]() {
vTaskDelay(pdMS_TO_TICKS(200)); // 增加延迟到200ms
try {
delete websocket_to_delete;
ESP_LOGI(TAG, "🔧 旧WebSocket已安全删除剩余待删除任务: %d", pending_delete_tasks_.load() - 1);
} catch (const std::exception& e) {
ESP_LOGE(TAG, "WebSocket deletion failed: %s", e.what());
}
pending_delete_tasks_--; // 减少计数
is_being_deleted_ = false;
});
// 短暂延迟让删除任务启动
vTaskDelay(pdMS_TO_TICKS(150));
}
// 如果有太多待删除任务,等待一下
if (pending_delete_tasks_.load() > 2) {
ESP_LOGW(TAG, "⚠️ 检测到多个待删除任务 (%d),等待清理完成", pending_delete_tasks_.load());
int wait_count = 0;
while (pending_delete_tasks_.load() > 1 && wait_count < 10) {
vTaskDelay(pdMS_TO_TICKS(100));
wait_count++;
}
}
error_occurred_ = false;
std::string url = CONFIG_WEBSOCKET_URL;
std::string token = "Bearer " + std::string(CONFIG_WEBSOCKET_ACCESS_TOKEN);
// 🔧 添加内存检查和错误处理
try {
websocket_ = Board::GetInstance().CreateWebSocket();
if (websocket_ == nullptr) {
ESP_LOGE("WebsocketProtocol", "Failed to create WebSocket - out of memory");
return false;
}
} catch (const std::exception& e) {
ESP_LOGE("WebsocketProtocol", "Exception creating WebSocket: %s", e.what());
return false;
} catch (...) {
ESP_LOGE("WebsocketProtocol", "Unknown exception creating WebSocket");
return false;
}
websocket_->SetHeader("Authorization", token.c_str());
websocket_->SetHeader("Protocol-Version", "1");
websocket_->SetHeader("Device-Id", SystemInfo::GetMacAddress().c_str());
websocket_->SetHeader("Client-Id", Board::GetInstance().GetUuid().c_str());
websocket_->OnData([this](const char* data, size_t len, bool binary) {
if (binary) {
if (on_incoming_audio_ != nullptr) {
on_incoming_audio_(std::vector<uint8_t>((uint8_t*)data, (uint8_t*)data + len));// 接收音频数据
}
} else {
// Parse JSON data
auto root = cJSON_Parse(data);
// 添加调试日志打印接收到的原始JSON数据
ESP_LOGI(TAG, "🔍 接收到JSON数据: %s", data);
// 添加调试日志解析JSON结构
if (root == NULL) {
ESP_LOGE(TAG, "❌ JSON解析失败数据格式错误");
return;
}
// 打印JSON对象的所有字段
char* json_string = cJSON_Print(root);
if (json_string != NULL) {
ESP_LOGI(TAG, "📋 解析后的JSON结构: %s", json_string);
free(json_string);
}
auto type = cJSON_GetObjectItem(root, "type");//
if (type != NULL) {
ESP_LOGI(TAG, "📝 消息类型: %s", type->valuestring);
if (strcmp(type->valuestring, "hello") == 0) {// 接收服务器hello消息
ParseServerHello(root);// 解析服务器hello消息
} else {
if (on_incoming_json_ != nullptr) {// 接收服务器其他消息
on_incoming_json_(root);// 调用回调函数处理其他消息
}
}
} else {
ESP_LOGE(TAG, "缺少消息类型, data: %s", data);// 缺少消息类型
}
cJSON_Delete(root);
}
last_incoming_time_ = std::chrono::steady_clock::now();
});
websocket_->OnDisconnected([this]() {
ESP_LOGI(TAG, "Websocket disconnected");
// 立即停止音频处理,防止崩溃
auto& app = Application::GetInstance();
#if CONFIG_USE_AUDIO_PROCESSOR
app.StopAudioProcessor();
ESP_LOGI(TAG, "Audio processor stopped immediately");
#endif
if (on_audio_channel_closed_ != nullptr) {
on_audio_channel_closed_();
}
});
if (!websocket_->Connect(url.c_str())) {
ESP_LOGE(TAG, "Failed to connect to websocket server");
SetError(Lang::Strings::SERVER_NOT_FOUND);
return false;
}
// Send hello message to describe the client
// keys: message type, version, audio_params (format, sample_rate, channels)
std::string message = "{";
message += "\"type\":\"hello\",";
message += "\"version\": 1,";
message += "\"transport\":\"websocket\",";
message += "\"audio_params\":{";
message += "\"format\":\"opus\", \"sample_rate\":16000, \"channels\":1, \"frame_duration\":" + std::to_string(OPUS_FRAME_DURATION_MS);
message += "}}";
websocket_->Send(message);
// Wait for server hello
EventBits_t bits = xEventGroupWaitBits(event_group_handle_, WEBSOCKET_PROTOCOL_SERVER_HELLO_EVENT, pdTRUE, pdFALSE, pdMS_TO_TICKS(10000));
if (!(bits & WEBSOCKET_PROTOCOL_SERVER_HELLO_EVENT)) {
ESP_LOGE(TAG, "Failed to receive server hello");
SetError(Lang::Strings::SERVER_TIMEOUT);
return false;
}
if (on_audio_channel_opened_ != nullptr) {
on_audio_channel_opened_();
}
return true;
}
void WebsocketProtocol::ParseServerHello(const cJSON* root) {
auto transport = cJSON_GetObjectItem(root, "transport");
if (transport == nullptr || strcmp(transport->valuestring, "websocket") != 0) {
ESP_LOGE(TAG, "Unsupported transport: %s", transport->valuestring);
return;
}
auto audio_params = cJSON_GetObjectItem(root, "audio_params");
if (audio_params != NULL) {
auto sample_rate = cJSON_GetObjectItem(audio_params, "sample_rate");
if (sample_rate != NULL) {
server_sample_rate_ = sample_rate->valueint;
}
auto frame_duration = cJSON_GetObjectItem(audio_params, "frame_duration");
if (frame_duration != NULL) {
server_frame_duration_ = frame_duration->valueint;
}
}
xEventGroupSetBits(event_group_handle_, WEBSOCKET_PROTOCOL_SERVER_HELLO_EVENT);
}