toy-Kapi_Rtc/main/application.cc
Rdzleo b9bbcc456c 1、新增HTTPS故事播放功能(SendStoryRequest通过蓝牙MAC请求故事API,支持intro+body两段式无缝播放);
2、新增HttpsPlaybackFromUrl通用HTTPS音频下载播放方法,obtain_story同时支持HTTPS URL和WebSocket两种方式;
3、新增RTC↔HTTPS双向音频切换三标志位状态机(opus_playback_active_/https_playback_active_/https_playback_abort_),HTTPS播放期间静默丢弃RTC PCM包,OnAudioOutput捕获is_opus_frame防止残留Opus帧杂音;
4、新增AbortHttpsPlayback中止方法,使用独立高优先级任务(priority=10)执行DMA flush;
5、协议层新增OnBotMessage回调,Bot下行消息立即中止HTTPS播放;volc_rtc_protocol移除is_binary依赖改为直接前缀检测,新增info前缀和subv跳过逻辑;
6、新增subtitle字幕消息解析,通过bot_前缀区分USER/AI,用户说话时立即中止HTTPS播放;
7、AbortSpeaking新增HTTPS中止信号和DMA缓冲区flush;
8、Kconfig新增STORY_API_URL故事播放API地址配置;

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 11:27:07 +08:00

