318 lines
11 KiB
C++
318 lines
11 KiB
C++
#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);
|
||
} |