4107 lines
192 KiB
C++
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 "application.h"
// #include "ble_service_config.h" // BLE JSON Service 暂不使用
#include "board.h"
#include "wifi_board.h"
#include "display.h"
#include "system_info.h"
#include "ml307_ssl_transport.h"
#include "audio_codec.h"
#include "settings.h"
#include "mqtt_protocol.h"
#include "websocket_protocol.h"
#include "volc_rtc_protocol.h"
#include "font_awesome_symbols.h"
#include "iot/thing_manager.h"
#include "assets/lang_config.h"
#include "volume_config.h"
#include "boards/common/qmi8658a.h" // 添加qmi8658a_data_t类型的头文件
#include "boards/movecall-moji-esp32s3/movecall_moji_esp32s3.h" // 添加MovecallMojiESP32S3类的头文件
#include "weather_api.h"
#include <cstring>
#include <esp_log.h>
#include <cJSON.h>
#include <driver/gpio.h>
#include <arpa/inet.h>
#include <esp_app_desc.h>
#include <cmath>
#include <chrono>
#include <esp_wifi.h>
#include <nvs.h>
#include <esp_http_client.h>
#include <esp_crt_bundle.h>
#define TAG "Application"
#define MAC_TAG "BluetoothMAC"
// 设备空闲无对话状态 倒计时
#define DIALOG_IDLE_COUNTDOWN_SECONDS 40
// 定义设备状态字符串
static const char* const STATE_STRINGS[] = {
"unknown",
"starting",
"configuring",
"idle",
"connecting",
"listening",
"speaking",
"dialog",
"upgrading",
"activating",
"fatal_error"
};
Application::Application() {
event_group_ = xEventGroupCreate();
background_task_ = new BackgroundTask(4096 * 8);
last_audible_output_time_ = std::chrono::steady_clock::now(); // 初始化最后一次有声音输出的时间点
skip_dialog_idle_session_ = false; // 初始化跳过对话待机会话标志为false
dialog_watchdog_running_ = false; // 初始化对话看门狗运行标志
dialog_watchdog_last_logged_ = -1; // 初始化对话看门狗日志记录
dialog_watchdog_task_handle_ = nullptr; // 初始化对话看门狗任务句柄
clock_ticks_ = 0; // 初始化时钟计数
main_loop_task_handle_ = nullptr; // 初始化主循环任务句柄
check_new_version_task_handle_ = nullptr; // 初始化版本检查任务句柄
audio_loop_task_handle_ = nullptr; // 初始化音频循环任务句柄
esp_timer_create_args_t clock_timer_args = {
.callback = [](void* arg) {
Application* app = (Application*)arg;
app->OnClockTimer();
},
.arg = this,
.dispatch_method = ESP_TIMER_TASK,
.name = "clock_timer",
.skip_unhandled_events = true
};
esp_timer_create(&clock_timer_args, &clock_timer_handle_);
}
Application::~Application() {
// 停止并清理对话看门狗
StopDialogWatchdog();
if (clock_timer_handle_ != nullptr) {
esp_timer_stop(clock_timer_handle_);
esp_timer_delete(clock_timer_handle_);
}
if (background_task_ != nullptr) {
delete background_task_;
}
if (recorder_pipeline_) {
recorder_pipeline_close(recorder_pipeline_);
recorder_pipeline_ = nullptr;
}
if (player_pipeline_) {
player_pipeline_close(player_pipeline_);
player_pipeline_ = nullptr;
}
vEventGroupDelete(event_group_);
}
void Application::CheckNewVersion() {
// ESP_LOGI(TAG, "OTA版本检查已临时禁用");
// return;
auto& board = Board::GetInstance();
auto display = board.GetDisplay();
// Check if there is a new firmware version available
ota_.SetPostData(board.GetJson());// 发送当前设备的JSON数据到OTA服务器用于检查是否有新的固件版本 包办板载信息 BOARD_TYPE
const int MAX_RETRY = 10;
int retry_count = 0;
while (true) {
if (!ota_.CheckVersion()) {
retry_count++;
if (retry_count >= MAX_RETRY) {
ESP_LOGE(TAG, "Too many retries, exit version check");
return;
}
ESP_LOGW(TAG, "Check new version failed, retry in %d seconds (%d/%d)", 60, retry_count, MAX_RETRY);
vTaskDelay(pdMS_TO_TICKS(60000));
continue;
}
retry_count = 0;
if (ota_.HasNewVersion()) {
Alert(Lang::Strings::OTA_UPGRADE, Lang::Strings::UPGRADING, "happy", Lang::Sounds::P3_UPGRADE);
// Wait for the chat state to be idle
do {
vTaskDelay(pdMS_TO_TICKS(3000));
} while (GetDeviceState() != kDeviceStateIdle);
// Use main task to do the upgrade, not cancelable
Schedule([this, display]() {
SetDeviceState(kDeviceStateUpgrading);
display->SetIcon(FONT_AWESOME_DOWNLOAD);
std::string message = std::string(Lang::Strings::NEW_VERSION) + ota_.GetFirmwareVersion();
display->SetChatMessage("system", message.c_str());
auto& board = Board::GetInstance();
board.SetPowerSaveMode(false);// 关闭低功耗模式
#if CONFIG_USE_WAKE_WORD_DETECT || CONFIG_USE_CUSTOM_WAKE_WORD
wake_word_detect_.Stop();
#endif
// 预先关闭音频输出,避免升级过程有音频操作
auto codec = board.GetAudioCodec();
codec->EnableInput(false);
codec->EnableOutput(false);
{
std::lock_guard<std::mutex> lock(mutex_);
audio_decode_queue_.clear();
}
background_task_->WaitForCompletion();
delete background_task_;
background_task_ = nullptr;
vTaskDelay(pdMS_TO_TICKS(1000));
ota_.StartUpgrade([display](int progress, size_t speed) {
char buffer[64];
snprintf(buffer, sizeof(buffer), "%d%% %zuKB/s", progress, speed / 1024);
display->SetChatMessage("system", buffer);
});
// If upgrade success, the device will reboot and never reach here
display->SetStatus(Lang::Strings::UPGRADE_FAILED);
ESP_LOGI(TAG, "Firmware upgrade failed...");
vTaskDelay(pdMS_TO_TICKS(3000));
Reboot();
});
return;
}
// No new version, mark the current version as valid
ota_.MarkCurrentVersionValid();
std::string message = std::string(Lang::Strings::VERSION) + ota_.GetCurrentVersion();
display->ShowNotification(message.c_str());
// 检查是否有设备激活码
// if (ota_.HasActivationCode()) {
// // Activation code is valid
// SetDeviceState(kDeviceStateActivating);//设置设备状态为激活中
// // ShowActivationCode();//显示设备激活码
// // Check again in 60 seconds or until the device is idle
// for (int i = 0; i < 60; ++i) {
// if (device_state_ == kDeviceStateIdle) {
// break;
// }
// vTaskDelay(pdMS_TO_TICKS(1000));
// }
// continue;
// }
SetDeviceState(kDeviceStateIdle);
display->SetChatMessage("system", "");
ResetDecoder();
PlaySound(Lang::Sounds::P3_SUCCESS);
// Exit the loop if upgrade or idle
break;
}
}
// 取消设备激活码播报,当前设备绑定使用Wi-Fi的Mac地址进行绑定
// void Application::ShowActivationCode() {
// auto& message = ota_.GetActivationMessage();
// auto& code = ota_.GetActivationCode();
// struct digit_sound {
// char digit;
// const std::string_view& sound;
// };
// static const std::array<digit_sound, 10> digit_sounds{{
// digit_sound{'0', Lang::Sounds::P3_0},
// digit_sound{'1', Lang::Sounds::P3_1},
// digit_sound{'2', Lang::Sounds::P3_2},
// digit_sound{'3', Lang::Sounds::P3_3},
// digit_sound{'4', Lang::Sounds::P3_4},
// digit_sound{'5', Lang::Sounds::P3_5},
// digit_sound{'6', Lang::Sounds::P3_6},
// digit_sound{'7', Lang::Sounds::P3_7},
// digit_sound{'8', Lang::Sounds::P3_8},
// digit_sound{'9', Lang::Sounds::P3_9}
// }};
// // This sentence uses 9KB of SRAM, so we need to wait for it to finish
// Alert(Lang::Strings::ACTIVATION, message.c_str(), "happy", Lang::Sounds::P3_ACTIVATION);
// vTaskDelay(pdMS_TO_TICKS(1000));
// background_task_->WaitForCompletion();
// for (const auto& digit : code) {
// auto it = std::find_if(digit_sounds.begin(), digit_sounds.end(),
// [digit](const digit_sound& ds) { return ds.digit == digit; });
// if (it != digit_sounds.end()) {
// PlaySound(it->sound);
// }
// }
// }
// 新增代码(小程序控制 暂停播放 音频)
// =========================================================
void Application::PauseAudioPlayback() {
std::unique_lock<std::mutex> lock(mutex_);
if (!audio_paused_) {
audio_paused_ = true;// 暂停播放(更新标志位)
ESP_LOGI(TAG, "🔇 从服务器接收到暂停播放指令");
// 恢复原始处理方式:立即停止音频输出
auto codec = Board::GetInstance().GetAudioCodec();
if (codec) {
codec->EnableOutput(false);// 暂停时立即停止音频输出
ESP_LOGI(TAG, "⏸️ 音频编解码器输出已禁用,实现立即暂停");
}
ESP_LOGI(TAG, "⏸️ 音频播放已暂停");
}
}
// 新增代码(小程序控制 继续播放 音频)
void Application::ResumeAudioPlayback() {
std::unique_lock<std::mutex> lock(mutex_);
if (audio_paused_) {
audio_paused_ = false;// 恢复播放(更新标志位)
ESP_LOGI(TAG, "<EFBFBD> 从服务器接收到继续播放指令");
// 恢复原始处理方式:重新启用音频输出
auto codec = Board::GetInstance().GetAudioCodec();
if (codec) {
codec->EnableOutput(true);// 恢复时重新启用音频输出
ESP_LOGI(TAG, "▶️ 音频编解码器输出已启用");
}
ESP_LOGI(TAG, "▶️ 音频播放已恢复");
}
}
// =========================================================
void Application::Alert(const char* status, const char* message, const char* emotion, const std::string_view& sound) {
ESP_LOGW(TAG, "Alert %s: %s [%s]", status, message, emotion);
auto display = Board::GetInstance().GetDisplay();
display->SetStatus(status);
display->SetEmotion(emotion);
display->SetChatMessage("system", message);
if (!sound.empty()) {
ResetDecoder();
PlaySound(sound);
}
}
void Application::DismissAlert() {
if (device_state_ == kDeviceStateIdle) {
auto display = Board::GetInstance().GetDisplay();
display->SetStatus(Lang::Strings::STANDBY);
display->SetEmotion("neutral");
display->SetChatMessage("system", "");
}
}
// 播放音频文件的函数,用于播放存储在内存中的音频数据
void Application::PlaySound(const std::string_view& sound) {
// The assets are encoded at 16000Hz, 60ms frame duration
SetDecodeSampleRate(16000, 60);
const char* data = sound.data();
size_t size = sound.size();
for (const char* p = data; p < data + size; ) {
auto p3 = (BinaryProtocol3*)p;
p += sizeof(BinaryProtocol3);
auto payload_size = ntohs(p3->payload_size);
std::vector<uint8_t> opus;
opus.resize(payload_size);
memcpy(opus.data(), p3->payload, payload_size);
p += payload_size;
std::lock_guard<std::mutex> lock(mutex_);
audio_decode_queue_.emplace_back(std::move(opus));
}
}
// 切换聊天状态的函数,用于在不同的设备状态之间进行切换
void Application::ToggleChatState() {
// 如果当前设备状态是激活中,则将状态设置为空闲并返回
if (device_state_ == kDeviceStateActivating) {
SetDeviceState(kDeviceStateIdle); // 设置设备状态为空闲
return; // 直接返回,不执行后续逻辑
}
// 检查协议对象是否已初始化
if (!protocol_) {
ESP_LOGE(TAG, "协议未初始化"); // 记录错误日志:协议未初始化
return; // 协议未初始化则直接返回
}
// 如果当前设备状态是idle空闲则尝试进入对话模式
if (device_state_ == kDeviceStateIdle) {
Schedule([this]() {
SetDeviceState(kDeviceStateConnecting);
ESP_LOGI(TAG, "正在尝试打开音频通道");
Board::GetInstance().SetPowerSaveMode(false);// 关闭低功耗模式
if (!protocol_->OpenAudioChannel()) {
auto ac = Board::GetInstance().GetAudioCodec();
ESP_LOGW(TAG, "打开音频通道失败将在2秒后重试");
if (ac) {
ESP_LOGW(TAG, "Diag: codec out_channels=%d in_channels=%d out_sr=%d in_sr=%d", ac->output_channels(), ac->input_channels(), ac->output_sample_rate(), ac->input_sample_rate());
}
SetDeviceState(kDeviceStateIdle);
Schedule([this]() {
vTaskDelay(pdMS_TO_TICKS(2000));
ESP_LOGI(TAG, "正在重试音频通道连接");
ToggleChatState();// 打开音频通道
});
return;
}
listening_mode_ = kListeningModeRealtime;// 设置监听模式为实时监听
SetDeviceState(kDeviceStateDialog);// 设置设备状态为对话模式
protocol_->SendStartListening(listening_mode_);// 发送开始监听消息
auto codec = Board::GetInstance().GetAudioCodec();// 获取音频编解码器
if (codec) {
codec->EnableOutput(true);// 启用音频输出
}
ESP_LOGI(TAG, "进入对话框状态:启用全双工");
});
} else if (device_state_ == kDeviceStateDialog) {
Schedule([this]() {
// protocol_->CloseAudioChannel();// 关闭音频通道
// ESP_LOGI(TAG, "关闭音频通道并切换到空闲状态");// 关闭音频通道并切换到空闲状态
protocol_->SendStartListening(listening_mode_);// 发送开始监听消息
});
} else if (device_state_ == kDeviceStateSpeaking) {
Schedule([this]() {
AbortSpeaking(kAbortReasonNone);
protocol_->CloseAudioChannel();
ESP_LOGI(TAG, "关闭音频通道并切换到空闲状态");// 关闭音频通道并切换到空闲状态
});
} else if (device_state_ == kDeviceStateListening || (listening_mode_ == kListeningModeRealtime && device_state_ == kDeviceStateSpeaking)) {
Schedule([this]() {
protocol_->CloseAudioChannel();
ESP_LOGI(TAG, "关闭音频通道并切换到空闲状态");// 关闭音频通道并切换到空闲状态
});
}
}
void Application::ToggleListeningState() {
if (device_state_ == kDeviceStateActivating) {
SetDeviceState(kDeviceStateIdle);
return;
}
if (!protocol_) {
ESP_LOGE(TAG, "协议未初始化!");// 记录错误日志:协议未初始化
return;
}
// 简单的状态切换idle <-> listening
if (device_state_ == kDeviceStateIdle) {
// 从待命状态进入聆听状态
Schedule([this]() {
SetDeviceState(kDeviceStateConnecting);
if (!protocol_->OpenAudioChannel()) {
return;
}
SetListeningMode(kListeningModeManualStop);
ESP_LOGI(TAG, "中断按钮:进入聆听状态");// 中断按钮:进入聆听状态
});
} else if (device_state_ == kDeviceStateListening) {
// 从聆听状态返回待命状态
Schedule([this]() {
protocol_->CloseAudioChannel();
ESP_LOGI(TAG, "中断按钮:返回待命状态");// 中断按钮:返回待命状态
});
} else if (device_state_ == kDeviceStateSpeaking) {
// 如果正在说话,中止说话并返回待命状态
Schedule([this]() {
AbortSpeaking(kAbortReasonNone);
if (protocol_) {
protocol_->CloseAudioChannel();
}
SetDeviceState(kDeviceStateIdle);
ESP_LOGI(TAG, "中断按钮:停止说话,关闭音频通道并返回待命状态");// 中断按钮:停止说话,关闭音频通道并返回待命状态
});
} else if (device_state_ == kDeviceStateConnecting) {
// 如果正在连接,直接返回待命状态
Schedule([this]() {
SetDeviceState(kDeviceStateIdle);
ESP_LOGI(TAG, "中断按钮:取消连接并返回待命状态");// 中断按钮:取消连接并返回待命状态
});
}
}
void Application::StartListening() {
if (device_state_ == kDeviceStateActivating) {
SetDeviceState(kDeviceStateIdle);
return;
}
if (!protocol_) {
ESP_LOGE(TAG, "协议未初始化!");// 记录错误日志:协议未初始化
return;
}
if (device_state_ == kDeviceStateIdle) {
Schedule([this]() {
if (!protocol_->IsAudioChannelOpened()) {
SetDeviceState(kDeviceStateConnecting);// 切换到连接状态
if (!protocol_->OpenAudioChannel()) {
return;
}
}
SetListeningMode(kListeningModeManualStop);// 设置监听模式为手动停止
});
} else if (device_state_ == kDeviceStateSpeaking) {
Schedule([this]() {
AbortSpeaking(kAbortReasonNone);// 中止说话
SetListeningMode(kListeningModeManualStop);// 设置监听模式为手动停止
});
}
}
void Application::StopListening() {
Schedule([this]() {
if (device_state_ == kDeviceStateListening) {
protocol_->SendStopListening();
SetDeviceState(kDeviceStateIdle);
}
});
}
// 🔊 发送文本消息到RTC传入大模型上下文信息
void Application::SendTextMessage(const std::string& text) {
if (!protocol_) {
ESP_LOGE(TAG, "协议未初始化!");// 记录错误日志:协议未初始化
return;
}
if (device_state_ == kDeviceStateIdle) {
Schedule([this, text]() {
SetDeviceState(kDeviceStateConnecting);// 切换到连接状态
if (!protocol_->OpenAudioChannel()) {
return;
}
SetDeviceState(kDeviceStateDialog);
protocol_->SendStartListening(listening_mode_);
// 发送文本消息
protocol_->SendTextMessage(text);
ESP_LOGI(TAG, "发送文本消息:%s", text.c_str());// 发送文本消息:%s
// 立即启动监听模式以接收语音回复
ESP_LOGI(TAG, "realtime_chat_enabled_=%s", realtime_chat_enabled_ ? "true" : "false");
SetListeningMode(realtime_chat_enabled_ ? kListeningModeRealtime : kListeningModeManualStop);
});
} else if (device_state_ == kDeviceStateDialog) {
Schedule([this, text]() {
// if (!protocol_->IsAudioChannelOpened()) {// 如果音频通道未打开
// if (!protocol_->OpenAudioChannel()) {// 尝试打开音频通道
// return;
// }
// }
if (!dialog_upload_enabled_) {
SetDialogUploadEnabled(true);// 启用对话上传
protocol_->SendStartListening(listening_mode_);// 发送开始监听消息
}
protocol_->SendTextMessage(text);// 发送文本消息
ESP_LOGI(TAG, "发送文本消息:%s", text.c_str());// 发送文本消息:%s
});
} else if (device_state_ == kDeviceStateSpeaking) {
Schedule([this, text]() {
AbortSpeaking(kAbortReasonNone);
protocol_->SendTextMessage(text);
ESP_LOGI(TAG, "发送文本消息:%s", text.c_str());// 发送文本消息:%s
// 启动监听模式以接收语音回复
ESP_LOGI(TAG, "realtime_chat_enabled_=%s", realtime_chat_enabled_ ? "true" : "false");
SetListeningMode(realtime_chat_enabled_ ? kListeningModeRealtime : kListeningModeManualStop);
});
} else if (device_state_ == kDeviceStateListening) {
Schedule([this, text]() {
protocol_->SendTextMessage(text);
ESP_LOGI(TAG, "发送文本消息:%s", text.c_str());// 发送文本消息:%s
});
}
}
void Application::Start() {
auto& board = Board::GetInstance();
SetDeviceState(kDeviceStateStarting);
// 读取NVS中的重启标志
Settings sys("system", true);
int32_t reboot_dlg_idle = sys.GetInt("reboot_dlg_idle", 0);
int32_t reboot_origin = sys.GetInt("reboot_origin", 0);
// 检查是否是因为对话空闲倒计时而重启的
if (reboot_dlg_idle == 1 && reboot_origin == 1) {
ESP_LOGI(TAG, "检测到对话空闲倒计时重启标志,将跳过开机播报和网络连接播报");
skip_dialog_idle_session_ = true;
Settings sysclr("system", true);
sysclr.SetInt("reboot_dlg_idle", 0);
sysclr.SetInt("reboot_origin", 0);
sysclr.Commit();
} else {
ESP_LOGI(TAG, "正常启动流程,将执行开机播报和网络连接播报");
skip_dialog_idle_session_ = false;
}
/* Setup the display */
auto display = board.GetDisplay();
/* Setup the audio codec */
auto codec = board.GetAudioCodec();
opus_decoder_ = std::make_unique<OpusDecoderWrapper>(codec->output_sample_rate(), 1, OPUS_FRAME_DURATION_MS);
opus_encoder_ = std::make_unique<OpusEncoderWrapper>(16000, 1, OPUS_FRAME_DURATION_MS);
if (realtime_chat_enabled_) {
ESP_LOGI(TAG, "实时聊天已启用将opus编码器复杂度设置为0");// 实时聊天已启用将opus编码器复杂度设置为0
opus_encoder_->SetComplexity(0);
} else if (board.GetBoardType() == "ml307") {
ESP_LOGI(TAG, "检测到ML307板卡将opus编码器复杂度设置为5");// 检测到ML307板卡将opus编码器复杂度设置为5
opus_encoder_->SetComplexity(5);
} else {
ESP_LOGI(TAG, "检测到WiFi板卡将opus编码器复杂度设置为3");// 检测到WiFi板卡将opus编码器复杂度设置为3
opus_encoder_->SetComplexity(3);
}
if (codec->input_sample_rate() != 16000) {
input_resampler_.Configure(codec->input_sample_rate(), 16000);
reference_resampler_.Configure(codec->input_sample_rate(), 16000);
}
uplink_resampler_.Configure(16000, 8000);
codec->Start();
{
int battery_level = 0;
bool charging = false;
bool discharging = false;
if (board.GetBatteryLevel(battery_level, charging, discharging)) {
// 如果电池电量低于25%则将输出音量设置为0静音
if (battery_level <= 25) {
codec->SetOutputVolumeRuntime(0);
} else {
Settings s("audio", false);
int vol = s.GetInt("output_volume", AudioCodec::default_output_volume());
if (vol <= 0) {
vol = AudioCodec::default_output_volume();
}
codec->SetOutputVolumeRuntime(vol);// 设置运行时输出音量
}
}
}
// // 在启动阶段创建并运行播放管道以统一输出(开机启动播放管道)
// if (!player_pipeline_) {
// player_pipeline_ = player_pipeline_open();
// player_pipeline_run(player_pipeline_);
// }
xTaskCreatePinnedToCore([](void* arg) {
Application* app = (Application*)arg;
app->AudioLoop();
vTaskDelete(NULL);
}, "audio_loop", 4096 * 3, this, 8, &audio_loop_task_handle_, realtime_chat_enabled_ ? 1 : 0);
/* Start the main loop */
xTaskCreatePinnedToCore([](void* arg) {
Application* app = (Application*)arg;
app->MainLoop();
vTaskDelete(NULL);
}, "main_loop", 4096 * 3, this, 4, &main_loop_task_handle_, 0);
// 根据标志决定是否播放开机播报语音
if (!skip_dialog_idle_session_) {
ESP_LOGI(TAG, "设备启动完成,播放开机播报语音");// 设备启动完成,播放开机播报语音
//PlaySound(Lang::Sounds::P3_KAIJIBOBAO); 原有蜡笔小新音色
if(strcmp(CONFIG_DEVICE_ROLE, "KAKA") == 0){
PlaySound(Lang::Sounds::P3_KAKA_KAIJIBOBAO);
}
else if(strcmp(CONFIG_DEVICE_ROLE, "RTC_Test") == 0){
PlaySound(Lang::Sounds::P3_LALA_KAIJIBOBAO);
}
} else {
ESP_LOGI(TAG, "跳过开机播报语音");
}
/* Wait for the network to be ready */
board.StartNetwork();
// Initialize the protocol
display->SetStatus(Lang::Strings::LOADING_PROTOCOL);
#if CONFIG_CONNECTION_TYPE_VOLC_RTC
auto volc_protocol = std::make_unique<VolcRtcProtocol>();// 初始化VolcRtc协议
// 设置AgentConfig: 这里的配置会在RTC入会时透传给云端
// WelcomeMessage: 设置开场白
// std::string agent_config = "{\"agent_config\":{\"WelcomeMessage\":\"我是推销员雷军,有什么产品可以帮您介绍的嘛\"}}"; // 已请求成功,配置生效
// std::string config = "{\"Config\":{\"WebSearchAgentConfig\":{\"ComfortWords\":\"啦啦正在上网查询,等一下哦~\"}}}"; // 已请求成功,配置生效
// std::string config = "{\"Config\":{\"WebSearchAgentConfig\":{\"ParamsString\":\"{\\\"bot_id\\\":\\\"7585449675889608233\\\",\\\"stream\\\":true,\\\"location_info\\\":{\\\"city\\\":\\\"上海\\\"}}\"}}}";// 已经请求成功,无报错,配置不生效
std::string city = g_weather_api.GetDefaultLocation();// 获取当前默认城市信息
wifi_config_t wc{};// 获取当前WiFi配置
esp_wifi_get_config(WIFI_IF_STA, &wc);// 获取当前WiFi配置
std::string ssid = std::string(reinterpret_cast<char*>(wc.sta.ssid));// 获取当前WiFi SSID
wifi_ap_record_t ap{};// 获取当前AP信息
std::string bssid;// 获取当前AP BSSID
if (esp_wifi_sta_get_ap_info(&ap) == ESP_OK) {
char buf[18];
snprintf(buf, sizeof(buf), "%02x:%02x:%02x:%02x:%02x:%02x",ap.bssid[0], ap.bssid[1], ap.bssid[2],ap.bssid[3], ap.bssid[4], ap.bssid[5]);
bssid.assign(buf);
}
nvs_handle_t h;
// 从NVS中读取当前WiFi SSID和BSSID对应的城市信息
if (nvs_open("wifi_city_map", NVS_READONLY, &h) == ESP_OK) {
auto try_get = [&](const std::string& key)->std::string{
size_t len = 0;
if (nvs_get_str(h, key.c_str(), NULL, &len) == ESP_OK && len > 0) {
std::vector<char> buf(len);
if (nvs_get_str(h, key.c_str(), buf.data(), &len) == ESP_OK) {
return std::string(buf.data());
}
}
return std::string();// 如果NVS中没有对应城市信息返回空字符串
};
// 从NVS中读取当前WiFi SSID和BSSID对应的城市信息
if (!ssid.empty()) {
std::string city_hit;// 从NVS中读取当前WiFi SSID和BSSID对应的城市信息
if (!bssid.empty()) {
city_hit = try_get(ssid + "|" + bssid);// 从NVS中读取当前WiFi SSID和BSSID对应的城市信息
}
if (city_hit.empty()) {
city_hit = try_get(ssid);// 从NVS中读取当前WiFi SSID对应的城市信息
}
if (!city_hit.empty()) {
city = city_hit;// 如果NVS中存在对应城市信息更新当前城市信息
}
}
nvs_close(h);// 关闭NVS句柄
}
// 更新config参数
std::string params = std::string("{\\\"bot_id\\\":\\\"7585449675889608233\\\",\\\"stream\\\":true,\\\"location_info\\\":{\\\"city\\\":\\\"") + city + "\\\"}}";
std::string config = std::string("{\"Config\":{\"WebSearchAgentConfig\":{\"ParamsString\":\"") + params + "\"}}}";
volc_protocol->SetAgentConfig(config);// 设置AgentConfig: 这里的配置会在RTC入会时透传给云端 WebSearchAgentConfigvxiassfdfdfdevde
protocol_ = std::move(volc_protocol);
#elif CONFIG_CONNECTION_TYPE_WEBSOCKET
protocol_ = std::make_unique<WebsocketProtocol>();
#else
protocol_ = std::make_unique<MqttProtocol>();
#endif
protocol_->OnNetworkError([this](const std::string& message) {
// SetDeviceState(kDeviceStateIdle);
// Alert(Lang::Strings::ERROR, message.c_str(), "sad", Lang::Sounds::P3_EXCLAMATION);
ESP_LOGW(TAG, "网络错误发生:%s", message.c_str());// 网络错误发生:%s
// 检查是否是TLS连接重置错误
if (message.find("TLS") != std::string::npos || message.find("-76") != std::string::npos) {
ESP_LOGI(TAG, "检测到TLS连接重置错误将在3秒后自动重试连接");// 检测到TLS连接重置错误将在3秒后自动重试连接
SetDeviceState(kDeviceStateIdle);
// 3秒后自动重试连接
Schedule([this]() {
vTaskDelay(pdMS_TO_TICKS(3000));
if (GetDeviceState() == kDeviceStateIdle) {
ESP_LOGI(TAG, "自动重试连接TLS错误已解决");// 自动重试连接TLS错误已解决
ToggleChatState();
}
});
} else {
// 其他网络错误正常处理
SetDeviceState(kDeviceStateIdle);
Alert(Lang::Strings::ERROR, message.c_str(), "sad", Lang::Sounds::P3_EXCLAMATION);
}
});
// 收到Bot下行消息subv字幕等立即中止HTTPS音频播放
// 不等音频PCM到达再中止避免故事在Bot回复期间继续播放数秒
// 收到非字幕的Bot下行消息ctrl/conv/tool/info等时中止HTTPS播放
// subv字幕消息在协议层跳过此回调由subtitle handler处理可区分USER/AI
protocol_->OnBotMessage([this]() {
if (https_playback_active_.load() && !https_playback_abort_.load()) {
AbortHttpsPlayback("收到Bot响应消息");
}
});
protocol_->OnIncomingAudio([this](std::vector<uint8_t>&& data) {
// HTTPS播放中含HTTP请求阶段静默丢弃RTC PCM包
// opus_playback_active_ 在任务启动时即设置覆盖HTTP请求阶段
// https_playback_active_ 在音频入队时设置,覆盖音频播放阶段
if (https_playback_active_.load() || opus_playback_active_.load()) {
return;
}
if (websocket_protocol_ && websocket_protocol_->IsAudioChannelOpened()) {
aborted_ = true;
{
std::lock_guard<std::mutex> lock(mutex_);// 🔒 保护音频队列操作
// 如果音频队列不为空
if (!audio_decode_queue_.empty()) {
ESP_LOGI(TAG, "清空音频队列,大小=%zu", audio_decode_queue_.size());
audio_decode_queue_.clear();// 清空音频队列
}
}
ResetDecoder();
ws_downlink_enabled_.store(false);
opus_playback_active_.store(false);
websocket_protocol_->CloseAudioChannel();// 关闭WebSocket通道
Schedule([this]() {
vTaskDelay(pdMS_TO_TICKS(120));
aborted_ = false;
});
}
std::lock_guard<std::mutex> lock(mutex_);
size_t len = data.size();
audio_decode_queue_.emplace_back(std::move(data));
static bool first_enqueue_logged = false;
if (!first_enqueue_logged && len > 0) {
ESP_LOGI(TAG, "收到下行音频首包入队: 字节=%zu", len);
first_enqueue_logged = true;
}
ESP_LOGD(TAG, "收到下行音频入队: 字节=%zu 队列大小=%zu", len, audio_decode_queue_.size());
});
protocol_->OnAudioChannelOpened([this, codec, &board]() {
ESP_LOGI(TAG, "🟢 音频通道已打开");
ESP_LOGI(TAG, "当前设备状态: %s", STATE_STRINGS[device_state_]);
// 🔧 关键修复:立即取消所有待执行的电源管理任务
static TaskHandle_t power_save_task = nullptr;
if (power_save_task != nullptr) {
vTaskDelete(power_save_task);
power_save_task = nullptr;
ESP_LOGI(TAG, "🔧 取消了待执行的电源管理任务");
}
// 唤醒PowerSaveTimer从低功耗模式恢复到正常模式
board.WakeUp();
// 立即禁用电源管理,确保连接稳定
ESP_LOGI(TAG, "🔄 禁用电源低功耗管理模式");
board.SetPowerSaveMode(false);
// 关键修复:检查服务器采样率与设备输出采样率是否匹配
if (protocol_->server_sample_rate() != codec->output_sample_rate()) {
ESP_LOGW(TAG, "⚠️ 服务器采样率 %d 与设备输出采样率 %d 不匹配,重采样可能导致失真",
protocol_->server_sample_rate(), codec->output_sample_rate());
}
// 设置解码采样率和帧持续时间
SetDecodeSampleRate(protocol_->server_sample_rate(), protocol_->server_frame_duration());
// 关键修复:明确启用音频编解码器输出
ESP_LOGI(TAG, "🔊 启用音频编解码器输出");
codec->EnableOutput(true);// 启用音频编解码器输出
if (!player_pipeline_) {
player_pipeline_ = player_pipeline_open();
player_pipeline_run(player_pipeline_);
}
// 发送IoT状态信息
auto& thing_manager = iot::ThingManager::GetInstance();
protocol_->SendIotDescriptors(thing_manager.GetDescriptorsJson());
std::string states;
if (thing_manager.GetStatesJson(states, false)) {
protocol_->SendIotStates(states);
}
// if (websocket_protocol_ && !websocket_protocol_->IsAudioChannelOpened()) {
// ESP_LOGI(TAG, "WS辅助通道连接");
// websocket_protocol_->OpenAudioChannel();//
// }
// 🔧 修复RTC连接后切换到Speaking状态以播放欢迎语音
ESP_LOGI(TAG, "🔄 音频通道打开,准备播放欢迎语音");
if (GetDeviceState() != kDeviceStateDialog) {
SetDeviceState(kDeviceStateSpeaking);
}
ESP_LOGI(TAG, "当前设备状态: %s", STATE_STRINGS[device_state_]);
ESP_LOGI(TAG, "🟢 音频通道初始化完成");
});
protocol_->OnAudioChannelClosed([this, &board]() {
ESP_LOGI(TAG, "🔴 音频通道关闭,开始清理任务");
// 🔧 关键修复:取消所有待执行的电源管理任务,防止时序冲突
static TaskHandle_t power_save_task = nullptr;
if (power_save_task != nullptr) {
vTaskDelete(power_save_task);
power_save_task = nullptr;
ESP_LOGI(TAG, "🔧 取消了之前的电源管理任务,防止时序冲突");
}
// 音频处理器已经在WebSocket断开时停止了
// 等待所有后台任务完成
background_task_->WaitForCompletion();
if (player_pipeline_) {
player_pipeline_close(player_pipeline_);
player_pipeline_ = nullptr;
}
ESP_LOGI(TAG, "🔴 后台任务完成");
// 🔧 方案2先设置设备状态再启用电源管理避免时序问题
Schedule([this, &board]() {
ESP_LOGI(TAG, "🔄 设置设备为空闲状态");
auto display = Board::GetInstance().GetDisplay();
display->SetChatMessage("system", "");
SetDeviceState(kDeviceStateIdle);
// 状态设置完成后,再启用电源管理
vTaskDelay(pdMS_TO_TICKS(100));
ESP_LOGI(TAG, "🔄 设备已稳定在idle状态启用电源低功耗管理");
try {
board.SetPowerSaveMode(true);
} catch (...) {
ESP_LOGE(TAG, "❌ 设置电源管理模式失败");
}
});
});
protocol_->OnIncomingJson([this, display](const cJSON* root) {
// Parse JSON data
auto type = cJSON_GetObjectItem(root, "type");
if (!(type && cJSON_IsString(type))) {
auto tool_calls = cJSON_GetObjectItem(root, "tool_calls");
if (tool_calls && cJSON_IsArray(tool_calls)) {
for (int i = 0; i < cJSON_GetArraySize(tool_calls); ++i) {
cJSON* call = cJSON_GetArrayItem(tool_calls, i);
cJSON* fn = cJSON_GetObjectItem(call, "function");
if (fn && cJSON_IsObject(fn)) {
cJSON* name = cJSON_GetObjectItem(fn, "name");
cJSON* args = cJSON_GetObjectItem(fn, "arguments");
cJSON* args_obj = nullptr;
const char* args_str = (args && cJSON_IsString(args) && args->valuestring) ? args->valuestring : "";
if (args && cJSON_IsString(args) && args->valuestring) {
args_obj = cJSON_Parse(args->valuestring);
}
if (name && cJSON_IsString(name) && name->valuestring) {
if (args_obj) {
char* printed = cJSON_PrintUnformatted(args_obj);
if (printed) {
ESP_LOGI(TAG, "工具调用: name=%s arguments=%s", name->valuestring, printed);
cJSON_free(printed);
} else {
ESP_LOGI(TAG, "工具调用: name=%s arguments=%s", name->valuestring, args_str);
}
if (strcmp(name->valuestring, "adjust_audio_val") == 0) {
auto codec = Board::GetInstance().GetAudioCodec();
int user = HARDWARE_TO_USER_VOLUME(codec->output_volume());
cJSON* v = cJSON_GetObjectItem(args_obj, "value");
if (!v) v = cJSON_GetObjectItem(args_obj, "action");
if (v) {
std::string msg;
if (cJSON_IsString(v) && v->valuestring) {
if (strcmp(v->valuestring, "up") == 0) {
user += 10;
msg = "音量已经调大了哦~";
} else if (strcmp(v->valuestring, "down") == 0) {
user -= 10;
msg = "音量已经调小了哦~";
} else {
// 处理字符串形式的数字
char* endptr;
long val = strtol(v->valuestring, &endptr, 10);
if (*endptr == '\0' && val >= 0 && val <= 100) {
user = (int)val;
msg = std::string("音量值已经调整为") + std::to_string(user) + "%";
}
}
} else if (cJSON_IsNumber(v)) {
user = (int)v->valuedouble;
msg = std::string("音量值已经调整为") + std::to_string(user) + "%";
}
if (user > 100) user = 100;
if (user < 0) user = 0;
int mapped = USER_TO_HARDWARE_VOLUME(user);
codec->SetOutputVolume(mapped);
ESP_LOGI(TAG, "设置音量: 用户=%d%% 硬件=%d%%", user, mapped);
if (!msg.empty()) {
cJSON* call_id_item = cJSON_GetObjectItem(call, "id");
const char* call_id = (call_id_item && cJSON_IsString(call_id_item) && call_id_item->valuestring) ? call_id_item->valuestring : "";
if (protocol_ && call_id && call_id[0] != '\0') {
protocol_->SendFunctionResult(call_id, msg);
} else if (protocol_) {
protocol_->SendTextMessage(msg);
}
}
}
}
// 讲故事功能支持HTTPS下载或WebSocket两种方式
else if (strcmp(name->valuestring, "obtain_story") == 0) {
ESP_LOGI(TAG, "收到obtain_story工具调用");
cJSON* sn = cJSON_GetObjectItem(args_obj, "story_name");
const char* story = (sn && cJSON_IsString(sn) && sn->valuestring) ? sn->valuestring : "random";
cJSON* url_item = cJSON_GetObjectItem(args_obj, "story_url");
ESP_LOGI(TAG, "故事名称: %s", story);
AbortSpeaking(kAbortReasonNone);
std::string msg;
if (url_item && cJSON_IsString(url_item) && url_item->valuestring && strlen(url_item->valuestring) > 0) {
// HTTPS方式直接下载JSON音频文件播放
ESP_LOGI(TAG, "[HTTPS播放] 使用URL方式: %s", url_item->valuestring);
HttpsPlaybackFromUrl(url_item->valuestring);
msg = "正在通过HTTPS为你播放故事";
} else {
// WebSocket方式通过服务器推送
ESP_LOGI(TAG, "[WS播放] 使用WebSocket方式请求音频");
SendStoryRequest();
msg = "正在为你获取故事";
}
cJSON* call_id_item = cJSON_GetObjectItem(call, "id");
const char* call_id = (call_id_item && cJSON_IsString(call_id_item) && call_id_item->valuestring) ? call_id_item->valuestring : "";
if (protocol_ && call_id && call_id[0] != '\0') {
protocol_->SendFunctionResult(call_id, msg);
}
}
// // 添加天气查询功能处理 get_weather_aihub
// else if (strcmp(name->valuestring, "get_weather_aihub") == 0) {
// ESP_LOGI(TAG, "[WeatherAPI] ===== 收到get_weather_aihub工具调用 =====");
//
// // 打印完整参数信息用于调试
// ESP_LOGI(TAG, "[WeatherAPI] 参数对象检查:");
// if (args_obj && cJSON_IsObject(args_obj)) {
// cJSON* current = args_obj->child;
// while (current) {
// ESP_LOGI(TAG, "[WeatherAPI] %s: %s",
// current->string,
// cJSON_IsString(current) ? current->valuestring :
// (cJSON_IsNumber(current) ? "(number)" :
// (cJSON_IsBool(current) ? (current->valueint ? "true" : "false") :
// "(other)")));
// current = current->next;
// }
// } else {
// ESP_LOGI(TAG, "[WeatherAPI] args_obj为空或不是对象类型");
// }
//
// // 解析参数
// cJSON* location = cJSON_GetObjectItem(args_obj, "location");
// cJSON* lang = cJSON_GetObjectItem(args_obj, "lang");
//
// ESP_LOGI(TAG, "[WeatherAPI] location参数存在: %s, 类型: %s",
// location ? "是" : "否",
// location && cJSON_IsString(location) ? "字符串" :
// (location ? "非字符串" : "不适用"));
// ESP_LOGI(TAG, "[WeatherAPI] lang参数存在: %s, 类型: %s",
// lang ? "是" : "否",
// lang && cJSON_IsString(lang) ? "字符串" :
// (lang ? "非字符串" : "不适用"));
//
// // 设置默认值
// const char* location_str = (location && cJSON_IsString(location)) ? location->valuestring : "";
// const char* lang_str = (lang && cJSON_IsString(lang)) ? lang->valuestring : "zh_CN";
// std::string location_copy = std::string(location_str);
// std::string lang_copy = std::string(lang_str);
// if (location_copy.empty() || location_copy == "None") {
// wifi_config_t wc{};
// esp_wifi_get_config(WIFI_IF_STA, &wc);
// std::string ssid = std::string(reinterpret_cast<char*>(wc.sta.ssid));
// wifi_ap_record_t ap{};
// std::string bssid;
// if (esp_wifi_sta_get_ap_info(&ap) == ESP_OK) {
// char buf[18];
// snprintf(buf, sizeof(buf), "%02x:%02x:%02x:%02x:%02x:%02x",
// ap.bssid[0], ap.bssid[1], ap.bssid[2],
// ap.bssid[3], ap.bssid[4], ap.bssid[5]);
// bssid.assign(buf);
// }
// nvs_handle_t h;
// if (nvs_open("wifi_city_map", NVS_READONLY, &h) == ESP_OK) {
// auto try_get = [&](const std::string& key)->std::string{
// size_t len = 0;
// if (nvs_get_str(h, key.c_str(), NULL, &len) == ESP_OK && len > 0) {
// std::vector<char> buf(len);
// if (nvs_get_str(h, key.c_str(), buf.data(), &len) == ESP_OK) {
// return std::string(buf.data());
// }
// }
// return std::string();
// };
// if (!ssid.empty()) {
// std::string city;
// if (!bssid.empty()) {
// city = try_get(ssid + "|" + bssid);
// }
// if (city.empty()) {
// city = try_get(ssid);
// }
// if (!city.empty()) {
// location_copy = city;
// }
// }
// nvs_close(h);
// }
// }
//
// ESP_LOGI(TAG, "[WeatherAPI] 提取的参数值: location='%s' (长度: %zu), lang='%s'",
// location_str, strlen(location_str), lang_str);
//
// // 获取call_id用于后续响应
// cJSON* call_id_item = cJSON_GetObjectItem(call, "id");
// const char* call_id = (call_id_item && cJSON_IsString(call_id_item) && call_id_item->valuestring) ? call_id_item->valuestring : "";
// std::string call_id_copy = call_id ? call_id : "";
//
// ESP_LOGI(TAG, "[WeatherAPI] call_id_item存在: %s", call_id_item ? "是" : "否");
// ESP_LOGI(TAG, "[WeatherAPI] 获取的call_id: '%s'", call_id_copy.c_str());
//
// // 创建异步任务处理天气获取
// ESP_LOGI(TAG, "[WeatherAPI] 准备创建异步任务处理天气API调用");
// Schedule([this, location_copy, lang_copy, call_id_copy]() {
// ESP_LOGI(TAG, "[WeatherAPI] 异步任务开始执行");
// try {
// ESP_LOGI(TAG, "[WeatherAPI] 准备调用全局函数GetWeatherInfo(location='%s', lang='%s')",
// location_copy.c_str(), lang_copy.c_str());
//
// // 调用天气API获取结果
// std::string weather_result = GetWeatherInfo(location_copy, lang_copy);
//
// ESP_LOGI(TAG, "[WeatherAPI] GetWeatherInfo调用完成结果长度: %zu 字节", weather_result.length());
// ESP_LOGD(TAG, "[WeatherAPI] GetWeatherInfo返回结果前100字节: '%s'",
// weather_result.substr(0, std::min(size_t(100), weather_result.length())).c_str());
//
// ESP_LOGI(TAG, "[WeatherAPI] 准备发送天气结果响应");
// if (!call_id_copy.empty()) {
// ESP_LOGI(TAG, "[WeatherAPI] 使用call_id发送FunctionResult: '%s'", call_id_copy.c_str());
// } else {
// ESP_LOGI(TAG, "[WeatherAPI] 无call_id将发送TextMessage");
// }
//
// if (!call_id_copy.empty() && protocol_) {
// protocol_->SendFunctionResult(call_id_copy.c_str(), weather_result);
// ESP_LOGI(TAG, "[WeatherAPI] FunctionResult发送成功");
// } else if (protocol_) {
// protocol_->SendTextMessage(weather_result);// 发送天气结果
// ESP_LOGI(TAG, "[WeatherAPI] TextMessage发送成功");
// } else {
// ESP_LOGE(TAG, "[WeatherAPI] protocol_为空无法发送响应");
// }
// } catch (const std::exception& e) {
// ESP_LOGE(TAG, "[WeatherAPI] 天气获取异常: %s", e.what());
// std::string error_msg = "获取天气信息失败,请稍后重试";
// ESP_LOGI(TAG, "[WeatherAPI] 准备发送错误响应: '%s'", error_msg.c_str());
//
// if (!call_id_copy.empty() && protocol_) {
// protocol_->SendFunctionResult(call_id_copy.c_str(), error_msg);
// ESP_LOGI(TAG, "[WeatherAPI] 错误FunctionResult发送成功");
// } else if (protocol_) {
// protocol_->SendTextMessage(error_msg);
// ESP_LOGI(TAG, "[WeatherAPI] 错误TextMessage发送成功");
// } else {
// ESP_LOGE(TAG, "[WeatherAPI] protocol_为空无法发送错误响应");
// }
// }
// ESP_LOGI(TAG, "[WeatherAPI] ===== get_weather_aihub异步任务处理完成 =====");
// });
//
// ESP_LOGI(TAG, "[WeatherAPI] 异步任务已调度,将在后台执行");
// ESP_LOGI(TAG, "[WeatherAPI] ===== get_weather_aihub工具调用响应已发送 =====");
// }
cJSON_Delete(args_obj);// 释放参数对象
} else {
ESP_LOGI(TAG, "工具调用: name=%s arguments=%s", name->valuestring, args_str);
if (strcmp(name->valuestring, "adjust_audio_val") == 0) {
auto codec = Board::GetInstance().GetAudioCodec();
int user = HARDWARE_TO_USER_VOLUME(codec->output_volume());
if (args && cJSON_IsString(args) && args->valuestring) {
cJSON* tmp = cJSON_Parse(args->valuestring);
if (tmp) {
cJSON* v = cJSON_GetObjectItem(tmp, "value");
if (!v) v = cJSON_GetObjectItem(tmp, "action");
if (v) {
std::string msg;
if (cJSON_IsString(v) && v->valuestring) {
if (strcmp(v->valuestring, "up") == 0) {
user += 10;
msg = "音量已经调大了哦~";
} else if (strcmp(v->valuestring, "down") == 0) {
user -= 10;
msg = "音量已经调小了哦~";
} else {
// 处理字符串形式的数字
char* endptr;
long val = strtol(v->valuestring, &endptr, 10);
if (*endptr == '\0' && val >= 0 && val <= 100) {
user = (int)val;
msg = std::string("音量值已经调整为") + std::to_string(user) + "%";
}
}
} else if (cJSON_IsNumber(v)) {
user = (int)v->valuedouble;
msg = std::string("音量值已经调整为") + std::to_string(user) + "%";
}
if (user > 100) user = 100;
if (user < 0) user = 0;
int mapped = USER_TO_HARDWARE_VOLUME(user);
codec->SetOutputVolume(mapped);
ESP_LOGI(TAG, "设置音量: 用户=%d%% 硬件=%d%%", user, mapped);
if (!msg.empty()) {
cJSON* call_id_item = cJSON_GetObjectItem(call, "id");
const char* call_id = (call_id_item && cJSON_IsString(call_id_item) && call_id_item->valuestring) ? call_id_item->valuestring : "";
if (protocol_ && call_id && call_id[0] != '\0') {
protocol_->SendFunctionResult(call_id, msg);
} else if (protocol_) {
protocol_->SendTextMessage(msg);
}
}
}
cJSON_Delete(tmp);
}
}
}
// 讲故事功能支持HTTPS下载或WebSocket两种方式
else if (strcmp(name->valuestring, "obtain_story") == 0) {
ESP_LOGI(TAG, "收到obtain_story工具调用");
cJSON* url_item = cJSON_GetObjectItem(args_obj, "story_url");
AbortSpeaking(kAbortReasonNone);
std::string msg;
if (url_item && cJSON_IsString(url_item) && url_item->valuestring && strlen(url_item->valuestring) > 0) {
ESP_LOGI(TAG, "[HTTPS播放] 使用URL方式: %s", url_item->valuestring);
HttpsPlaybackFromUrl(url_item->valuestring);
msg = "正在通过HTTPS为你播放故事";
} else {
ESP_LOGI(TAG, "[WS播放] 使用WebSocket方式请求音频");
SendStoryRequest();
msg = "正在为你获取故事";
}
cJSON* call_id_item = cJSON_GetObjectItem(call, "id");
const char* call_id = (call_id_item && cJSON_IsString(call_id_item) && call_id_item->valuestring) ? call_id_item->valuestring : "";
if (protocol_ && call_id && call_id[0] != '\0') {
protocol_->SendFunctionResult(call_id, msg);
}
}
}
}
}
}
return;
}
return;
}
if (strcmp(type->valuestring, "tts") == 0) {
auto state = cJSON_GetObjectItem(root, "state");
if (strcmp(state->valuestring, "start") == 0) {
Schedule([this]() {
aborted_ = false;
if (device_state_ == kDeviceStateIdle || device_state_ == kDeviceStateListening) {
SetDeviceState(kDeviceStateSpeaking);
}
});
} else if (strcmp(state->valuestring, "stop") == 0) {
Schedule([this]() {
background_task_->WaitForCompletion();
if (device_state_ == kDeviceStateSpeaking) {
if (listening_mode_ == kListeningModeManualStop) {
SetDeviceState(kDeviceStateIdle);
} else {
SetDeviceState(kDeviceStateListening);
}
}
});
} else if (strcmp(state->valuestring, "sentence_start") == 0) {
auto text = cJSON_GetObjectItem(root, "text");
if (text != NULL) {
ESP_LOGI(TAG, "<< %s", text->valuestring);
Schedule([this, display, message = std::string(text->valuestring)]() {
display->SetChatMessage("assistant", message.c_str());
});
}
}
} else if (strcmp(type->valuestring, "stt") == 0) {
auto text = cJSON_GetObjectItem(root, "text");
if (text != NULL) {
ESP_LOGI(TAG, ">> %s", text->valuestring);
Schedule([this, display, message = std::string(text->valuestring)]() {
display->SetChatMessage("user", message.c_str());
});
}
} else if (strcmp(type->valuestring, "llm") == 0) {
auto emotion = cJSON_GetObjectItem(root, "emotion");
if (emotion != NULL) {
Schedule([this, display, emotion_str = std::string(emotion->valuestring)]() {
display->SetEmotion(emotion_str.c_str());
});
}
} else if (strcmp(type->valuestring, "subtitle") == 0) {
// 火山 RTC 字幕消息区分用户说的话和AI回答
auto data_arr = cJSON_GetObjectItem(root, "data");
if (data_arr && cJSON_IsArray(data_arr)) {
for (int i = 0; i < cJSON_GetArraySize(data_arr); ++i) {
auto item = cJSON_GetArrayItem(data_arr, i);
auto text = cJSON_GetObjectItem(item, "text");
auto user_id = cJSON_GetObjectItem(item, "userId");
auto definite = cJSON_GetObjectItem(item, "definite");
if (!text || !cJSON_IsString(text) || !text->valuestring[0]) continue;
bool is_final = definite && cJSON_IsTrue(definite);
// userId 以 "bot_" 开头为AI其余为用户
bool is_user = true;
if (user_id && cJSON_IsString(user_id)) {
if (strncmp(user_id->valuestring, "bot_", 4) == 0) {
is_user = false;
}
}
const char* role = is_user ? "USER" : "AI";
ESP_LOGI(TAG, "%s %s: %s", is_final ? "📝" : "..", role, text->valuestring);
// 用户说话时立即中止HTTPS音频播放
// subv字幕消息在协议层跳过了on_bot_message_由此处直接处理
if (is_user && https_playback_active_.load() && !https_playback_abort_.load()) {
AbortHttpsPlayback("检测到用户说话(字幕)");
}
}
}
} else if (strcmp(type->valuestring, "iot") == 0) {
auto commands = cJSON_GetObjectItem(root, "commands");
if (commands != NULL) {
auto& thing_manager = iot::ThingManager::GetInstance();
for (int i = 0; i < cJSON_GetArraySize(commands); ++i) {
auto command = cJSON_GetArrayItem(commands, i);
thing_manager.Invoke(command);
}
}
} else if (strcmp(type->valuestring, "response.function_call_arguments.done") == 0) {
auto name = cJSON_GetObjectItem(root, "name");
auto arguments = cJSON_GetObjectItem(root, "arguments");
cJSON* args_obj = nullptr;
const char* args_str = (arguments && cJSON_IsString(arguments) && arguments->valuestring) ? arguments->valuestring : "";
if (arguments && cJSON_IsString(arguments) && arguments->valuestring) {
args_obj = cJSON_Parse(arguments->valuestring);
}
if (name && cJSON_IsString(name) && name->valuestring) {
if (args_obj) {
char* printed = cJSON_PrintUnformatted(args_obj);
if (printed) {
ESP_LOGI(TAG, "工具调用: name=%s arguments=%s", name->valuestring, printed);
cJSON_free(printed);
} else {
ESP_LOGI(TAG, "工具调用: name=%s arguments=%s", name->valuestring, args_str);
}
if (strcmp(name->valuestring, "adjust_audio_val") == 0) {
auto codec = Board::GetInstance().GetAudioCodec();
int user = HARDWARE_TO_USER_VOLUME(codec->output_volume());
cJSON* v = cJSON_GetObjectItem(args_obj, "value");// 获取value字段
if (!v) v = cJSON_GetObjectItem(args_obj, "action");// 如果value字段不存在尝试action字段
if (v) {
std::string msg;
if (cJSON_IsString(v) && v->valuestring) {
if (strcmp(v->valuestring, "up") == 0) {
user += 10;
msg = "音量已经调大了哦~";
} else if (strcmp(v->valuestring, "down") == 0) {
user -= 10;
msg = "音量已经调小了哦~";
} else {
// 处理字符串形式的数字
char* endptr;
long val = strtol(v->valuestring, &endptr, 10);
if (*endptr == '\0' && val >= 0 && val <= 100) {
user = (int)val;
msg = std::string("音量值已经调整为") + std::to_string(user) + "%";
}
}
} else if (cJSON_IsNumber(v)) {
user = (int)v->valuedouble;
msg = std::string("音量值已经调整为") + std::to_string(user) + "%";
}
if (user > 100) user = 100;
if (user < 0) user = 0;
int mapped = USER_TO_HARDWARE_VOLUME(user);
codec->SetOutputVolume(mapped);
ESP_LOGI(TAG, "设置音量: 用户=%d%% 硬件=%d%%", user, mapped);
if (!msg.empty()) {
cJSON* call_id_item = cJSON_GetObjectItem(root, "call_id");
const char* call_id = (call_id_item && cJSON_IsString(call_id_item) && call_id_item->valuestring) ? call_id_item->valuestring : "";
if (protocol_ && call_id && call_id[0] != '\0') {
protocol_->SendFunctionResult(call_id, msg);
} else if (protocol_) {
protocol_->SendTextMessage(msg);
}
}
}
}
// 讲故事功能支持HTTPS下载或WebSocket两种方式
else if (strcmp(name->valuestring, "obtain_story") == 0) {
ESP_LOGI(TAG, "收到obtain_story工具调用");
cJSON* url_item = cJSON_GetObjectItem(args_obj, "story_url");
AbortSpeaking(kAbortReasonNone);
std::string msg;
if (url_item && cJSON_IsString(url_item) && url_item->valuestring && strlen(url_item->valuestring) > 0) {
ESP_LOGI(TAG, "[HTTPS播放] 使用URL方式: %s", url_item->valuestring);
HttpsPlaybackFromUrl(url_item->valuestring);
msg = "正在通过HTTPS为你播放故事";
} else {
ESP_LOGI(TAG, "[WS播放] 使用WebSocket方式请求音频");
SendStoryRequest();
msg = "正在为你获取故事";
}
cJSON* call_id_item = cJSON_GetObjectItem(root, "call_id");
const char* call_id = (call_id_item && cJSON_IsString(call_id_item) && call_id_item->valuestring) ? call_id_item->valuestring : "";
if (protocol_ && call_id && call_id[0] != '\0') {
protocol_->SendFunctionResult(call_id, msg);
}
}
cJSON_Delete(args_obj);
} else {
ESP_LOGI(TAG, "工具调用: name=%s arguments=%s", name->valuestring, args_str);
if (strcmp(name->valuestring, "adjust_audio_val") == 0) {
auto codec = Board::GetInstance().GetAudioCodec();
int user = HARDWARE_TO_USER_VOLUME(codec->output_volume());
if (arguments && cJSON_IsString(arguments) && arguments->valuestring) {
cJSON* tmp = cJSON_Parse(arguments->valuestring);
if (tmp) {
cJSON* v = cJSON_GetObjectItem(tmp, "value");
if (!v) v = cJSON_GetObjectItem(tmp, "action");
if (v) {
std::string msg;
if (cJSON_IsString(v) && v->valuestring) {
if (strcmp(v->valuestring, "up") == 0) {
user += 10;
msg = "音量已经调大了哦~";
} else if (strcmp(v->valuestring, "down") == 0) {
user -= 10;
msg = "音量已经调小了哦~";
} else {
// 处理字符串形式的数字
char* endptr;
long val = strtol(v->valuestring, &endptr, 10);
if (*endptr == '\0' && val >= 0 && val <= 100) {
user = (int)val;
msg = std::string("音量值已经调整为") + std::to_string(user) + "%";
}
}
} else if (cJSON_IsNumber(v)) {
user = (int)v->valuedouble;
msg = std::string("音量值已经调整为") + std::to_string(user) + "%";
}
if (user > 100) user = 100;
if (user < 0) user = 0;
int mapped = USER_TO_HARDWARE_VOLUME(user);
codec->SetOutputVolume(mapped);
ESP_LOGI(TAG, "设置音量: 用户=%d%% 硬件=%d%%", user, mapped);
if (!msg.empty()) {
cJSON* call_id_item = cJSON_GetObjectItem(root, "call_id");
const char* call_id = (call_id_item && cJSON_IsString(call_id_item) && call_id_item->valuestring) ? call_id_item->valuestring : "";
if (protocol_ && call_id && call_id[0] != '\0') {
protocol_->SendFunctionResult(call_id, msg);
} else if (protocol_) {
protocol_->SendTextMessage(msg);
}
}
}
cJSON_Delete(tmp);
}
}
}
// 讲故事功能支持HTTPS下载或WebSocket两种方式
else if (strcmp(name->valuestring, "obtain_story") == 0) {
ESP_LOGI(TAG, "收到obtain_story工具调用");
cJSON* args_parsed = nullptr;
if (arguments && cJSON_IsString(arguments) && arguments->valuestring) {
args_parsed = cJSON_Parse(arguments->valuestring);
}
cJSON* url_item = args_parsed ? cJSON_GetObjectItem(args_parsed, "story_url") : nullptr;
AbortSpeaking(kAbortReasonNone);
std::string msg;
if (url_item && cJSON_IsString(url_item) && url_item->valuestring && strlen(url_item->valuestring) > 0) {
ESP_LOGI(TAG, "[HTTPS播放] 使用URL方式: %s", url_item->valuestring);
HttpsPlaybackFromUrl(url_item->valuestring);
msg = "正在通过HTTPS为你播放故事";
} else {
ESP_LOGI(TAG, "[WS播放] 使用WebSocket方式请求音频");
SendStoryRequest();
msg = "正在为你获取故事";
}
if (args_parsed) cJSON_Delete(args_parsed);
cJSON* call_id_item = cJSON_GetObjectItem(root, "call_id");
const char* call_id = (call_id_item && cJSON_IsString(call_id_item) && call_id_item->valuestring) ? call_id_item->valuestring : "";
if (protocol_ && call_id && call_id[0] != '\0') {
protocol_->SendFunctionResult(call_id, msg);
}
}
}
}
// 新增代码(小程序控制 暂停/继续播放 音频)
// ====================================================================
}
else if (strcmp(type->valuestring, "music_control") == 0) {
auto action = cJSON_GetObjectItem(root, "action");
if (action && cJSON_IsString(action) && strcmp(action->valuestring, "pause") == 0) {
// 只有在speaking状态下才响应暂停指令
if (device_state_ == kDeviceStateSpeaking) {
ESP_LOGI(TAG, "🔇 从服务器接收到暂停播放指令 (speaking状态)");
Schedule([this]() {
PauseAudioPlayback();// 暂停播放
});
} else {
ESP_LOGI(TAG, "🔇 收到暂停指令但设备不在speaking状态忽略指令 (当前状态: %s)", STATE_STRINGS[device_state_]);
}
} else if (action && cJSON_IsString(action) && strcmp(action->valuestring, "resume") == 0) {
// 只有在speaking状态下才响应恢复播放指令
if (device_state_ == kDeviceStateSpeaking) {
ESP_LOGI(TAG, "🔊 从服务器接收到继续播放指令 (speaking状态)");
Schedule([this]() {
ResumeAudioPlayback();// 恢复播放
});
} else {
ESP_LOGI(TAG, "🔊 收到恢复播放指令但设备不在speaking状态忽略指令 (当前状态: %s)", STATE_STRINGS[device_state_]);
}
} else if (action && cJSON_IsString(action) && strcmp(action->valuestring, "play") == 0) {
// 处理新故事推送 - 确保在音频暂停状态和播放状态下都能正常播放
ESP_LOGI(TAG, "🎵 从服务器接收到新故事推送指令 (action: play)");
Schedule([this]() {
// 参考 AbortSpeakingAndReturnToListening 第1583-1651行的逻辑
// 检查并处理音频暂停状态,确保新故事能正常播放
if (audio_paused_) {
ESP_LOGI(TAG, "🔵 检测到音频暂停状态,为新故事推送清除暂停状态");
audio_paused_ = false;
ESP_LOGI(TAG, "✅ 音频暂停状态已清除");
// 清空音频播放队列,避免播放暂停时残留的音频
std::unique_lock<std::mutex> lock(mutex_);
audio_decode_queue_.clear();
lock.unlock();
ESP_LOGI(TAG, "🧹 已清空音频播放队列,避免播放残留音频");
// 重新启用音频编解码器输出
auto& board = Board::GetInstance();
auto codec = board.GetAudioCodec();
if (codec) {
codec->EnableOutput(true);
ESP_LOGI(TAG, "🔧 为新故事推送重新启用音频编解码器输出");
}
}
// 如果当前在播放状态,也需要清空队列确保新故事能正常播放
if (device_state_ == kDeviceStateSpeaking) {
ESP_LOGI(TAG, "🔵 当前在播放状态,为新故事推送清空音频队列");
std::unique_lock<std::mutex> lock(mutex_);
audio_decode_queue_.clear();
lock.unlock();
ESP_LOGI(TAG, "🧹 已清空音频播放队列,准备播放新故事");
// 确保音频编解码器输出已启用
auto& board = Board::GetInstance();
auto codec = board.GetAudioCodec();
if (codec) {
codec->EnableOutput(true);
ESP_LOGI(TAG, "🔧 确保音频编解码器输出已启用");
}
}
ESP_LOGI(TAG, "🎵 新故事推送处理完成,音频系统已准备就绪");
});
}
}
// ====================================================================
});
protocol_->Start();
// Check for new firmware version or get the MQTT broker address
ota_.SetCheckVersionUrl(CONFIG_OTA_VERSION_URL);
ota_.SetHeader("Device-Id", SystemInfo::GetMacAddress().c_str());
ota_.SetHeader("Client-Id", board.GetUuid());
ota_.SetHeader("Accept-Language", Lang::CODE);
auto app_desc = esp_app_get_description();
ota_.SetHeader("User-Agent", std::string(BOARD_NAME "/") + app_desc->version);
// 禁用自动OTA - 注释掉下面的任务创建OTA自动升级
xTaskCreate([](void* arg) {
Application* app = (Application*)arg;
app->CheckNewVersion();
vTaskDelete(NULL);
}, "check_new_version", 4096 * 2, this, 2, nullptr);
#if CONFIG_USE_AUDIO_PROCESSOR
audio_processor_.Initialize(codec, realtime_chat_enabled_);
// 🎯 根据语音打断功能启用状态配置VAD参数
EchoAwareVadParams enhanced_params;
if (realtime_chat_enabled_) {
// 语音打断功能启用:配置增强的回声感知参数 - 基于小智AI官方优化方案
// 🎯 平衡配置 - 防误触发同时保证音频流畅
enhanced_params.snr_threshold = 60.0f; // 平衡基础阈值:足够严格但不过度
enhanced_params.min_silence_ms = 2000; // 平衡静音要求2秒
enhanced_params.interrupt_cooldown_ms = 10000; // 平衡冷却时间10秒
enhanced_params.adaptive_threshold = true; // 启用自适应阈值
// 🔊 平衡噪声抑制参数 - 优化性能与效果
enhanced_params.adaptive_noise_suppression = true; // 启用自适应噪声抑制
enhanced_params.noise_suppression_base = 5.0f; // 平衡基础抑制强度
enhanced_params.volume_sensitivity = 3.0f; // 平衡音量敏感度:适度的音量影响
enhanced_params.echo_detection_threshold = 0.15f; // 平衡回声检测阈值
enhanced_params.distance_estimation_factor = 3.0f; // 平衡距离估算因子
ESP_LOGI(TAG, "🎯 Adaptive noise suppression enabled for realtime chat - smart volume/distance adjustment");
} else {
// 🔧 语音打断功能禁用关闭复杂VAD只使用简单VAD
enhanced_params.adaptive_threshold = false; // 禁用自适应阈值
enhanced_params.adaptive_noise_suppression = false; // 禁用自适应噪声抑制
ESP_LOGI(TAG, "🔧 Using simple VAD for basic voice detection - complex echo-aware VAD disabled");
}
audio_processor_.SetEchoAwareParams(enhanced_params);// 🔊 设置回声感知参数
// 🔊 注册音频处理输出回调 - 处理回声感知后的PCM数据
audio_processor_.OnOutput([this](std::vector<int16_t>&& data) {
background_task_->Schedule([this, data = std::move(data)]() mutable {
static uint64_t last_us = 0;
static size_t frames = 0;
std::vector<int16_t> resampled(uplink_resampler_.GetOutputSamples(data.size()));
if (!resampled.empty()) {
uplink_resampler_.Process(data.data(), data.size(), resampled.data());
}
std::vector<uint8_t> bytes(resampled.size() * sizeof(int16_t));
for (size_t i = 0; i < resampled.size(); ++i) {
int16_t s = resampled[i];
bytes[i * 2] = (uint8_t)(s & 0xFF);
bytes[i * 2 + 1] = (uint8_t)((s >> 8) & 0xFF);
}
frames += 1;
uint64_t now_us = esp_timer_get_time();
if (last_us == 0) last_us = now_us;
if (now_us - last_us >= 2000000) {
ESP_LOGI(TAG, "AFE输出统计: 帧=%zu 样本=%zu ", frames, data.size());
frames = 0;
last_us = now_us;
}
Schedule([this, bytes = std::move(bytes)]() {
if (protocol_ && protocol_->IsAudioChannelOpened() && (device_state_ == kDeviceStateListening || device_state_ == kDeviceStateDialog || (listening_mode_ == kListeningModeRealtime && device_state_ == kDeviceStateSpeaking))) {
protocol_->SendPcm(bytes);
} else {
ESP_LOGD(TAG, "通道未打开或不在dialog/listening状态时跳过发送上行");
}
});
});
});
// 🎯 根据语音打断功能启用状态选择VAD类型
if (realtime_chat_enabled_) {
// 语音打断功能启用使用复杂的回声感知VAD
audio_processor_.OnVadStateChange([this](bool speaking) {
ESP_LOGI(TAG, "Complex VAD state change: speaking=%s, device_state=%d, listening_mode=%d",
speaking ? "true" : "false", (int)device_state_, (int)listening_mode_);
if (device_state_ == kDeviceStateListening) {
Schedule([this, speaking]() {
if (speaking) {
voice_detected_ = true;
} else {
voice_detected_ = false;
}
auto led = Board::GetInstance().GetLed();
led->OnStateChanged();
});
}
});
}
// 🔧 简单VAD用于普通业务触摸忽略、LED状态等
audio_processor_.OnSimpleVadStateChange([this](bool speaking) {
ESP_LOGI(TAG, "Simple VAD state change: speaking=%s, device_state=%d",
speaking ? "true" : "false", (int)device_state_);
if (device_state_ == kDeviceStateListening) {
Schedule([this, speaking]() {
if (speaking) {
voice_detected_ = true;
} else {
voice_detected_ = false;
}
auto led = Board::GetInstance().GetLed();
led->OnStateChanged();
});
}
// 🔊 语音打断逻辑只在简单VAD中处理因为复杂VAD可能过于严格
if (device_state_ == kDeviceStateSpeaking && listening_mode_ == kListeningModeRealtime) {
Schedule([this, speaking]() {
static auto speech_start_time = std::chrono::steady_clock::now();
static bool speech_confirmation_pending = false;
auto now = std::chrono::steady_clock::now();
if (speaking) {
// 小智AI方案检测到人声开始启动确认流程
speech_start_time = now;
speech_confirmation_pending = true;
ESP_LOGD(TAG, "Human voice detected during playback, starting interrupt evaluation");
} else if (speech_confirmation_pending) {
// 小智AI方案人声结束评估是否触发打断
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(now - speech_start_time);
// 🎯 平衡自适应打断策略:防误触发同时保证响应性
// 基础持续时间3秒平衡根据干扰情况调整
int required_duration = 3000; // 基础要求3秒
// 🔊 根据当前音量动态调整持续时间要求(平衡策略)
if (current_speaker_volume_ > 0.4f) {
required_duration = 5000; // 高音量5秒
} else if (current_speaker_volume_ > 0.1f) {
required_duration = 4000; // 中音量4秒
}
// 低音量或静音保持3秒
if (duration.count() >= required_duration) {
static auto last_interrupt_time = std::chrono::steady_clock::now();
auto interrupt_duration = std::chrono::duration_cast<std::chrono::milliseconds>(now - last_interrupt_time);
// 🎯 平衡自适应多重保护机制:防误触发同时保证性能
bool volume_protection = (current_speaker_volume_ > 0.01f); // 平衡音量保护1%阈值
bool cooldown_protection = (interrupt_duration.count() <= 10000); // 平衡冷却10秒
static int false_positive_count = 0; // 误触发计数器
// 🎯 平衡连续误触发保护:适度学习机制
if (interrupt_duration.count() <= 20000) { // 20秒内的误触发相关
false_positive_count++;
} else if (interrupt_duration.count() > 60000) { // 60秒后开始衰减
false_positive_count = std::max(false_positive_count - 1, 0); // 适度衰减
}
bool consecutive_protection = (false_positive_count >= 2); // 2次误触发后保护
if (!volume_protection && !cooldown_protection && !consecutive_protection) {
// 小智AI核心逻辑StopPlayback -> SetDeviceState(Listening)
ESP_LOGI(TAG, "🎯 Adaptive voice interrupt triggered (duration: %.0fms/%dms, vol: %.3f) - stopping playback",
(float)duration.count(), required_duration, current_speaker_volume_);
AbortSpeaking(kAbortReasonVoiceInterrupt);
SetDeviceState(kDeviceStateListening);
last_interrupt_time = now;
} else {
ESP_LOGI(TAG, "🎯 Adaptive interrupt suppressed - vol_protection: %s (%.3f), cooldown: %.0fms, consecutive: %d",
volume_protection ? "true" : "false", current_speaker_volume_,
10000.0f - (float)interrupt_duration.count(), false_positive_count);
}
} else {
ESP_LOGI(TAG, "🎯 Voice too brief (%.0fms), likely echo or noise - adaptive threshold: %dms",
(float)duration.count(), required_duration);
}
speech_confirmation_pending = false;
}
});
}
});
#endif
#if CONFIG_USE_WAKE_WORD_DETECT || CONFIG_USE_CUSTOM_WAKE_WORD
if (!wake_word_detect_.Initialize(codec)) {
ESP_LOGE(TAG, "Failed to initialize wake word detection");
return;
}
wake_word_detect_.OnWakeWordDetected([this](const std::string& wake_word) {
Schedule([this, &wake_word]() {
if (device_state_ == kDeviceStateIdle) {
SetDeviceState(kDeviceStateConnecting);
wake_word_detect_.EncodeWakeWordData();
// 将OpenAudioChannel调用移到后台任务执行避免main任务栈溢出
background_task_->Schedule([this, wake_word]() {
// 打开音频通道并发送唤醒词数据到服务器
if (!protocol_->OpenAudioChannel()) {
Schedule([this]() {
wake_word_detect_.Start();
});
return;
}
// 编码并发送唤醒词音频数据
std::vector<uint8_t> opus;
// Encode and send the wake word data to the server
while (wake_word_detect_.GetWakeWordOpus(opus)) {
protocol_->SendAudio(opus);
}
// Set the chat state to wake word detected
protocol_->SendWakeWordDetected(wake_word);
ESP_LOGI(TAG, "Wake word detected: %s", wake_word.c_str());
// SetListeningMode需要在main task中执行因为它涉及UI更新等
Schedule([this]() {
SetListeningMode(realtime_chat_enabled_ ? kListeningModeRealtime : kListeningModeAutoStop);
});
});
} else if (device_state_ == kDeviceStateSpeaking) {
AbortSpeaking(kAbortReasonWakeWordDetected);
} else if (device_state_ == kDeviceStateActivating) {
SetDeviceState(kDeviceStateIdle);
}
});
});
wake_word_detect_.Start();
#endif
SetDeviceState(kDeviceStateIdle);
// BLE JSON 通讯服务已移至 WifiBoard 中,仅在配网模式下启动
// 每次设备开机后idle状态下测试 自动检测并设置当前位置打印
//此逻辑为冗余操作当前NVS中没有城市信息时会自动调用 位置查询API
// Schedule([]() {
// AutoDetectAndSetLocation();
// });
esp_timer_start_periodic(clock_timer_handle_, 1000000);
#if 0
while (true) {
SystemInfo::PrintRealTimeStats(pdMS_TO_TICKS(1000));
vTaskDelay(pdMS_TO_TICKS(10000));
}
#endif
}
// 时钟定时器回调函数
void Application::OnClockTimer() {
clock_ticks_++;
// 每10秒打印一次调试信息
if (clock_ticks_ % 10 == 0) {
int free_sram = heap_caps_get_free_size(MALLOC_CAP_INTERNAL);
int min_free_sram = heap_caps_get_minimum_free_size(MALLOC_CAP_INTERNAL);
// ESP_LOGI(TAG, "Free internal: %u minimal internal: %u", free_sram, min_free_sram);// 打印内部内存空闲大小和最小空闲大小
// // 打印蓝牙MAC地址
// ESP_LOGI(MAC_TAG, "Bluetooth MAC Address: %s", SystemInfo::GetBleMacAddress().c_str());// 生产测试打印
//ESP_LOGI(TAG, "此设备角色为: %s",CONFIG_DEVICE_ROLE);
// ESP_LOGI(TAG, "此设备角色为: KAKA 1028 升级成功!");
// 如果我们已经同步了服务器时间如果设备处于空闲状态请将状态设置为时钟“HH:MM”
if (ota_.HasServerTime()) {
if (device_state_ == kDeviceStateIdle) {
Schedule([this]() {
// Set status to clock "HH:MM"
time_t now = time(NULL);// 获取当前时间
char time_str[64];// 时间字符串缓冲区
strftime(time_str, sizeof(time_str), "%H:%M ", localtime(&now));// 格式化时间字符串
Board::GetInstance().GetDisplay()->SetStatus(time_str);// 设置显示状态
});
}
}
}
}
// 添加任务到主循环
void Application::Schedule(std::function<void()> callback) {
{
std::lock_guard<std::mutex> lock(mutex_);// 加锁保护任务队列
main_tasks_.push_back(std::move(callback));// 添加任务到队列
}
xEventGroupSetBits(event_group_, SCHEDULE_EVENT);// 通知主循环有任务需要执行
}
// 主循环控制聊天状态和Websocket连接
// 如果其他任务需要访问Websocket或聊天状态
// 它们应该使用Schedule调用此函数
void Application::MainLoop() {
while (true) {
auto bits = xEventGroupWaitBits(event_group_, SCHEDULE_EVENT, pdTRUE, pdFALSE, portMAX_DELAY);// 等待任务事件触发
// 检查是否有任务需要执行
if (bits & SCHEDULE_EVENT) {
std::unique_lock<std::mutex> lock(mutex_);// 加锁保护任务队列
std::list<std::function<void()>> tasks = std::move(main_tasks_);// 移动任务队列到本地
lock.unlock();// 解锁,允许其他任务添加到队列
for (auto& task : tasks) {
task();// 执行任务
}
}
}
}
// 音频环路用于输入和输出音频数据
void Application::AudioLoop() {
auto codec = Board::GetInstance().GetAudioCodec();
while (true) {
OnAudioInput();
if (codec->output_enabled()) {
OnAudioOutput();
}
}
}
// 启动对话看门狗
void Application::StartDialogWatchdog() {
if (dialog_watchdog_task_handle_ != nullptr) {
return;// 如果看门狗任务已存在,直接返回
}
dialog_watchdog_running_ = true;// 设置看门狗运行标志为true
dialog_watchdog_last_logged_ = -1;// 重置上次记录的日志时间为-1
xTaskCreatePinnedToCore([](void* arg) {
Application* app = (Application*)arg;// 获取应用实例指针
ESP_LOGI(TAG, "Dialog watchdog started, initial device state: %d", app->GetDeviceState());
while (app->dialog_watchdog_running_) {
vTaskDelay(pdMS_TO_TICKS(2000));// 减少延时到2秒更及时地检测和更新倒计时
// 检查设备状态
DeviceState current_state = app->GetDeviceState();
if (current_state != kDeviceStateDialog) {
ESP_LOGD(TAG, "Dialog watchdog skipping check, not in dialog state (current: %d)", current_state);
continue;
}
auto now = std::chrono::steady_clock::now();// 获取当前时间点
auto elapsed = std::chrono::duration_cast<std::chrono::seconds>(now - app->last_audible_output_time_).count();// 计算自上次有音频输出以来的秒数
// 确保elapsed为非负数
if (elapsed < 0) {
ESP_LOGW(TAG, "Dialog watchdog: invalid elapsed time: %lld", elapsed);
continue;
}
int remaining = DIALOG_IDLE_COUNTDOWN_SECONDS - (int)elapsed;// 计算对话空闲倒计时剩余秒数
// 调试日志
ESP_LOGD(TAG, "Dialog watchdog: elapsed=%d, remaining=%d", (int)elapsed, remaining);
// 如果剩余秒数小于等于0说明对话空闲倒计时已到需要重启设备
if (remaining <= 0) {
ESP_LOGI(TAG, "Dialog watchdog idle reached, elapsed=%d, marking and rebooting", (int)elapsed);
Settings sys("system", true);
ESP_LOGI(TAG, "Dialog watchdog: preparing NVS writes (system)");
sys.SetInt("reboot_dlg_idle", 1);
sys.SetInt("reboot_origin", 1);
ESP_LOGI(TAG, "Dialog watchdog: committing NVS (system)");
sys.Commit();
Settings sysr("system");
int32_t verify = sysr.GetInt("reboot_dlg_idle", 0);
int32_t origin_read = sysr.GetInt("reboot_origin", 0);
if (verify != 1) {
ESP_LOGW(TAG, "Dialog watchdog: NVS verify failed, cause=%d, origin=%d", (int)verify, (int)origin_read);
ESP_LOGW(TAG, "建议: 检查NVS空间是否不足、确认nvs_flash_init成功、避免并发写入(system)");
}
ESP_LOGI(TAG, "Dialog watchdog (task) set reboot_cause=1, verify=%d, restart in 2000ms", (int)verify);
// 重启前上报设备离线状态
Board::GetInstance().OnBeforeRestart();
vTaskDelay(pdMS_TO_TICKS(2000));
esp_restart();// 重启设备
app->dialog_watchdog_running_ = false;// 设置看门狗运行标志为false
} else {
// 简化条件判断,移除冗余检查
// 优化桶计算逻辑使用1秒一个桶更精确地显示倒计时
int bucket = remaining; // 使用剩余秒数作为桶标识,实现每秒更新
if (bucket != app->dialog_watchdog_last_logged_ && remaining <= DIALOG_IDLE_COUNTDOWN_SECONDS) {
ESP_LOGI(TAG, "dialog对话空闲倒计时剩余: %d 秒", remaining);// 打印剩余秒数
app->dialog_watchdog_last_logged_ = bucket;// 更新上次记录的日志时间为当前桶
}
}
}
app->dialog_watchdog_task_handle_ = nullptr;
ESP_LOGI(TAG, "Dialog watchdog stopped");
vTaskDelete(NULL);
}, "dialog_watchdog", 4096, this, 5, &dialog_watchdog_task_handle_, 0);
}
// 停止对话看门狗
void Application::StopDialogWatchdog() {
dialog_watchdog_running_ = false;
if (dialog_watchdog_task_handle_ != nullptr) {
vTaskDelete(dialog_watchdog_task_handle_);
dialog_watchdog_task_handle_ = nullptr;
ESP_LOGI(TAG, "Dialog watchdog stopped");
}
dialog_watchdog_last_logged_ = -1;
}
// 音频输出函数
void Application::OnAudioOutput() {
auto now = std::chrono::steady_clock::now();// 获取当前时间
auto codec = Board::GetInstance().GetAudioCodec();// 获取音频编解码器
const int max_silence_seconds = 10;// 最大静音秒数
std::unique_lock<std::mutex> lock(mutex_);// 加锁保护音频队列
// 调试日志:检查设备状态和音频队列
ESP_LOGD(TAG, "🔊 OnAudioOutput called, device_state: %d, audio_queue_size: %zu, codec_output_enabled: %d",
device_state_, audio_decode_queue_.size(), codec->output_enabled());
// 新增代码(小程序控制 暂停/继续播放 音频)
// =========================================================
// 🔧 暂停状态下停止从队列取数据,但保留队列状态
if (audio_paused_) {
// 暂停时重置音量状态,避免误判
if (current_speaker_volume_ > 0.0f) {
current_speaker_volume_ = 0.0f;// 暂停时重置音量状态
#if CONFIG_USE_AUDIO_PROCESSOR
audio_processor_.SetSpeakerVolume(0.0f);
#endif
}
return;
}
// =========================================================
if (audio_decode_queue_.empty()) {
// 重要:没有音频数据时立即重置音量状态
if (current_speaker_volume_ > 0.0f) {
current_speaker_volume_ = 0.0f;
#if CONFIG_USE_AUDIO_PROCESSOR
audio_processor_.SetSpeakerVolume(0.0f);
#endif
}
ESP_LOGD(TAG, "🔊 音频队列为空");
// Disable the output if there is no audio data for a long time
if (device_state_ == kDeviceStateIdle) {
auto duration = std::chrono::duration_cast<std::chrono::seconds>(now - last_output_time_).count();
if (duration > max_silence_seconds) {
codec->EnableOutput(false);
}
}
return;
}
if (device_state_ == kDeviceStateListening && listening_mode_ != kListeningModeRealtime) {
audio_decode_queue_.clear();
current_speaker_volume_ = 0.0f;// 清空音频队列后重置音量状态
#if CONFIG_USE_AUDIO_PROCESSOR
audio_processor_.SetSpeakerVolume(0.0f);
#endif
return;
}
auto opus = std::move(audio_decode_queue_.front());
audio_decode_queue_.pop_front();
// 在出队时捕获opus解码标志避免background_task异步执行时标志已变化
// 导致残留的Opus帧被当作PCM播放产生杂音
bool is_opus_frame = opus_playback_active_.load();
lock.unlock();
background_task_->Schedule([this, codec, opus = std::move(opus), is_opus_frame]() mutable {
if (aborted_) {
return;
}
// 跳过已中止的HTTPS opus残留帧出队时is_opus_frame=true但中止后opus_playback_active_=false
// 不能用https_playback_abort_判断因为故事任务退出时会将其清为false导致残留帧漏过
if (is_opus_frame && !opus_playback_active_.load()) {
return;
}
std::vector<int16_t> pcm;
bool decoded = false;
bool treat_as_pcm = (protocol_ && protocol_->downlink_is_pcm() && !is_opus_frame);
if (!treat_as_pcm) {
decoded = opus_decoder_->Decode(std::move(opus), pcm);
}
if (!decoded) {
if (treat_as_pcm && !opus.empty() && (opus.size() % 2 == 0)) {
pcm.resize(opus.size() / 2);
memcpy(pcm.data(), opus.data(), opus.size());
int srv = protocol_ ? protocol_->server_sample_rate() : 16000;
if (!player_pipeline_) {
if (srv != codec->output_sample_rate()) {
output_resampler_.Configure(srv, codec->output_sample_rate());
int target_size = output_resampler_.GetOutputSamples(pcm.size());
std::vector<int16_t> resampled(target_size);
output_resampler_.Process(pcm.data(), pcm.size(), resampled.data());
pcm = std::move(resampled);
}
}
} else {
return;
}
}
// Resample if the sample rate is different
if (!treat_as_pcm && decoded && !player_pipeline_ && opus_decoder_->sample_rate() != codec->output_sample_rate()) {
int target_size = output_resampler_.GetOutputSamples(pcm.size());
std::vector<int16_t> resampled(target_size);
output_resampler_.Process(pcm.data(), pcm.size(), resampled.data());
pcm = std::move(resampled);
}
// 计算并同步音频输出音量到音频处理器用于动态VAD阈值调整
if (!pcm.empty()) {
// 计算音频块的RMS音量 (0.0 - 1.0)
float sum_squares = 0.0f;
for (const auto& sample : pcm) {
float normalized = (float)sample / 32768.0f;
sum_squares += normalized * normalized;
}
float rms_volume = std::sqrt(sum_squares / pcm.size());
// 当有可听见的音频输出时,更新最后声音输出时间戳
const float audible_volume_threshold = 0.01f; // 设置一个合理的音量阈值
if (rms_volume >= audible_volume_threshold) {
this->last_audible_output_time_ = std::chrono::steady_clock::now();
ESP_LOGD(TAG, "🔊 更新last_audible_output_time_当前音量: %.4f", rms_volume);
}
#if CONFIG_USE_AUDIO_PROCESSOR
// 同步音量到音频处理器,用于动态阈值调整
current_speaker_volume_ = rms_volume; // 保存当前音量供打断逻辑使用
audio_processor_.SetSpeakerVolume(rms_volume);
#endif
}
int src_rate = decoded ? opus_decoder_->sample_rate() : (protocol_ ? protocol_->server_sample_rate() : 16000);
static bool first_play_logged = false;
if (player_pipeline_) {
player_pipeline_set_src_rate(player_pipeline_, src_rate);
int bytes = (int)(pcm.size() * sizeof(int16_t));
ESP_LOGD(TAG, "写入播放管道: 采样率=%d 字节=%d", src_rate, bytes);
player_pipeline_write(player_pipeline_, (char*)pcm.data(), bytes);
if (bytes > 0) {
this->last_audible_output_time_ = std::chrono::steady_clock::now();
}
if (!first_play_logged && bytes > 0) {
ESP_LOGI(TAG, "开始播放下行音频: 字节=%d 采样率=%d", bytes, src_rate);
first_play_logged = true;
}
} else {
ESP_LOGD(TAG, "直接输出PCM到编解码器: 样本=%zu", pcm.size());
codec->OutputData(pcm);// 直接输出PCM数据
if (!pcm.empty()) {
this->last_audible_output_time_ = std::chrono::steady_clock::now();
}
if (!first_play_logged && !pcm.empty()) {
ESP_LOGI(TAG, "开始播放下行音频: 样本=%zu 采样率=%d", pcm.size(), src_rate);
first_play_logged = true;
}
// 解决本地资源声音播放尖锐问题方案1
// // 如果是单声道,转换为立体声
// if (codec->output_channels() == 2) {// 单声道转换为立体声
// std::vector<int16_t> stereo(pcm.size() * 2);// 立体声PCM数据
// for (size_t i = 0, j = 0; i < pcm.size(); ++i) {
// stereo[j++] = pcm[i];// 左声道
// stereo[j++] = pcm[i];// 右声道
// }
// codec->OutputData(stereo);// 输出立体声PCM数据
// } else {
// codec->OutputData(pcm);// 输出单声道PCM数据
// }
// 解决本地资源声音播放尖锐问题方案2
// player_pipeline_ = player_pipeline_open();// 打开音频播放管道
// player_pipeline_run(player_pipeline_);// 启动音频播放管道
// player_pipeline_set_src_rate(player_pipeline_, src_rate);// 设置播放管道源采样率
// player_pipeline_write(player_pipeline_, (char*)pcm.data(), (int)(pcm.size() * sizeof(int16_t)));// 写入PCM数据到播放管道
}
last_output_time_ = std::chrono::steady_clock::now();// 更新最后输出时间
});
}
void Application::OnAudioInput() {
std::vector<int16_t> data;
#if CONFIG_USE_WAKE_WORD_DETECT || CONFIG_USE_CUSTOM_WAKE_WORD
if (wake_word_detect_.IsRunning()) {
ReadAudio(data, 16000, wake_word_detect_.GetFeedSize());
wake_word_detect_.Feed(data);// 将音频数据喂给唤醒词检测器
return;
}
#endif
#if CONFIG_USE_AUDIO_PROCESSOR
if (audio_processor_.IsRunning()) {
ReadAudio(data, 16000, audio_processor_.GetFeedSize());
if (!data.empty()) {
int n = (int)data.size();
int64_t sum = 0;
int peak = 0;
for (int i = 0; i < n; ++i) {
int v = data[i];
int a = v < 0 ? -v : v;
sum += a;
if (a > peak) peak = a;
}
(void)sum;
// if (avg > 150 || peak > 800) {
// ESP_LOGI(TAG, "🎙️ 输入幅度: 均值=%d 峰值=%d 样本=%d", avg, peak, n);
// }
}
audio_processor_.Feed(data);
return;
}
#else
if (device_state_ == kDeviceStateListening || device_state_ == kDeviceStateDialog) {
if (send_g711a_uplink_) {
ReadAudio(data, 16000, 20 * 16000 / 1000);
if (!data.empty()) {
std::vector<int16_t> resampled(uplink_resampler_.GetOutputSamples((int)data.size()));
if (!resampled.empty()) {
uplink_resampler_.Process(data.data(), (int)data.size(), resampled.data());
}
int out_samples = (int)resampled.size();
std::vector<uint8_t> bytes(out_samples);
for (int i = 0; i < out_samples; ++i) {
int16_t s = resampled[i];
int sign = (s >> 8) & 0x80;
if (sign) s = -s;
if (s > 32635) s = 32635;
int exp = 7;
for (int mask = 0x4000; exp > 0 && (s & mask) == 0; mask >>= 1) exp--;
int mant = (s >> ((exp == 0) ? 4 : (exp + 3))) & 0x0F;
uint8_t a = (uint8_t)(sign | (exp << 4) | mant);
bytes[i] = (uint8_t)(a ^ 0xD5);
}
Schedule([this, bytes = std::move(bytes)]() {
if (protocol_ && protocol_->IsAudioChannelOpened()) {
protocol_->SendG711A(bytes);// 发送G711A音频数据
}
});
}
} else if (send_pcm_uplink_) {
ReadAudio(data, 16000, 20 * 16000 / 1000);
if (!data.empty()) {
int out_samples = (int)data.size() / 2;
std::vector<int16_t> down(out_samples);
for (int i = 0, j = 0; i < out_samples; ++i, j += 2) {
down[i] = data[j];
}
std::vector<int16_t> resampled(uplink_resampler_.GetOutputSamples((int)down.size()));
if (!resampled.empty()) {
uplink_resampler_.Process(down.data(), (int)down.size(), resampled.data());
}
std::vector<uint8_t> bytes(resampled.size() * sizeof(int16_t));
for (size_t i = 0; i < resampled.size(); ++i) {
int16_t s = resampled[i];
bytes[i * 2] = (uint8_t)(s & 0xFF);
bytes[i * 2 + 1] = (uint8_t)((s >> 8) & 0xFF);
}
Schedule([this, bytes = std::move(bytes)]() {
if (protocol_ && protocol_->IsAudioChannelOpened() && (device_state_ == kDeviceStateListening || device_state_ == kDeviceStateDialog || (listening_mode_ == kListeningModeRealtime && device_state_ == kDeviceStateSpeaking))) {
protocol_->SendPcm(bytes);
}
});
}
} else {
ReadAudio(data, 16000, 30 * 16000 / 1000);
background_task_->Schedule([this, data = std::move(data)]() mutable {
opus_encoder_->Encode(std::move(data), [this](std::vector<uint8_t>&& opus) {
Schedule([this, opus = std::move(opus)]() {
if (protocol_) {
protocol_->SendAudio(opus);
}
});
});
});
}
return;
}
#endif
vTaskDelay(pdMS_TO_TICKS(30));
}
void Application::ReadAudio(std::vector<int16_t>& data, int sample_rate, int samples) {
auto codec = Board::GetInstance().GetAudioCodec();
#if CONFIG_USE_AUDIO_PROCESSOR
// 当音频处理器运行且存在参考通道时保持原有双通道读取以支持AEC
if (audio_processor_.IsRunning() && codec->input_channels() == 2) {
if (codec->input_sample_rate() != sample_rate) {
data.resize(samples * codec->input_sample_rate() / sample_rate);
if (!codec->InputData(data)) {
return;
}
auto mic_channel = std::vector<int16_t>(data.size() / 2);
auto reference_channel = std::vector<int16_t>(data.size() / 2);
for (size_t i = 0, j = 0; i < mic_channel.size(); ++i, j += 2) {
mic_channel[i] = data[j];
reference_channel[i] = data[j + 1];
}
auto resampled_mic = std::vector<int16_t>(input_resampler_.GetOutputSamples(mic_channel.size()));
auto resampled_reference = std::vector<int16_t>(reference_resampler_.GetOutputSamples(reference_channel.size()));
input_resampler_.Process(mic_channel.data(), mic_channel.size(), resampled_mic.data());
reference_resampler_.Process(reference_channel.data(), reference_channel.size(), resampled_reference.data());
data.resize(resampled_mic.size() + resampled_reference.size());
for (size_t i = 0, j = 0; i < resampled_mic.size(); ++i, j += 2) {
data[j] = resampled_mic[i];
data[j + 1] = resampled_reference[i];
}
return;
} else {
data.resize(samples * codec->input_channels());
if (!codec->InputData(data)) {
return;
}
static bool first_equal_sr_dual_read_logged = false;
if (!first_equal_sr_dual_read_logged) {
ESP_LOGI(TAG, "AFE输入首包: 双通道等采样率 目标样本=%d 通道=%d 实际向量=%zu", samples, codec->input_channels(), data.size());
first_equal_sr_dual_read_logged = true;
}
return;
}
}
#endif
// 默认优先使用recorder管道读取目标采样率16000无参考通道需求
if (recorder_pipeline_ && sample_rate == 16000) {
int need_bytes = samples * (int)sizeof(int16_t);
int default_bytes = recorder_pipeline_get_default_read_size(recorder_pipeline_);
std::vector<int16_t> out;
out.reserve(samples);// 预分配内存空间,避免后续动态扩容
std::vector<char> buf(default_bytes);// 内存音频缓冲区,用于存储从录音管道读取的音频数据
while ((int)out.size() < samples) {
int to_read = std::min(default_bytes, (need_bytes - (int)out.size() * (int)sizeof(int16_t)));// 计算本次读取的字节数,不超过默认读取大小和剩余需要读取的字节数
if (to_read <= 0) break;// 读取到的数据大小小于等于0,跳出循环
int got = recorder_pipeline_read(recorder_pipeline_, buf.data(), to_read);// 从录音管道读取音频数据,并赋值给内存音频缓冲区
if (got <= 0) {
ESP_LOGW(TAG, "🎙️ 录音管道读取失败,未收到输入数据");
break;
}
int got_samples = got / (int)sizeof(int16_t);// 计算本次读取的样本数,即读取到的字节数除以每个样本的字节数
int16_t* p = (int16_t*)buf.data();// 将内存音频缓冲区转换为int16_t指针,方便按样本读取
for (int i = 0; i < got_samples && (int)out.size() < samples; ++i) {
out.push_back(p[i]);// 将读取到的样本添加到输出向量中,直到达到预期样本数或读取完所有数据
}
}
if (!out.empty()) {
data.assign(out.begin(), out.end());// 将输出向量中的数据赋值给输出参数data
return;
}
}
// 回退到直接从codec读取的实现
if (codec->input_sample_rate() != sample_rate) {
data.resize(samples * codec->input_sample_rate() / sample_rate);
if (!codec->InputData(data)) {
ESP_LOGW(TAG, "🎙️ 麦克风采样失败(重采样路径),未收到输入数据");
return;
}
if (codec->input_channels() == 2) {
// 双通道约定:当前缓冲按 [主麦,参考] 排列;必须与 ALGORITHM_INPUT_FORMAT 的 M/R 顺序
// 以及 CHANNEL_FORMAT 的物理通道对应一致,否则可能导致 AEC 失效或增益反向
auto mic_channel = std::vector<int16_t>(data.size() / 2);
auto reference_channel = std::vector<int16_t>(data.size() / 2);
for (size_t i = 0, j = 0; i < mic_channel.size(); ++i, j += 2) {
mic_channel[i] = data[j];
reference_channel[i] = data[j + 1];
}
auto resampled_mic = std::vector<int16_t>(input_resampler_.GetOutputSamples(mic_channel.size()));
auto resampled_reference = std::vector<int16_t>(reference_resampler_.GetOutputSamples(reference_channel.size()));
input_resampler_.Process(mic_channel.data(), mic_channel.size(), resampled_mic.data());
reference_resampler_.Process(reference_channel.data(), reference_channel.size(), resampled_reference.data());
data.resize(resampled_mic.size() + resampled_reference.size());
for (size_t i = 0, j = 0; i < resampled_mic.size(); ++i, j += 2) {
data[j] = resampled_mic[i];
data[j + 1] = resampled_reference[i];
}
} else {
auto resampled = std::vector<int16_t>(input_resampler_.GetOutputSamples(data.size()));
input_resampler_.Process(data.data(), data.size(), resampled.data());
data = std::move(resampled);
}
} else {
data.resize(samples);
if (!codec->InputData(data)) {
ESP_LOGW(TAG, "🎙️ 麦克风采样失败(直读路径),未收到输入数据");
return;
}
}
}
// 打断语音播报(终止播放)
void Application::AbortSpeaking(AbortReason reason) {
// 🔧 防止重复中止操作,避免竞态条件
bool expected = false;
if (!is_aborting_.compare_exchange_strong(expected, true)) {
ESP_LOGD(TAG, "AbortSpeaking already in progress, ignoring duplicate call");
return;
}
ESP_LOGI(TAG, "🔴 Abort speaking - immediate stop");
aborted_ = true;
// 中止HTTPS音频播放如果正在进行
if (https_playback_active_.load()) {
https_playback_abort_.store(true);
ESP_LOGI(TAG, "🔴 HTTPS音频播放中止信号已发送");
}
// 🔧 更新安全操作时间戳
last_safe_operation_.store(std::chrono::steady_clock::now());
// 🔧 修复:立即清空音频队列,确保音频播放立即停止
{
std::lock_guard<std::mutex> lock(mutex_);
if (!audio_decode_queue_.empty()) {
ESP_LOGI(TAG, "🔴 Clearing %zu audio frames from queue", audio_decode_queue_.size());
audio_decode_queue_.clear();
// 重置音量状态
current_speaker_volume_ = 0.0f;
#if CONFIG_USE_AUDIO_PROCESSOR
audio_processor_.SetSpeakerVolume(0.0f);
#endif
}
}
// ⚠️ 移除WaitForCompletion避免死锁让后台任务通过aborted_标志自然结束
ESP_LOGI(TAG, "🔴 Audio queue cleared, background tasks will stop on next iteration");
// 重启codec输出以清空I2S DMA缓冲区中残留音频确保扬声器立即静音
// 移除output_enabled()守卫确保始终执行flush
if (background_task_) {
background_task_->Schedule([this]() {
auto codec = Board::GetInstance().GetAudioCodec();
if (codec) {
ESP_LOGI(TAG, "DMA flush: output_enabled=%d", codec->output_enabled());
codec->EnableOutput(false);
vTaskDelay(pdMS_TO_TICKS(10));
codec->EnableOutput(true);
ESP_LOGI(TAG, "🔇 音频输出已重置DMA缓冲区已清空");
}
});
}
// 🔧 修复始终尝试发送中止消息以打断RTC下行不受IsSafeToOperate限制
if (protocol_) {
try {
// 🔧 额外的连接状态验证
if (protocol_->IsAudioChannelOpened()) {
protocol_->SendAbortSpeaking(reason);
ESP_LOGI(TAG, "AbortSpeaking message sent successfully");
// 更新安全操作时间戳
last_safe_operation_.store(std::chrono::steady_clock::now());
} else {
ESP_LOGW(TAG, "Audio channel not properly opened, skipping AbortSpeaking");
}
} catch (const std::exception& e) {
ESP_LOGW(TAG, "Failed to send AbortSpeaking message: %s", e.what());
}
} else {
ESP_LOGD(TAG, "Skipping AbortSpeaking message - protocol_ is null");
}
// 🔧 确保中止窗口后恢复播放流程避免长时间阻塞导致WS音频无法播放
Schedule([this]() {
vTaskDelay(pdMS_TO_TICKS(120));
aborted_ = false;
ESP_LOGI(TAG, "🔵 Abort window ended, resume playback tasks");
});
// 🔧 重置中止标志,允许后续操作
is_aborting_.store(false);
}
// 中止HTTPS音频播放清空队列、重置解码器、清除标志、DMA flush
void Application::AbortHttpsPlayback(const char* reason) {
ESP_LOGI(TAG, "🔴 %s中止HTTPS音频播放", reason);
https_playback_abort_.store(true);
{
std::lock_guard<std::mutex> lock(mutex_);
if (!audio_decode_queue_.empty()) {
ESP_LOGI(TAG, "清空HTTPS音频队列大小=%zu", audio_decode_queue_.size());
audio_decode_queue_.clear();
}
}
ResetDecoder();
opus_playback_active_.store(false);
https_playback_active_.store(false);
ESP_LOGI(TAG, "🔴 HTTPS播放标志已清除RTC音频通道已打开");
// DMA flush用独立任务立即清空I2S DMA缓冲区
// 不能用background_task_RTC音频lambda会持续占用它导致延迟数秒
xTaskCreate([](void* arg) {
auto codec = Board::GetInstance().GetAudioCodec();
if (codec) {
ESP_LOGI(TAG, "DMA flush: output_enabled=%d", codec->output_enabled());
codec->EnableOutput(false);
vTaskDelay(pdMS_TO_TICKS(10));
codec->EnableOutput(true);
ESP_LOGI(TAG, "🔇 音频输出已重置DMA缓冲区已清空");
}
vTaskDelete(NULL);
}, "dma_flush", 4096, NULL, 10, NULL);
}
// 通过故事API请求并播放故事intro标题 + body正文无缝衔接
void Application::SendStoryRequest() {
// 防止重复启动opus_playback_active_ 在任务启动时设置覆盖HTTP请求阶段
if (https_playback_active_.load() || https_playback_abort_.load() || opus_playback_active_.load()) {
ESP_LOGW(TAG, "[故事API] 已有音频正在播放或退出中,忽略本次请求");
return;
}
xTaskCreate([](void* arg) {
auto& app = Application::GetInstance();
// 先设置opus和abort标志用于重复启动守卫和OnIncomingAudio阻断RTC PCM
// 注意https_playback_active_ 延迟到intro音频入队前设置
// 这样HTTP请求期间~500ms残留的Bot subv消息不会触发OnBotMessage中止
app.opus_playback_active_.store(true);
app.https_playback_abort_.store(false);
// base64 解码查找表
static uint8_t b64_table[256] = {0};
static bool b64_inited = false;
if (!b64_inited) {
const char* chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
for (int c = 0; chars[c]; c++) {
b64_table[(uint8_t)chars[c]] = (uint8_t)c;
}
b64_inited = true;
}
// ========== 步骤1: 请求故事API ==========
std::string mac = SystemInfo::GetBleMacAddress();
// 转大写
for (auto& c : mac) {
if (c >= 'a' && c <= 'f') c -= 32;
}
char api_url[256];
snprintf(api_url, sizeof(api_url), "%s?mac_address=%s",
CONFIG_STORY_API_URL, mac.c_str());
ESP_LOGI(TAG, "[故事API] 请求: %s", api_url);
ESP_LOGI(TAG, "[故事API] 空闲堆: %lu", (unsigned long)esp_get_free_heap_size());
esp_http_client_config_t api_config = {};
api_config.url = api_url;
api_config.method = HTTP_METHOD_GET;
api_config.timeout_ms = 10000;
api_config.buffer_size = 2048;
api_config.buffer_size_tx = 512;
esp_http_client_handle_t api_client = esp_http_client_init(&api_config);
if (!api_client) {
ESP_LOGE(TAG, "[故事API] HTTP客户端初始化失败");
app.https_playback_active_.store(false);
app.opus_playback_active_.store(false);
vTaskDelete(NULL);
return;
}
esp_err_t err = esp_http_client_open(api_client, 0);
if (err != ESP_OK) {
ESP_LOGE(TAG, "[故事API] 连接失败: %s", esp_err_to_name(err));
esp_http_client_cleanup(api_client);
app.https_playback_active_.store(false);
app.opus_playback_active_.store(false);
vTaskDelete(NULL);
return;
}
esp_http_client_fetch_headers(api_client);
int api_status = esp_http_client_get_status_code(api_client);
if (api_status != 200) {
ESP_LOGE(TAG, "[故事API] 请求失败,状态码: %d", api_status);
esp_http_client_close(api_client);
esp_http_client_cleanup(api_client);
app.https_playback_active_.store(false);
app.opus_playback_active_.store(false);
vTaskDelete(NULL);
return;
}
// 读取API响应通常 < 10KB
std::string api_response;
char buf[1024];
int rlen;
while ((rlen = esp_http_client_read(api_client, buf, sizeof(buf))) > 0) {
api_response.append(buf, rlen);
}
esp_http_client_close(api_client);
esp_http_client_cleanup(api_client);
ESP_LOGI(TAG, "[故事API] 响应: %d 字节", (int)api_response.size());
if (app.https_playback_abort_.load()) {
app.https_playback_active_.store(false);
app.https_playback_abort_.store(false);
app.opus_playback_active_.store(false);
ESP_LOGI(TAG, "[故事API] HTTP请求阶段被中止");
vTaskDelete(NULL);
return;
}
// 解析外层JSON
cJSON* root = cJSON_Parse(api_response.c_str());
api_response.clear();
api_response.shrink_to_fit();
if (!root) {
ESP_LOGE(TAG, "[故事API] JSON解析失败");
app.https_playback_active_.store(false);
app.opus_playback_active_.store(false);
vTaskDelete(NULL);
return;
}
cJSON* code_item = cJSON_GetObjectItem(root, "code");
if (!code_item || code_item->valueint != 0) {
cJSON* msg_item = cJSON_GetObjectItem(root, "message");
ESP_LOGE(TAG, "[故事API] 服务端错误: %s",
(msg_item && msg_item->valuestring) ? msg_item->valuestring : "unknown");
cJSON_Delete(root);
app.https_playback_active_.store(false);
app.opus_playback_active_.store(false);
vTaskDelete(NULL);
return;
}
cJSON* data = cJSON_GetObjectItem(root, "data");
cJSON* title_item = data ? cJSON_GetObjectItem(data, "title") : nullptr;
cJSON* intro_str = data ? cJSON_GetObjectItem(data, "intro_opus_data") : nullptr;
cJSON* opus_url_item = data ? cJSON_GetObjectItem(data, "opus_url") : nullptr;
if (!intro_str || !cJSON_IsString(intro_str) || !intro_str->valuestring ||
!opus_url_item || !cJSON_IsString(opus_url_item) || !opus_url_item->valuestring) {
ESP_LOGE(TAG, "[故事API] 缺少intro_opus_data或opus_url字段");
cJSON_Delete(root);
app.https_playback_active_.store(false);
app.opus_playback_active_.store(false);
vTaskDelete(NULL);
return;
}
ESP_LOGI(TAG, "[故事API] 标题: %s",
(title_item && title_item->valuestring) ? title_item->valuestring : "未知");
// 提取字符串后释放外层JSON
std::string intro_json_str = intro_str->valuestring;
std::string opus_url = opus_url_item->valuestring;
cJSON_Delete(root);
// ========== 步骤2: 解析 intro_opus_data ==========
cJSON* intro_root = cJSON_Parse(intro_json_str.c_str());
intro_json_str.clear();
intro_json_str.shrink_to_fit();
if (!intro_root) {
ESP_LOGE(TAG, "[故事API] intro JSON解析失败");
app.https_playback_active_.store(false);
app.opus_playback_active_.store(false);
vTaskDelete(NULL);
return;
}
cJSON* intro_sr = cJSON_GetObjectItem(intro_root, "sample_rate");
cJSON* intro_fd = cJSON_GetObjectItem(intro_root, "frame_duration_ms");
cJSON* intro_frames = cJSON_GetObjectItem(intro_root, "frames");
if (!intro_frames || !cJSON_IsArray(intro_frames)) {
ESP_LOGE(TAG, "[故事API] intro缺少frames数组");
cJSON_Delete(intro_root);
app.https_playback_active_.store(false);
app.opus_playback_active_.store(false);
vTaskDelete(NULL);
return;
}
int sample_rate = (intro_sr && cJSON_IsNumber(intro_sr)) ? intro_sr->valueint : 16000;
int frame_duration = (intro_fd && cJSON_IsNumber(intro_fd)) ? intro_fd->valueint : 60;
int intro_count = cJSON_GetArraySize(intro_frames);
ESP_LOGI(TAG, "[故事API] intro: 采样率=%d, 帧时长=%dms, 帧数=%d (%.1f秒)",
sample_rate, frame_duration, intro_count,
intro_count * frame_duration / 1000.0f);
app.SetDecodeSampleRate(sample_rate, frame_duration);
// 音频即将入队现在激活播放标志允许OnBotMessage中止
app.https_playback_active_.store(true);
// ========== 步骤3: 入队 intro frames ==========
int enqueued = 0;
int errors = 0;
for (int i = 0; i < intro_count; i++) {
if (app.https_playback_abort_.load()) break;
cJSON* fi = cJSON_GetArrayItem(intro_frames, i);
if (!fi || !cJSON_IsString(fi) || !fi->valuestring) { errors++; continue; }
const char* b64 = fi->valuestring;
size_t b64_len = strlen(b64);
if (b64_len == 0) { errors++; continue; }
size_t out_len = (b64_len * 3) / 4;
if (b64_len >= 1 && b64[b64_len - 1] == '=') out_len--;
if (b64_len >= 2 && b64[b64_len - 2] == '=') out_len--;
std::vector<uint8_t> frame(out_len);
size_t j = 0, k = 0;
while (j < b64_len) {
uint32_t a = b64_table[(uint8_t)b64[j++]];
uint32_t b = (j < b64_len) ? b64_table[(uint8_t)b64[j++]] : 0;
uint32_t c = (j < b64_len) ? b64_table[(uint8_t)b64[j++]] : 0;
uint32_t d = (j < b64_len) ? b64_table[(uint8_t)b64[j++]] : 0;
uint32_t triple = (a << 18) | (b << 12) | (c << 6) | d;
if (k < out_len) frame[k++] = (triple >> 16) & 0xFF;
if (k < out_len) frame[k++] = (triple >> 8) & 0xFF;
if (k < out_len) frame[k++] = triple & 0xFF;
}
{
std::lock_guard<std::mutex> lock(app.mutex_);
app.audio_decode_queue_.emplace_back(std::move(frame));
}
enqueued++;
// 队列节流
while (!app.https_playback_abort_.load()) {
size_t qs;
{ std::lock_guard<std::mutex> lock(app.mutex_); qs = app.audio_decode_queue_.size(); }
if (qs < 50) break;
vTaskDelay(pdMS_TO_TICKS(30));
}
}
cJSON_Delete(intro_root);
ESP_LOGI(TAG, "[故事API] intro入队完成: %d帧, 错误: %d", enqueued, errors);
if (app.https_playback_abort_.load()) {
ESP_LOGI(TAG, "[故事API] intro阶段被中止");
app.https_playback_active_.store(false);
app.https_playback_abort_.store(false);
app.opus_playback_active_.store(false);
vTaskDelete(NULL);
return;
}
// ========== 步骤4: 下载 opus_url 正文 ==========
ESP_LOGI(TAG, "[故事API] 开始下载正文: %s", opus_url.c_str());
esp_http_client_config_t opus_config = {};
opus_config.url = opus_url.c_str();
opus_config.method = HTTP_METHOD_GET;
opus_config.transport_type = HTTP_TRANSPORT_OVER_SSL;
opus_config.timeout_ms = 15000;
opus_config.buffer_size = 2048;
opus_config.buffer_size_tx = 512;
#ifdef CONFIG_MBEDTLS_CERTIFICATE_BUNDLE
opus_config.crt_bundle_attach = esp_crt_bundle_attach;
#endif
esp_http_client_handle_t opus_client = esp_http_client_init(&opus_config);
if (!opus_client) {
ESP_LOGE(TAG, "[故事API] opus HTTP初始化失败");
app.https_playback_active_.store(false);
app.https_playback_abort_.store(false);
app.opus_playback_active_.store(false);
vTaskDelete(NULL);
return;
}
err = esp_http_client_open(opus_client, 0);
if (err != ESP_OK) {
ESP_LOGE(TAG, "[故事API] opus连接失败: %s", esp_err_to_name(err));
esp_http_client_cleanup(opus_client);
app.https_playback_active_.store(false);
app.https_playback_abort_.store(false);
app.opus_playback_active_.store(false);
vTaskDelete(NULL);
return;
}
int64_t opus_content_len = esp_http_client_fetch_headers(opus_client);
int opus_status = esp_http_client_get_status_code(opus_client);
ESP_LOGI(TAG, "[故事API] opus状态码: %d, 长度: %lld", opus_status, (long long)opus_content_len);
if (opus_status != 200) {
ESP_LOGE(TAG, "[故事API] opus请求失败状态码: %d", opus_status);
esp_http_client_close(opus_client);
esp_http_client_cleanup(opus_client);
app.https_playback_active_.store(false);
app.https_playback_abort_.store(false);
app.opus_playback_active_.store(false);
vTaskDelete(NULL);
return;
}
std::string opus_json;
if (opus_content_len > 0) opus_json.reserve(opus_content_len);
int total_read = 0;
while ((rlen = esp_http_client_read(opus_client, buf, sizeof(buf))) > 0) {
if (app.https_playback_abort_.load()) break;
opus_json.append(buf, rlen);
total_read += rlen;
}
esp_http_client_close(opus_client);
esp_http_client_cleanup(opus_client);
ESP_LOGI(TAG, "[故事API] opus下载完成: %d 字节, 堆: %lu",
total_read, (unsigned long)esp_get_free_heap_size());
if (app.https_playback_abort_.load()) {
ESP_LOGI(TAG, "[故事API] opus下载被中止");
app.https_playback_active_.store(false);
app.https_playback_abort_.store(false);
app.opus_playback_active_.store(false);
vTaskDelete(NULL);
return;
}
// ========== 步骤5: 解析并入队 body frames ==========
cJSON* opus_root = cJSON_Parse(opus_json.c_str());
opus_json.clear();
opus_json.shrink_to_fit();
ESP_LOGI(TAG, "[故事API] opus JSON已释放, 堆: %lu", (unsigned long)esp_get_free_heap_size());
if (!opus_root) {
ESP_LOGE(TAG, "[故事API] opus JSON解析失败");
app.https_playback_active_.store(false);
app.https_playback_abort_.store(false);
app.opus_playback_active_.store(false);
vTaskDelete(NULL);
return;
}
cJSON* body_frames = cJSON_GetObjectItem(opus_root, "frames");
if (!body_frames || !cJSON_IsArray(body_frames)) {
ESP_LOGE(TAG, "[故事API] opus缺少frames数组");
cJSON_Delete(opus_root);
app.https_playback_active_.store(false);
app.https_playback_abort_.store(false);
app.opus_playback_active_.store(false);
vTaskDelete(NULL);
return;
}
// 检查body采样率是否与intro不同
cJSON* body_sr = cJSON_GetObjectItem(opus_root, "sample_rate");
cJSON* body_fd = cJSON_GetObjectItem(opus_root, "frame_duration_ms");
int body_sample_rate = (body_sr && cJSON_IsNumber(body_sr)) ? body_sr->valueint : sample_rate;
int body_frame_duration = (body_fd && cJSON_IsNumber(body_fd)) ? body_fd->valueint : frame_duration;
int body_count = cJSON_GetArraySize(body_frames);
ESP_LOGI(TAG, "[故事API] body: 采样率=%d, 帧时长=%dms, 帧数=%d (%.1f秒)",
body_sample_rate, body_frame_duration, body_count,
body_count * body_frame_duration / 1000.0f);
if (body_sample_rate != sample_rate || body_frame_duration != frame_duration) {
app.SetDecodeSampleRate(body_sample_rate, body_frame_duration);
}
int body_enqueued = 0;
int body_errors = 0;
for (int i = 0; i < body_count; i++) {
if (app.https_playback_abort_.load()) {
ESP_LOGI(TAG, "[故事API] body入队中止: %d/%d", body_enqueued, body_count);
break;
}
cJSON* fi = cJSON_GetArrayItem(body_frames, i);
if (!fi || !cJSON_IsString(fi) || !fi->valuestring) { body_errors++; continue; }
const char* b64 = fi->valuestring;
size_t b64_len = strlen(b64);
if (b64_len == 0) { body_errors++; continue; }
size_t out_len = (b64_len * 3) / 4;
if (b64_len >= 1 && b64[b64_len - 1] == '=') out_len--;
if (b64_len >= 2 && b64[b64_len - 2] == '=') out_len--;
std::vector<uint8_t> frame(out_len);
size_t j = 0, k = 0;
while (j < b64_len) {
uint32_t a = b64_table[(uint8_t)b64[j++]];
uint32_t b = (j < b64_len) ? b64_table[(uint8_t)b64[j++]] : 0;
uint32_t c = (j < b64_len) ? b64_table[(uint8_t)b64[j++]] : 0;
uint32_t d = (j < b64_len) ? b64_table[(uint8_t)b64[j++]] : 0;
uint32_t triple = (a << 18) | (b << 12) | (c << 6) | d;
if (k < out_len) frame[k++] = (triple >> 16) & 0xFF;
if (k < out_len) frame[k++] = (triple >> 8) & 0xFF;
if (k < out_len) frame[k++] = triple & 0xFF;
}
{
std::lock_guard<std::mutex> lock(app.mutex_);
app.audio_decode_queue_.emplace_back(std::move(frame));
}
body_enqueued++;
// 队列节流
while (!app.https_playback_abort_.load()) {
size_t qs;
{ std::lock_guard<std::mutex> lock(app.mutex_); qs = app.audio_decode_queue_.size(); }
if (qs < 50) break;
vTaskDelay(pdMS_TO_TICKS(30));
}
// 每100帧打印进度
if (body_enqueued % 100 == 0) {
size_t qs;
{ std::lock_guard<std::mutex> lock(app.mutex_); qs = app.audio_decode_queue_.size(); }
ESP_LOGI(TAG, "[故事API] body进度: %d/%d (%.0f%%), 队列: %zu, 堆: %lu",
body_enqueued, body_count,
body_enqueued * 100.0f / body_count, qs,
(unsigned long)esp_get_free_heap_size());
}
}
cJSON_Delete(opus_root);
ESP_LOGI(TAG, "[故事API] body入队完成: %d帧, 错误: %d", body_enqueued, body_errors);
// ========== 步骤6: 等待播放完毕 ==========
if (!app.https_playback_abort_.load()) {
ESP_LOGI(TAG, "[故事API] 全部入队完成,等待播放完毕...");
while (!app.https_playback_abort_.load()) {
size_t qs;
{ std::lock_guard<std::mutex> lock(app.mutex_); qs = app.audio_decode_queue_.size(); }
if (qs == 0) break;
vTaskDelay(pdMS_TO_TICKS(100));
}
}
bool was_aborted = app.https_playback_abort_.load();
app.https_playback_active_.store(false);
app.https_playback_abort_.store(false);
app.opus_playback_active_.store(false);
ESP_LOGI(TAG, "[故事API] 播放结束, aborted=%d, 堆: %lu",
was_aborted, (unsigned long)esp_get_free_heap_size());
vTaskDelete(NULL);
}, "story_play", 10240, NULL, 5, NULL);
}
// 通过HTTPS下载JSON并流式播放音频故事/歌曲等)
void Application::HttpsPlaybackFromUrl(const std::string& url) {
// 防止重复启动opus_playback_active_ 在任务启动时设置覆盖HTTP请求阶段
if (https_playback_active_.load() || https_playback_abort_.load() || opus_playback_active_.load()) {
ESP_LOGW(TAG, "[HTTPS播放] 已有音频正在播放或退出中,忽略本次请求");
return;
}
// 在独立任务中执行,避免阻塞调用线程
std::string url_copy = url;
xTaskCreate([](void* arg) {
std::string* url_ptr = static_cast<std::string*>(arg);
std::string playback_url = std::move(*url_ptr);
delete url_ptr;
auto& app = Application::GetInstance();
// 先设置opus和abort标志用于重复启动守卫和OnIncomingAudio阻断RTC PCM
// https_playback_active_ 延迟到音频入队前设置防止残留subv触发OnBotMessage
app.opus_playback_active_.store(true);
app.https_playback_abort_.store(false);
ESP_LOGI(TAG, "[HTTPS播放] 开始下载: %s", playback_url.c_str());
ESP_LOGI(TAG, "[HTTPS播放] 空闲堆内存: %lu 字节", (unsigned long)esp_get_free_heap_size());
// 配置HTTP客户端
esp_http_client_config_t config = {};
config.url = playback_url.c_str();
config.method = HTTP_METHOD_GET;
config.transport_type = HTTP_TRANSPORT_OVER_SSL;
config.timeout_ms = 15000;
config.buffer_size = 2048; // 接收缓冲区(节省内存)
config.buffer_size_tx = 512;
#ifdef CONFIG_MBEDTLS_CERTIFICATE_BUNDLE
config.crt_bundle_attach = esp_crt_bundle_attach;
#endif
esp_http_client_handle_t client = esp_http_client_init(&config);
if (!client) {
ESP_LOGE(TAG, "[HTTPS播放] HTTP客户端初始化失败");
app.https_playback_active_.store(false);
app.opus_playback_active_.store(false);
vTaskDelete(NULL);
return;
}
esp_err_t err = esp_http_client_open(client, 0);
if (err != ESP_OK) {
ESP_LOGE(TAG, "[HTTPS播放] HTTP连接失败: %s", esp_err_to_name(err));
esp_http_client_cleanup(client);
app.https_playback_active_.store(false);
app.opus_playback_active_.store(false);
vTaskDelete(NULL);
return;
}
int64_t content_length = esp_http_client_fetch_headers(client);
int status_code = esp_http_client_get_status_code(client);
ESP_LOGI(TAG, "[HTTPS播放] HTTP状态码: %d, 内容长度: %lld", status_code, (long long)content_length);
if (status_code != 200) {
ESP_LOGE(TAG, "[HTTPS播放] HTTP请求失败状态码: %d", status_code);
esp_http_client_close(client);
esp_http_client_cleanup(client);
app.https_playback_active_.store(false);
app.opus_playback_active_.store(false);
vTaskDelete(NULL);
return;
}
// 流式读取整个JSON必须完整读取才能解析frames数组
// 但使用分块读取减少单次分配峰值
std::string json_data;
if (content_length > 0) {
json_data.reserve(content_length);
}
char read_buf[2048];
int read_len;
int total_read = 0;
while ((read_len = esp_http_client_read(client, read_buf, sizeof(read_buf))) > 0) {
if (app.https_playback_abort_.load()) {
ESP_LOGI(TAG, "[HTTPS播放] 下载被中止");
break;
}
json_data.append(read_buf, read_len);
total_read += read_len;
}
// 关闭HTTP连接释放TLS资源
esp_http_client_close(client);
esp_http_client_cleanup(client);
ESP_LOGI(TAG, "[HTTPS播放] 下载完成: %d 字节, 堆剩余: %lu",
total_read, (unsigned long)esp_get_free_heap_size());
if (app.https_playback_abort_.load()) {
ESP_LOGI(TAG, "[HTTPS播放] 播放已取消,释放资源");
app.https_playback_active_.store(false);
app.https_playback_abort_.store(false);
app.opus_playback_active_.store(false);
vTaskDelete(NULL);
return;
}
// 解析JSON
cJSON* root = cJSON_Parse(json_data.c_str());
// 解析完成后立即释放原始JSON字符串
json_data.clear();
json_data.shrink_to_fit();
ESP_LOGI(TAG, "[HTTPS播放] JSON字符串已释放, 堆剩余: %lu",
(unsigned long)esp_get_free_heap_size());
if (!root) {
ESP_LOGE(TAG, "[HTTPS播放] JSON解析失败");
app.https_playback_active_.store(false);
app.opus_playback_active_.store(false);
vTaskDelete(NULL);
return;
}
// 读取音频参数
cJSON* sample_rate_item = cJSON_GetObjectItem(root, "sample_rate");
cJSON* channels_item = cJSON_GetObjectItem(root, "channels");
cJSON* frame_duration_item = cJSON_GetObjectItem(root, "frame_duration_ms");
cJSON* frames_array = cJSON_GetObjectItem(root, "frames");
if (!frames_array || !cJSON_IsArray(frames_array)) {
ESP_LOGE(TAG, "[HTTPS播放] JSON中缺少frames数组");
cJSON_Delete(root);
app.https_playback_active_.store(false);
app.opus_playback_active_.store(false);
vTaskDelete(NULL);
return;
}
int sample_rate = (sample_rate_item && cJSON_IsNumber(sample_rate_item)) ? sample_rate_item->valueint : 16000;
int channels = (channels_item && cJSON_IsNumber(channels_item)) ? channels_item->valueint : 1;
int frame_duration = (frame_duration_item && cJSON_IsNumber(frame_duration_item)) ? frame_duration_item->valueint : 60;
int frame_count = cJSON_GetArraySize(frames_array);
ESP_LOGI(TAG, "[HTTPS播放] 音频参数: 采样率=%d, 通道=%d, 帧时长=%dms, 总帧数=%d",
sample_rate, channels, frame_duration, frame_count);
ESP_LOGI(TAG, "[HTTPS播放] 预计时长: %.1f 秒", frame_count * frame_duration / 1000.0f);
// 设置解码器采样率复用现有Opus解码器
app.SetDecodeSampleRate(sample_rate, frame_duration);
// 音频即将入队现在激活播放标志允许OnBotMessage中止
app.https_playback_active_.store(true);
// 逐帧base64解码并入队播放
int enqueued = 0;
int decode_errors = 0;
// base64 解码查找表C++ 兼容初始化)
static uint8_t b64_table[256] = {0};
static bool b64_inited = false;
if (!b64_inited) {
const char* chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
for (int c = 0; chars[c]; c++) {
b64_table[(uint8_t)chars[c]] = (uint8_t)c;
}
b64_inited = true;
}
for (int i = 0; i < frame_count; i++) {
if (app.https_playback_abort_.load()) {
ESP_LOGI(TAG, "[HTTPS播放] 播放中止,已入队 %d/%d 帧", enqueued, frame_count);
break;
}
cJSON* frame_item = cJSON_GetArrayItem(frames_array, i);
if (!frame_item || !cJSON_IsString(frame_item) || !frame_item->valuestring) {
decode_errors++;
continue;
}
const char* b64 = frame_item->valuestring;
size_t b64_len = strlen(b64);
if (b64_len == 0) {
decode_errors++;
continue;
}
// base64 解码
size_t out_len = (b64_len * 3) / 4;
if (b64_len >= 1 && b64[b64_len - 1] == '=') out_len--;
if (b64_len >= 2 && b64[b64_len - 2] == '=') out_len--;
std::vector<uint8_t> opus_frame(out_len);
size_t j = 0, k = 0;
while (j < b64_len) {
uint32_t sextet_a = (j < b64_len) ? b64_table[(uint8_t)b64[j++]] : 0;
uint32_t sextet_b = (j < b64_len) ? b64_table[(uint8_t)b64[j++]] : 0;
uint32_t sextet_c = (j < b64_len) ? b64_table[(uint8_t)b64[j++]] : 0;
uint32_t sextet_d = (j < b64_len) ? b64_table[(uint8_t)b64[j++]] : 0;
uint32_t triple = (sextet_a << 18) | (sextet_b << 12) | (sextet_c << 6) | sextet_d;
if (k < out_len) opus_frame[k++] = (triple >> 16) & 0xFF;
if (k < out_len) opus_frame[k++] = (triple >> 8) & 0xFF;
if (k < out_len) opus_frame[k++] = triple & 0xFF;
}
// 入队到音频解码队列和WebSocket入队方式完全一致
{
std::lock_guard<std::mutex> lock(app.mutex_);
app.audio_decode_queue_.emplace_back(std::move(opus_frame));
}
enqueued++;
// 控制入队速度:队列过大时等待消费,避免内存堆积
// 每帧60ms队列超过50帧3秒缓冲时等待
while (!app.https_playback_abort_.load()) {
size_t queue_size;
{
std::lock_guard<std::mutex> lock(app.mutex_);
queue_size = app.audio_decode_queue_.size();
}
if (queue_size < 50) break;
vTaskDelay(pdMS_TO_TICKS(30)); // 等待消费
}
// 每100帧打印一次进度
if (enqueued % 100 == 0) {
size_t queue_size;
{
std::lock_guard<std::mutex> lock(app.mutex_);
queue_size = app.audio_decode_queue_.size();
}
ESP_LOGI(TAG, "[HTTPS播放] 进度: %d/%d 帧 (%.0f%%), 队列: %zu, 堆: %lu",
enqueued, frame_count, enqueued * 100.0f / frame_count,
queue_size, (unsigned long)esp_get_free_heap_size());
}
}
// 释放cJSON解析树
cJSON_Delete(root);
ESP_LOGI(TAG, "[HTTPS播放] JSON解析树已释放, 堆剩余: %lu",
(unsigned long)esp_get_free_heap_size());
if (app.https_playback_abort_.load()) {
ESP_LOGI(TAG, "[HTTPS播放] 播放被用户中止,入队 %d 帧,解码错误 %d",
enqueued, decode_errors);
} else {
ESP_LOGI(TAG, "[HTTPS播放] 全部入队完成: %d 帧,解码错误 %d等待播放完毕...",
enqueued, decode_errors);
}
// 等待队列播放完毕(或被中止)
while (!app.https_playback_abort_.load()) {
size_t queue_size;
{
std::lock_guard<std::mutex> lock(app.mutex_);
queue_size = app.audio_decode_queue_.size();
}
if (queue_size == 0) break;
vTaskDelay(pdMS_TO_TICKS(100));
}
app.https_playback_active_.store(false);
app.https_playback_abort_.store(false);
app.opus_playback_active_.store(false);
ESP_LOGI(TAG, "[HTTPS播放] 播放结束, 最终堆剩余: %lu",
(unsigned long)esp_get_free_heap_size());
vTaskDelete(NULL);
}, "https_play", 8192, new std::string(url_copy), 5, NULL);
}
// 设置监听模式
void Application::SetListeningMode(ListeningMode mode) {
ESP_LOGI(TAG, "Setting listening mode to %d", (int)mode);// 打印设置监听模式日志
listening_mode_ = mode;
SetDeviceState(kDeviceStateListening);
}
// 设置设备状态
void Application::SetDeviceState(DeviceState state) {
if (device_state_ == state) {
return;
}
clock_ticks_ = 0;
auto previous_state = device_state_;// 记录上一个设备状态
device_state_ = state;// 设置设备状态
if (state == kDeviceStateDialog) {
StartDialogWatchdog();
} else if (previous_state == kDeviceStateDialog) {
StopDialogWatchdog();
}
ESP_LOGI(TAG, "打印设置设备状态日志: %s", STATE_STRINGS[device_state_]);// 打印设置设备状态日志
// The state is changed, wait for all background tasks to finish
background_task_->WaitForCompletion();
auto& board = Board::GetInstance();
auto display = board.GetDisplay();
auto led = board.GetLed();
led->OnStateChanged();
// 检查是否正在进行BLE配网配网时禁止播放待命音效(新增代码)
// =================================================================
bool is_ble_provisioning = false;
if (Board::GetInstance().GetBoardType() == "wifi") {
auto& wifi_board = static_cast<WifiBoard&>(Board::GetInstance());
is_ble_provisioning = wifi_board.IsBleProvisioningActive();
}
// =================================================================
switch (state) {
case kDeviceStateUnknown:
case kDeviceStateIdle:
display->SetStatus(Lang::Strings::STANDBY);
display->SetEmotion("neutral");
// // 只有从非待命状态进入待命状态时才播放待命音效,避免重复播放(原来的代码)
// if (previous_state != kDeviceStateIdle &&
// previous_state != kDeviceStateUnknown &&
// previous_state != kDeviceStateWifiConfiguring) {
// ESP_LOGI(TAG, "Entering idle state, playing standby sound");
// PlaySound(Lang::Sounds::P3_DAIMING);
// }
// 开机后 进入待命状态 播报 卡卡正在待命(配网模式下不播报“卡卡正在待命”)-新增代码
//=====================================================================================
if (previous_state != kDeviceStateIdle && previous_state != kDeviceStateUnknown &&
previous_state != kDeviceStateWifiConfiguring && !is_ble_provisioning && !IsLowBatteryTransition()) {
ESP_LOGI(TAG, "Entering idle state, playing standby sound");
// PlaySound(Lang::Sounds::P3_DAIMING); 原有 待命 播报
if(strcmp(CONFIG_DEVICE_ROLE, "KAKA") == 0){
PlaySound(Lang::Sounds::P3_KAKA_DAIMING);
}
else if(strcmp(CONFIG_DEVICE_ROLE, "RTC_Test") == 0){
PlaySound(Lang::Sounds::P3_LALA_DAIMING);
}
}
//=====================================================================================
#if CONFIG_USE_AUDIO_PROCESSOR
audio_processor_.Stop();
#endif
#if 1
if (recorder_pipeline_) {
recorder_pipeline_close(recorder_pipeline_);
recorder_pipeline_ = nullptr;
}
#endif
#if CONFIG_USE_WAKE_WORD_DETECT || CONFIG_USE_CUSTOM_WAKE_WORD
wake_word_detect_.Start();
#endif
// 设备开机后首次进入idle状态时自动检测NVS中的位置并调用API后设置当前位置
if (!first_idle_location_checked_) {
first_idle_location_checked_ = true;// 首次查询城市天气
Schedule([]() {
AutoDetectAndSetLocation();// 自动检测并设置当前位置
});
}
break;
case kDeviceStateConnecting:
display->SetStatus(Lang::Strings::CONNECTING);
display->SetEmotion("neutral");
display->SetChatMessage("system", "");
break;
case kDeviceStateListening:
display->SetStatus(Lang::Strings::LISTENING);
display->SetEmotion("neutral");
// 关键修复:只有在非音效播放状态下才重置音量,避免中断正在播放的音效
// 检查是否有音频正在播放,如果有则延迟重置音量
if (IsAudioQueueEmpty()) {
current_speaker_volume_ = 0.0f;
#if CONFIG_USE_AUDIO_PROCESSOR
audio_processor_.SetSpeakerVolume(0.0f);
#endif
} else {
// 如果有音频正在播放,延迟重置音量
Schedule([this]() {
vTaskDelay(pdMS_TO_TICKS(500)); // 等待音效播放完成
current_speaker_volume_ = 0.0f;
#if CONFIG_USE_AUDIO_PROCESSOR
audio_processor_.SetSpeakerVolume(0.0f);
#endif
});
}
// Update the IoT states before sending the start listening command
UpdateIotStates();
// Make sure the audio processor is running
#if CONFIG_USE_AUDIO_PROCESSOR
if (!audio_processor_.IsRunning()) {
#else
if (true) {
#endif
// 🔧 关键修复:检查协议连接状态,防止发送到无效连接
if (protocol_ && protocol_->IsAudioChannelOpened()) {
// Send the start listening command
protocol_->SendStartListening(listening_mode_);
if (listening_mode_ == kListeningModeAutoStop && previous_state == kDeviceStateSpeaking) {
// FIXME: Wait for the speaker to empty the buffer
vTaskDelay(pdMS_TO_TICKS(120));
}
opus_encoder_->ResetState();
#if CONFIG_USE_WAKE_WORD_DETECT || CONFIG_USE_CUSTOM_WAKE_WORD
wake_word_detect_.Stop();
#endif
#if CONFIG_USE_AUDIO_PROCESSOR
audio_processor_.Start();
#endif
if (!recorder_pipeline_) {
recorder_pipeline_ = recorder_pipeline_open();
recorder_pipeline_run(recorder_pipeline_);
}
} else {
ESP_LOGW(TAG, "Audio channel not available, skipping SendStartListening");// 音频通道未打开,跳过发送开始聆听命令
// 保持在聆听状态不自动回退到idle状态
ESP_LOGI(TAG, "🔵 Staying in listening state despite audio channel unavailable");
}
}
break;
case kDeviceStateSpeaking:
display->SetStatus(Lang::Strings::SPEAKING);
if (listening_mode_ != kListeningModeRealtime) {
#if CONFIG_USE_AUDIO_PROCESSOR
audio_processor_.Stop();
#endif
#if CONFIG_USE_WAKE_WORD_DETECT || CONFIG_USE_CUSTOM_WAKE_WORD
wake_word_detect_.Start();
#endif
} else {
// 在实时模式下保持audio_processor运行以检测语音打断
#if CONFIG_USE_AUDIO_PROCESSOR
if (!audio_processor_.IsRunning()) {
audio_processor_.Start();
}
#endif
#if CONFIG_USE_WAKE_WORD_DETECT || CONFIG_USE_CUSTOM_WAKE_WORD
wake_word_detect_.Stop();
#endif
}
ResetDecoder();
break;
case kDeviceStateDialog:
display->SetStatus(Lang::Strings::SPEAKING);
#if CONFIG_USE_AUDIO_PROCESSOR
if (!audio_processor_.IsRunning()) {
audio_processor_.Start();
}
#endif
#if CONFIG_USE_WAKE_WORD_DETECT || CONFIG_USE_CUSTOM_WAKE_WORD
wake_word_detect_.Stop();
#endif
{
auto codec2 = Board::GetInstance().GetAudioCodec();// 获取音频编解码器
codec2->EnableOutput(true);// 启用音频输出
last_audible_output_time_ = std::chrono::steady_clock::now();// 更新最后有声音输出的时间
}
if (!recorder_pipeline_) {
recorder_pipeline_ = recorder_pipeline_open();
recorder_pipeline_run(recorder_pipeline_);
}
break;
default:
// Do nothing
break;
}
}
void Application::ResetDecoder() {
std::lock_guard<std::mutex> lock(mutex_);
opus_decoder_->ResetState();
audio_decode_queue_.clear();
last_output_time_ = std::chrono::steady_clock::now();
auto codec = Board::GetInstance().GetAudioCodec();
codec->EnableOutput(true);
}
void Application::SetDecodeSampleRate(int sample_rate, int frame_duration) {
if (opus_decoder_->sample_rate() == sample_rate && opus_decoder_->duration_ms() == frame_duration) {
return;
}
opus_decoder_.reset();
opus_decoder_ = std::make_unique<OpusDecoderWrapper>(sample_rate, 1, frame_duration);
auto codec = Board::GetInstance().GetAudioCodec();
if (opus_decoder_->sample_rate() != codec->output_sample_rate()) {
ESP_LOGI(TAG, "Resampling audio from %d to %d", opus_decoder_->sample_rate(), codec->output_sample_rate());
output_resampler_.Configure(opus_decoder_->sample_rate(), codec->output_sample_rate());
}
}
void Application::UpdateIotStates() {
auto& thing_manager = iot::ThingManager::GetInstance();
std::string states;
if (thing_manager.GetStatesJson(states, true)) {
protocol_->SendIotStates(states);
}
}
void Application::Reboot() {
ESP_LOGI(TAG, "Rebooting...");
esp_restart();
}
// 唤醒词触发函数
void Application::WakeWordInvoke(const std::string& wake_word) {
if (device_state_ == kDeviceStateIdle) {
ToggleChatState();
Schedule([this, wake_word]() {
if (protocol_) {
protocol_->SendWakeWordDetected(wake_word);
}
});
} else if (device_state_ == kDeviceStateSpeaking) {
//AbortSpeakingAndReturnToListening();// 使用唤醒词打断时立即切换到聆听状态
Schedule([this]() {
AbortSpeaking(kAbortReasonNone);
});
} else if (device_state_ == kDeviceStateListening) {
Schedule([this]() {
if (protocol_) {
protocol_->CloseAudioChannel();
}
});
}
}
bool Application::CanEnterSleepMode() {
if (device_state_ != kDeviceStateIdle) {
return false;
}
if (protocol_ && protocol_->IsAudioChannelOpened()) {
return false;
}
// Now it is safe to enter sleep mode
return true;
}
void Application::WaitForAudioPlayback() {
// 等待 audio_decode_queue_ 清空且音频输出完成
auto codec = Board::GetInstance().GetAudioCodec();
int timeout_count = 0;
const int max_timeout = 150; // 3秒超时 (150 * 20ms = 3000ms)
while (timeout_count < max_timeout) {
{
std::lock_guard<std::mutex> lock(mutex_);
if (audio_decode_queue_.empty()) {
// 检查音频输出是否已关闭或静音
if (!codec->output_enabled() || device_state_ != kDeviceStateSpeaking) {
ESP_LOGI(TAG, "Audio playback completed");
break;
}
}
}
vTaskDelay(pdMS_TO_TICKS(20));
timeout_count++;
}
if (timeout_count >= max_timeout) {
ESP_LOGW(TAG, "WaitForAudioPlayback timeout after 3 seconds");
}
}
bool Application::IsAudioQueueEmpty() {
std::lock_guard<std::mutex> lock(mutex_);
return audio_decode_queue_.empty();
}
void Application::ClearAudioQueue() {
std::lock_guard<std::mutex> lock(mutex_);
audio_decode_queue_.clear();
audio_paused_ = false; // 清除暂停状态
// ESP_LOGI(TAG, "🧹 音频播放队列已清空,暂停状态已清除");
ESP_LOGI(TAG, "🎵 测试模式:音频开始播放,等待播放完成"); // 生产测试打印
// 重新启用音频编解码器输出,确保后续音频能正常播放
auto& board = Board::GetInstance();
auto codec = board.GetAudioCodec();
if (codec) {
codec->EnableOutput(true);
// ESP_LOGI(TAG, "🔧 音频编解码器输出已重新启用");
ESP_LOGI(TAG, "✅ 测试模式:音频播放完成"); // 生产测试打印
}
}
// 🔧 检查当前是否可以安全执行操作
bool Application::IsSafeToOperate() {
// 检查是否正在执行中止操作
if (is_aborting_.load()) {
return false;
}
// 检查最近是否有操作过于频繁
auto now = std::chrono::steady_clock::now();
auto last_op = last_safe_operation_.load();
auto time_diff = std::chrono::duration_cast<std::chrono::milliseconds>(now - last_op);
// 如果距离上次操作少于50ms认为可能存在竞态风险
if (time_diff.count() < 50) {
ESP_LOGD(TAG, "Operation too frequent, waiting for safety");
return false;
}
return true;
}
void Application::StopAudioProcessor() {
#if CONFIG_USE_AUDIO_PROCESSOR
audio_processor_.Stop();
#endif
}
// 🔴 专门处理从说话状态到空闲状态的切换
void Application::AbortSpeakingAndReturnToIdle() {
ESP_LOGI(TAG, "🔴 AbortSpeakingAndReturnToIdle: Starting transition from speaking to idle state");
ESP_LOGI(TAG, "📊 当前设备状态: %s", STATE_STRINGS[device_state_]);
ESP_LOGI(TAG, "🎯 目标状态: idle (空闲状态)");
// 检查当前状态是否为说话状态
if (device_state_ != kDeviceStateSpeaking) {
ESP_LOGW(TAG, "🔴 AbortSpeakingAndReturnToIdle: Device not in speaking state, current state: %s", STATE_STRINGS[device_state_]);
return;
}
ESP_LOGI(TAG, "✅ 状态检查通过,当前处于说话状态");
// 检查操作安全性
if (!IsSafeToOperate()) {
ESP_LOGW(TAG, "🔴 AbortSpeakingAndReturnToIdle: Operation not safe, scheduling retry");
Schedule([this]() {
vTaskDelay(pdMS_TO_TICKS(100));
AbortSpeakingAndReturnToIdle();
});
return;
}
// 更新安全操作时间戳
last_safe_operation_.store(std::chrono::steady_clock::now());
ESP_LOGI(TAG, "⏰ 安全操作时间戳已更新");
// 立即停止音频处理
ESP_LOGI(TAG, "🔇 开始停止音频处理");
{
std::lock_guard<std::mutex> lock(mutex_);
if (!audio_decode_queue_.empty()) {
ESP_LOGI(TAG, "🗑️ 清空音频队列,当前队列大小: %zu", audio_decode_queue_.size());
audio_decode_queue_.clear();
current_speaker_volume_ = 0.0f;
#if CONFIG_USE_AUDIO_PROCESSOR
audio_processor_.SetSpeakerVolume(0.0f);
#endif
ESP_LOGI(TAG, "✅ 音频队列已清空音量已重置为0");
} else {
ESP_LOGI(TAG, " 音频队列已为空,无需清空");
}
}
ESP_LOGI(TAG, "🔴 AbortSpeakingAndReturnToIdle: Sending abort message to server");
// 发送中止消息给服务器
if (protocol_ && protocol_->IsAudioChannelOpened()) {
ESP_LOGI(TAG, "📡 WebSocket连接正常发送中止消息");
try {
protocol_->SendAbortSpeaking(kAbortReasonNone);
ESP_LOGI(TAG, "✅ 中止消息发送成功");
} catch (const std::exception& e) {
ESP_LOGW(TAG, "❌ 发送中止消息失败: %s", e.what());
}
// 延迟100ms后主动关闭连接确保服务器有时间处理中止消息
Schedule([this]() {
vTaskDelay(pdMS_TO_TICKS(100));
ESP_LOGI(TAG, "⏳ 延迟100ms后开始主动关闭WebSocket连接");
ESP_LOGI(TAG, "🔌 执行主动断开WebSocket连接");
if (protocol_) {
protocol_->CloseAudioChannel();
ESP_LOGI(TAG, "✅ CloseAudioChannel调用完成");
} else {
ESP_LOGW(TAG, "⚠️ protocol_为空无法关闭音频通道");
}
});
} else {
ESP_LOGW(TAG, "⚠️ WebSocket连接不可用强制关闭连接");
if (protocol_) {
ESP_LOGI(TAG, "🔌 强制执行WebSocket断开");
protocol_->CloseAudioChannel();
ESP_LOGI(TAG, "✅ 强制断开完成");
} else {
ESP_LOGW(TAG, "❌ protocol_为空无法执行断开操作");
}
}
ESP_LOGI(TAG, "🎯 主动断开流程已启动等待OnAudioChannelClosed回调触发状态转换");
ESP_LOGI(TAG, "📋 预期流程: WebSocket断开 → 回调触发 → 转换到idle状态 → 播放待机音");
}
// 🔵 专门处理从说话状态到聆听状态的切换
void Application::AbortSpeakingAndReturnToListening() {
ESP_LOGI(TAG, "🔵 AbortSpeakingAndReturnToListening: Starting transition from speaking to listening state (断开连接方案)");
// 检查当前状态是否为说话状态或可切换状态
// =========================================================================================
if (device_state_ != kDeviceStateSpeaking && device_state_ != kDeviceStateListening && device_state_ != kDeviceStateIdle) {
ESP_LOGW(TAG, "🔵 AbortSpeakingAndReturnToListening: Device not in valid state for transition, current state: %s", STATE_STRINGS[device_state_]);
return;
}
// 如果已经在listening状态直接返回避免重复切换
if (device_state_ == kDeviceStateListening) {
ESP_LOGI(TAG, "🔵 AbortSpeakingAndReturnToListening: Already in listening state, skipping transition");
return;
}
// 🔧 检查并处理音频播放状态(BOOT按键优化方案)
if (!audio_paused_ && device_state_ == kDeviceStateSpeaking) {
ESP_LOGI(TAG, "🔵 检测到播放状态,一次按键完成暂停和状态切换");
// 第一步:禁用音频输出(立即停止播放)
auto& board = Board::GetInstance();// 获取音频编解码器
auto codec = board.GetAudioCodec();// 获取音频编解码器
if (codec) {
codec->EnableOutput(false);// 暂停时禁用音频编解码器输出
ESP_LOGI(TAG, "🔧 暂停时禁用音频编解码器输出");
}
// 第二步:切换到暂停状态
audio_paused_ = true;
ESP_LOGI(TAG, "✅ 已切换到暂停状态");
// 第三步:立即执行状态切换逻辑(不返回,继续执行下面的代码)
ESP_LOGI(TAG, "🔵 继续执行状态切换到聆听状态");
}
// 🔧 检查并处理音频暂停状态(BOOT按键优化方案)
if (audio_paused_) {
ESP_LOGI(TAG, "🔵 检测到音频暂停状态应用BOOT按键优化方案");
audio_paused_ = false;
ESP_LOGI(TAG, "✅ 音频暂停状态已清除");
// 🔧 关键优化:清空音频播放队列,避免播放暂停时残留的音频
std::unique_lock<std::mutex> lock(mutex_);
audio_decode_queue_.clear();
lock.unlock();
ESP_LOGI(TAG, "🧹 已清空音频播放队列,避免播放残留音频");
// BOOT按键切换时的优化方案确保音频系统能正常响应状态切换
auto& board = Board::GetInstance();
auto codec = board.GetAudioCodec();
if (codec) {
codec->EnableOutput(true);
ESP_LOGI(TAG, "🔧 为状态切换重新启用音频编解码器输出");// 重新启用输出,后续可以播放
}
// 🔧 关键修复:强制停止音频处理器,确保后续状态切换时能重新启动
#if CONFIG_USE_AUDIO_PROCESSOR
if (audio_processor_.IsRunning()) {
ESP_LOGI(TAG, "🔧 强制停止音频处理器以确保状态切换成功");
audio_processor_.Stop();
}
#endif
// 🔧 音频暂停状态下直接切换,避免复杂的异步处理
ESP_LOGI(TAG, "🔵 音频暂停状态下直接执行状态切换");
// 播放提示音
if (codec && codec->output_enabled()) {
ESP_LOGI(TAG, "播放提示音:卡卡在呢");
// PlaySound(Lang::Sounds::P3_KAKAZAINNE); 原有蜡笔小新 音色播报
if(strcmp(CONFIG_DEVICE_ROLE, "KAKA") == 0){
PlaySound(Lang::Sounds::P3_KAKA_ZAINNE);
}
else if(strcmp(CONFIG_DEVICE_ROLE, "RTC_Test") == 0){
PlaySound(Lang::Sounds::P3_LALA_ZAINNE);
}
// 简化等待逻辑
vTaskDelay(pdMS_TO_TICKS(620)); // 等待音效播放完成
ESP_LOGI(TAG, "音频播放完成");
}
// 直接切换到聆听状态
SetDeviceState(kDeviceStateListening);
ESP_LOGI(TAG, "🔵 音频暂停状态下状态切换完成");
return;
}
// =========================================================================================
// 检查操作安全性
if (!IsSafeToOperate()) {
ESP_LOGW(TAG, "🔵 AbortSpeakingAndReturnToListening: Operation not safe, scheduling retry");
Schedule([this]() {
vTaskDelay(pdMS_TO_TICKS(100));
AbortSpeakingAndReturnToListening();
});
return;
}
// 更新安全操作时间戳
last_safe_operation_.store(std::chrono::steady_clock::now());
// 立即停止音频处理器和清空音频队列
#if CONFIG_USE_AUDIO_PROCESSOR
if (audio_processor_.IsRunning()) {
ESP_LOGI(TAG, "🔵 停止音频处理器");
audio_processor_.Stop();
}
// 清空音频队列并重置音量
if (!IsAudioQueueEmpty()) {
ESP_LOGI(TAG, "🔵 清空音频队列并重置音量");
while (!IsAudioQueueEmpty()) {
vTaskDelay(pdMS_TO_TICKS(10));
}
current_speaker_volume_ = 0.0f;
audio_processor_.SetSpeakerVolume(0.0f);
ESP_LOGI(TAG, "✅ 音频队列已清空音量已重置为0");
} else {
ESP_LOGI(TAG, " 音频队列已为空,无需清空");
}
#endif
ESP_LOGI(TAG, "🔵 AbortSpeakingAndReturnToListening: Sending abort message to server");
// 发送中止消息给服务器
if (protocol_ && protocol_->IsAudioChannelOpened()) {
ESP_LOGI(TAG, "📡 WebSocket连接正常发送中止消息");
try {
protocol_->SendAbortSpeaking(kAbortReasonVoiceInterrupt);
ESP_LOGI(TAG, "✅ 中止消息发送成功");
} catch (const std::exception& e) {
ESP_LOGW(TAG, "❌ 发送中止消息失败: %s", e.what());
}
// 延迟100ms后播放音效并直接切换到聆听状态不关闭WebSocket连接
Schedule([this]() {
vTaskDelay(pdMS_TO_TICKS(100));
ESP_LOGI(TAG, "⏳ 延迟100ms后播放音效并切换到聆听状态");
// 先播放"卡卡在呢"音效
ESP_LOGI(TAG, "🔵 AbortSpeakingAndReturnToListening: Playing KAKAZAINNE sound");
// 🔧 修复STATE:,确保硬件状态正确
auto& board = Board::GetInstance();
auto audio_codec = board.GetAudioCodec();
ESP_LOGI(TAG, "强制重新初始化音频输出");
audio_codec->EnableOutput(false); // 先关闭音频输出
vTaskDelay(pdMS_TO_TICKS(50)); // 短暂延迟让硬件复位
audio_codec->EnableOutput(true); // 再开启,强制硬件重新初始化
// 🔧 检查音频资源是否可用
if (audio_codec->output_enabled()) {
ESP_LOGI(TAG, "播放提示音:卡卡在呢");
ResetDecoder(); // 🔧 关键修复:重置解码器状态,清除残留
// 获取当前系统音量并临时设置以确保音效能播放
float system_volume = audio_codec ? (audio_codec->output_volume() / 100.0f) : 0.7f; // 默认70%
current_speaker_volume_ = system_volume;
#if CONFIG_USE_AUDIO_PROCESSOR
audio_processor_.SetSpeakerVolume(system_volume);
ESP_LOGI(TAG, "✅ 音量设置成功: %.2f", system_volume);
#endif
// PlaySound(Lang::Sounds::P3_KAKAZAINNE); 原有蜡笔小新 音色播报
if(strcmp(CONFIG_DEVICE_ROLE, "KAKA") == 0){
PlaySound(Lang::Sounds::P3_KAKA_ZAINNE);
}
else if(strcmp(CONFIG_DEVICE_ROLE, "RTC_Test") == 0){
PlaySound(Lang::Sounds::P3_LALA_ZAINNE);
}
// 🔧 修复:使用改进的等待逻辑,确保音频真正播放完成
ESP_LOGI(TAG, "等待音频播放完成...");
vTaskDelay(pdMS_TO_TICKS(100)); // 给音频足够的时间开始播放
// 等待音频队列清空 + 额外缓冲时间确保I2S硬件完成输出
int timeout_count = 0;
const int max_timeout = 150; // 3秒超时
while (timeout_count < max_timeout) {
if (IsAudioQueueEmpty()) {
// 队列清空后再等待500ms确保I2S硬件完成输出
ESP_LOGI(TAG, "音频队列已清空,等待硬件输出完成...");
vTaskDelay(pdMS_TO_TICKS(500));
ESP_LOGI(TAG, "音频播放完成");
break;
}
vTaskDelay(pdMS_TO_TICKS(20));
timeout_count++;
}
if (timeout_count >= max_timeout) {
ESP_LOGW(TAG, "等待音频播放超时,继续状态切换");
}
} else {
ESP_LOGW(TAG, "音频输出无法启用,跳过提示音播放");
}
// 直接切换到聆听状态,音频播放已在上面完成
ESP_LOGI(TAG, "🔵 AbortSpeakingAndReturnToListening: Switching to listening state (保持WebSocket连接)");
SetDeviceState(kDeviceStateListening);
});
} else {
ESP_LOGW(TAG, "⚠️ WebSocket连接不可用直接切换状态");
// 直接播放音效并切换状态
Schedule([this]() {
vTaskDelay(pdMS_TO_TICKS(100));
ESP_LOGI(TAG, "🔵 AbortSpeakingAndReturnToListening: Playing KAKAZAINNE sound");
// 🔧 修复:强制重新初始化音频输出,确保硬件状态正确
auto& board = Board::GetInstance();
auto audio_codec = board.GetAudioCodec();
ESP_LOGI(TAG, "强制重新初始化音频输出");
audio_codec->EnableOutput(false); // 先关闭音频输出
vTaskDelay(pdMS_TO_TICKS(50)); // 短暂延迟让硬件复位
audio_codec->EnableOutput(true); // 再开启,强制硬件重新初始化
// 🔧 检查音频资源是否可用
if (audio_codec->output_enabled()) {
ESP_LOGI(TAG, "播放提示音:卡卡在呢");
ResetDecoder(); // 🔧 关键修复:重置解码器状态,清除残留
// 获取当前系统音量并临时设置以确保音效能播放
float system_volume = audio_codec ? (audio_codec->output_volume() / 100.0f) : 0.7f; // 默认70%
current_speaker_volume_ = system_volume;
#if CONFIG_USE_AUDIO_PROCESSOR
audio_processor_.SetSpeakerVolume(system_volume);
ESP_LOGI(TAG, "✅ 音量设置成功: %.2f", system_volume);
#endif
// PlaySound(Lang::Sounds::P3_KAKAZAINNE); 原有蜡笔小新 音色播报
if(strcmp(CONFIG_DEVICE_ROLE, "KAKA") == 0){
PlaySound(Lang::Sounds::P3_KAKA_ZAINNE);
}
else if(strcmp(CONFIG_DEVICE_ROLE, "RTC_Test") == 0){
PlaySound(Lang::Sounds::P3_LALA_ZAINNE);
}
// 🔧 修复:使用改进的等待逻辑,确保音频真正播放完成
ESP_LOGI(TAG, "等待音频播放完成...");
vTaskDelay(pdMS_TO_TICKS(100)); // 给音频足够的时间开始播放
// 等待音频队列清空 + 额外缓冲时间确保I2S硬件完成输出
int timeout_count = 0;
const int max_timeout = 150; // 3秒超时
while (timeout_count < max_timeout) {
if (IsAudioQueueEmpty()) {
// 队列清空后再等待500ms确保I2S硬件完成输出
ESP_LOGI(TAG, "音频队列已清空,等待硬件输出完成...");
vTaskDelay(pdMS_TO_TICKS(500));
ESP_LOGI(TAG, "音频播放完成");
break;
}
vTaskDelay(pdMS_TO_TICKS(20));
timeout_count++;
}
if (timeout_count >= max_timeout) {
ESP_LOGW(TAG, "等待音频播放超时,继续状态切换");
}
} else {
ESP_LOGW(TAG, "音频输出无法启用,跳过提示音播放");
}
// 直接切换到聆听状态,音频播放已在上面完成
ESP_LOGI(TAG, "🔵 AbortSpeakingAndReturnToListening: Switching to listening state");
SetDeviceState(kDeviceStateListening);
});
}
ESP_LOGI(TAG, "🔵 AbortSpeakingAndReturnToListening: Transition initiated - keeping WebSocket connection and switching to listening");
}
// 姿态传感器接口实现
bool Application::IsImuSensorAvailable() {
auto& board = Board::GetInstance();
if (board.GetBoardType() == "movecall-moji-esp32s3") {
auto& moji_board = static_cast<MovecallMojiESP32S3&>(board);
return moji_board.IsImuInitialized();
}
return false;
}
bool Application::GetImuData(float* acc_x, float* acc_y, float* acc_z,
float* gyro_x, float* gyro_y, float* gyro_z,
float* temperature) {
auto& board = Board::GetInstance();
if (board.GetBoardType() == "movecall-moji-esp32s3") {
auto& moji_board = static_cast<MovecallMojiESP32S3&>(board);
qmi8658a_data_t imu_data;
if (moji_board.GetImuData(&imu_data)) {
if (acc_x) *acc_x = imu_data.acc_x;
if (acc_y) *acc_y = imu_data.acc_y;
if (acc_z) *acc_z = imu_data.acc_z;
if (gyro_x) *gyro_x = imu_data.gyro_x;
if (gyro_y) *gyro_y = imu_data.gyro_y;
if (gyro_z) *gyro_z = imu_data.gyro_z;
if (temperature) *temperature = imu_data.temperature;
return true;
}
}
return false;
}
void Application::ClearDialogIdleSkipSession() {
// 清除内存中的跳过标志
skip_dialog_idle_session_ = false;
// 清除NVS中的标志
Settings sys("system", true);
sys.SetInt("reboot_dlg_idle", 0);
sys.SetInt("reboot_origin", 0);
sys.Commit();
ESP_LOGI(TAG, "跳过对话待机会话标志已清除");
}
void Application::SetDialogUploadEnabled(bool enabled) {
dialog_upload_enabled_ = enabled;
ESP_LOGI(TAG, "对话上传状态已设置为: %s", enabled ? "启用" : "禁用");
}
void Application::OnMotionDetected() {
ESP_LOGI(TAG, "Motion detected by IMU sensor");
// 如果设备处于空闲状态,可以触发一些动作
if (device_state_ == kDeviceStateIdle) {
// 例如:显示运动检测提示
auto& board = Board::GetInstance();
auto display = board.GetDisplay();
display->SetChatMessage("system", "检测到运动");
// 可以在这里添加更多的运动检测处理逻辑
// 比如:唤醒设备、记录运动数据等
}
}
void Application::SetLowBatteryTransition(bool value) {
is_low_battery_transition_.store(value);// 设置低电量过渡状态
}
bool Application::IsLowBatteryTransition() const {
return is_low_battery_transition_.load();// 获取低电量过渡状态
}
// 🌐 初始化WebSocket协议RTC连接成功后调用
void Application::InitializeWebsocketProtocol() {
ESP_LOGI(TAG, "🌐 开始初始化WebSocket协议...");
// 检查是否已经初始化过
if (websocket_protocol_) {
ESP_LOGW(TAG, "⚠️ WebSocket协议已经初始化跳过重复初始化");
return;
}
// 创建WebsocketProtocol实例
ESP_LOGI(TAG, "🔧 创建WebsocketProtocol实例");
websocket_protocol_ = std::make_unique<WebsocketProtocol>();
websocket_protocol_->SetPrimary(false);
websocket_protocol_->OnIncomingAudio([this](std::vector<uint8_t>&& data) {
if (!ws_downlink_enabled_.load()) {
return;
}
opus_playback_active_.store(true);
std::lock_guard<std::mutex> lock(mutex_);
size_t len = data.size();
audio_decode_queue_.emplace_back(std::move(data));
ESP_LOGD(TAG, "WS辅助音频入队: 字节=%zu 队列大小=%zu", len, audio_decode_queue_.size());
});
websocket_protocol_->OnIncomingJson([this](const cJSON* root) {
auto type = cJSON_GetObjectItem(root, "type");
if (type && cJSON_IsString(type) && type->valuestring) {
ESP_LOGD(TAG, "WS辅助JSON消息: %s", type->valuestring);
if (strcmp(type->valuestring, "hello") == 0) {
auto audio_params = cJSON_GetObjectItem(root, "audio_params");
if (audio_params && cJSON_IsObject(audio_params)) {
auto sr = cJSON_GetObjectItem(audio_params, "sample_rate");
auto fd = cJSON_GetObjectItem(audio_params, "frame_duration");
int sample_rate = (sr && cJSON_IsNumber(sr)) ? sr->valueint : 16000;
int frame_duration = (fd && cJSON_IsNumber(fd)) ? fd->valueint : 60;
SetDecodeSampleRate(sample_rate, frame_duration);
} else {
SetDecodeSampleRate(16000, 60);
}
}
}
});
// 启动WebSocket协议
ESP_LOGI(TAG, "🚀 启动WebSocket协议");
websocket_protocol_->Start();// 启动WebSocket协议
ESP_LOGI(TAG, "✅ WebSocket协议初始化完成");
}
// void Application::SendTextViaWebsocket(const std::string& text) {
// Schedule([this, text]() {
// if (websocket_protocol_ && websocket_protocol_->IsAudioChannelOpened()) {
// websocket_protocol_->SendTextMessage(text);
// ESP_LOGI(TAG, "WS辅助文本发送%s", text.c_str());
// } else {
// ESP_LOGW(TAG, "WS辅助未连接丢弃文本%s", text.c_str());
// }
// });
// }
// ============================================================
// BLE JSON 通讯服务集成
// ============================================================
const char* Application::DeviceStateToString(DeviceState state) {
int idx = static_cast<int>(state);
if (idx >= 0 && idx < static_cast<int>(sizeof(STATE_STRINGS) / sizeof(STATE_STRINGS[0]))) {
return STATE_STRINGS[idx];
}
return "unknown";
}
// BLE JSON Service 命令处理(暂不使用,保留代码)
#if 0
void Application::HandleBleJsonCommand(const std::string& cmd, int msg_id, cJSON* data, BleJsonService& service) {
auto& board = Board::GetInstance();
if (cmd == "ping") {
service.SendResponse(cmd, msg_id, 0, "pong");
return;
}
if (cmd == "status") {
cJSON* resp = cJSON_CreateObject();
cJSON_AddStringToObject(resp, "s", DeviceStateToString(device_state_));
int battery_level = 0;
bool charging = false, discharging = false;
if (board.GetBatteryLevel(battery_level, charging, discharging)) {
cJSON_AddNumberToObject(resp, "bat", battery_level);
cJSON_AddBoolToObject(resp, "chg", charging);
}
auto* codec = board.GetAudioCodec();
if (codec) {
cJSON_AddNumberToObject(resp, "vol", codec->output_volume());
}
wifi_ap_record_t ap{};
if (esp_wifi_sta_get_ap_info(&ap) == ESP_OK) {
cJSON_AddStringToObject(resp, "ssid", reinterpret_cast<const char*>(ap.ssid));
cJSON_AddNumberToObject(resp, "rssi", ap.rssi);
}
service.SendResponse(cmd, msg_id, 0, "ok", resp);
cJSON_Delete(resp);
return;
}
if (cmd == "dev_info") {
cJSON* resp = cJSON_CreateObject();
cJSON_AddStringToObject(resp, "mac", SystemInfo::GetMacAddress().c_str());
cJSON_AddStringToObject(resp, "board", BOARD_NAME);
auto app_desc = esp_app_get_description();
cJSON_AddStringToObject(resp, "fw", app_desc->version);
cJSON_AddStringToObject(resp, "chip", SystemInfo::GetChipModelName().c_str());
cJSON_AddStringToObject(resp, "idf", app_desc->idf_ver);
service.SendResponse(cmd, msg_id, 0, "ok", resp);
cJSON_Delete(resp);
return;
}
if (cmd == "set_wifi") {
cJSON* ssid_item = cJSON_GetObjectItem(data, "ssid");
cJSON* pwd_item = cJSON_GetObjectItem(data, "pwd");
if (!ssid_item || !cJSON_IsString(ssid_item) || strlen(ssid_item->valuestring) == 0) {
service.SendResponse(cmd, msg_id, -1, "missing ssid");
return;
}
wifi_config_t wifi_config = {};
strncpy(reinterpret_cast<char*>(wifi_config.sta.ssid),
ssid_item->valuestring, sizeof(wifi_config.sta.ssid) - 1);
if (pwd_item && cJSON_IsString(pwd_item)) {
strncpy(reinterpret_cast<char*>(wifi_config.sta.password),
pwd_item->valuestring, sizeof(wifi_config.sta.password) - 1);
}
esp_err_t ret = esp_wifi_set_config(WIFI_IF_STA, &wifi_config);
if (ret != ESP_OK) {
service.SendResponse(cmd, msg_id, -2, "set config failed");
return;
}
esp_wifi_disconnect();
ret = esp_wifi_connect();
service.SendResponse(cmd, msg_id, 0,
ret == ESP_OK ? "connecting" : "connect failed");
return;
}
if (cmd == "wifi_list") {
wifi_scan_config_t scan_config = {};
scan_config.show_hidden = false;
esp_err_t ret = esp_wifi_scan_start(&scan_config, true);
if (ret != ESP_OK) {
service.SendResponse(cmd, msg_id, -1, "scan failed");
return;
}
uint16_t ap_count = 0;
esp_wifi_scan_get_ap_num(&ap_count);
if (ap_count > BLE_JSON_WIFI_LIST_MAX) {
ap_count = BLE_JSON_WIFI_LIST_MAX;
}
wifi_ap_record_t* ap_list = nullptr;
cJSON* resp = cJSON_CreateObject();
cJSON* arr = cJSON_AddArrayToObject(resp, "list");
if (ap_count > 0) {
ap_list = static_cast<wifi_ap_record_t*>(malloc(sizeof(wifi_ap_record_t) * ap_count));
if (ap_list && esp_wifi_scan_get_ap_records(&ap_count, ap_list) == ESP_OK) {
for (int i = 0; i < ap_count; i++) {
cJSON* item = cJSON_CreateObject();
cJSON_AddStringToObject(item, "ssid",
reinterpret_cast<const char*>(ap_list[i].ssid));
cJSON_AddNumberToObject(item, "rssi", ap_list[i].rssi);
cJSON_AddNumberToObject(item, "auth", ap_list[i].authmode);
cJSON_AddItemToArray(arr, item);
}
}
free(ap_list);
}
service.SendResponse(cmd, msg_id, 0, "ok", resp);
cJSON_Delete(resp);
return;
}
if (cmd == "set_vol") {
cJSON* vol_item = cJSON_GetObjectItem(data, "vol");
if (!vol_item || !cJSON_IsNumber(vol_item)) {
service.SendResponse(cmd, msg_id, -1, "missing vol");
return;
}
int vol = vol_item->valueint;
if (vol < 0) vol = 0;
if (vol > 100) vol = 100;
auto* codec = board.GetAudioCodec();
if (codec) {
codec->SetOutputVolume(vol);
Settings s("audio", true);
s.SetInt("output_volume", vol);
}
service.SendResponse(cmd, msg_id, 0, "ok");
return;
}
if (cmd == "reboot") {
service.SendResponse(cmd, msg_id, 0, "rebooting");
vTaskDelay(pdMS_TO_TICKS(500));
Reboot();
return;
}
if (cmd == "ota") {
if (device_state_ == kDeviceStateUpgrading) {
service.SendResponse(cmd, msg_id, -1, "already upgrading");
return;
}
service.SendResponse(cmd, msg_id, 0, "start ota");
Schedule([this]() {
CheckNewVersion();
});
return;
}
if (cmd == "iot") {
auto& thing_manager = iot::ThingManager::GetInstance();
std::string states;
if (thing_manager.GetStatesJson(states, true)) {
cJSON* resp = cJSON_Parse(states.c_str());
service.SendResponse(cmd, msg_id, 0, "ok", resp);
if (resp) cJSON_Delete(resp);
} else {
service.SendResponse(cmd, msg_id, 0, "ok");
}
return;
}
service.SendResponse(cmd, msg_id, -99, "unknown cmd");
}
#endif