feat: HTTPS故事播放 + RTC/HTTPS双向音频切换状态机 + 协议层优化

1、新增HTTPS故事播放功能:SendStoryRequest通过蓝牙MAC请求故事API,支持intro+body两段式无缝播放,替换原WebSocket故事请求方式;
2、新增HttpsPlaybackFromUrl通用HTTPS音频下载播放方法,支持JSON格式Opus帧流式解码播放;
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;AbortSpeaking也新增DMA缓冲区flush确保扬声器立即静音;
5、协议层新增OnBotMessage回调,非字幕Bot下行消息立即中止HTTPS播放;volc_rtc_protocol移除is_binary依赖改为直接前缀检测,新增info前缀识别,subv字幕排除on_bot_message_由subtitle handler单独处理;
6、subtitle字幕USER/AI区分从CONFIG_VOLC_DEVICE_NAME比较改为bot_前缀判断,用户说话时立即中止HTTPS播放;
7、Kconfig新增STORY_API_URL故事播放API地址配置;
8、设备注册RTC服务时,设备名称从Wi-Fi MAC地址改为使用蓝牙MAC地址

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Rdzleo 2026-03-05 13:45:52 +08:00
parent 1e7ba0763a
commit ccea0c681c
7 changed files with 790 additions and 76 deletions

View File

@ -12,6 +12,12 @@ config DEVICE_STATUS_REPORT_URL
help help
URL for reporting device status to server URL for reporting device status to server
config STORY_API_URL
string "Story API URL"
default "http://192.168.124.8:8000/api/v1/devices/stories/"
help
故事播放API接口地址设备会附加 ?mac_address=XX:XX:XX:XX:XX:XX 参数请求
choice choice
prompt "语言选择" prompt "语言选择"
default LANGUAGE_ZH_CN default LANGUAGE_ZH_CN

View File

@ -28,6 +28,8 @@
#include <cmath> #include <cmath>
#include <chrono> #include <chrono>
#include <esp_wifi.h> #include <esp_wifi.h>
#include <esp_http_client.h>
#include <esp_crt_bundle.h>
#include <nvs.h> #include <nvs.h>
#define TAG "Application" #define TAG "Application"
@ -737,12 +739,23 @@ void Application::Start() {
Alert(Lang::Strings::ERROR, message.c_str(), "sad", Lang::Sounds::P3_EXCLAMATION); Alert(Lang::Strings::ERROR, message.c_str(), "sad", Lang::Sounds::P3_EXCLAMATION);
} }
}); });
// 收到非字幕的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) { protocol_->OnIncomingAudio([this](std::vector<uint8_t>&& data) {
// HTTPS播放中含HTTP请求阶段静默丢弃RTC PCM包
if (https_playback_active_.load() || opus_playback_active_.load()) {
return;
}
if (websocket_protocol_ && websocket_protocol_->IsAudioChannelOpened()) { if (websocket_protocol_ && websocket_protocol_->IsAudioChannelOpened()) {
aborted_ = true; aborted_ = true;
{ {
std::lock_guard<std::mutex> lock(mutex_);// 🔒 保护音频队列操作 std::lock_guard<std::mutex> lock(mutex_);// 🔒 保护音频队列操作
// 如果音频队列不为空
if (!audio_decode_queue_.empty()) { if (!audio_decode_queue_.empty()) {
ESP_LOGI(TAG, "清空音频队列,大小=%zu", audio_decode_queue_.size()); ESP_LOGI(TAG, "清空音频队列,大小=%zu", audio_decode_queue_.size());
audio_decode_queue_.clear();// 清空音频队列 audio_decode_queue_.clear();// 清空音频队列
@ -750,7 +763,7 @@ void Application::Start() {
} }
ResetDecoder(); ResetDecoder();
ws_downlink_enabled_.store(false); ws_downlink_enabled_.store(false);
ws_playback_active_.store(false); opus_playback_active_.store(false);
websocket_protocol_->CloseAudioChannel();// 关闭WebSocket通道 websocket_protocol_->CloseAudioChannel();// 关闭WebSocket通道
Schedule([this]() { Schedule([this]() {
vTaskDelay(pdMS_TO_TICKS(120)); vTaskDelay(pdMS_TO_TICKS(120));
@ -1250,16 +1263,18 @@ void Application::Start() {
if (!text || !cJSON_IsString(text) || !text->valuestring[0]) continue; if (!text || !cJSON_IsString(text) || !text->valuestring[0]) continue;
bool is_final = definite && cJSON_IsTrue(definite); bool is_final = definite && cJSON_IsTrue(definite);
bool is_user = false; // userId 以 "bot_" 开头为AI其余为用户
bool is_user = true;
if (user_id && cJSON_IsString(user_id)) { if (user_id && cJSON_IsString(user_id)) {
is_user = (strcmp(user_id->valuestring, CONFIG_VOLC_DEVICE_NAME) == 0); if (strncmp(user_id->valuestring, "bot_", 4) == 0) {
is_user = false;
}
} }
std::string msg = text->valuestring; std::string msg = text->valuestring;
std::string emotion_str; std::string emotion_str;
// 提取并剥离字幕开头的情绪标签(如 "(平静)今天..." // 提取并剥离字幕开头的情绪标签(如 "(平静)今天..."
// UTF-8 中文全角括号:(= E2 80 98? 不对,(= \xef\xbc\x88= \xef\xbc\x89
// 实际 UTF-8= 0xEF 0xBC 0x88= 0xEF 0xBC 0x89 // 实际 UTF-8= 0xEF 0xBC 0x88= 0xEF 0xBC 0x89
if (!is_user && msg.size() >= 6) { if (!is_user && msg.size() >= 6) {
const char* p = msg.c_str(); const char* p = msg.c_str();
@ -1313,8 +1328,14 @@ void Application::Start() {
} }
} }
const char* role = is_user ? "user" : "assistant"; const char* role = is_user ? "USER" : "AI";
ESP_LOGI(TAG, "%s %s: %s", is_final ? ">>" : "..", role, msg.c_str()); ESP_LOGI(TAG, "%s %s: %s", is_final ? "📝" : "..", role, msg.c_str());
// 用户说话时立即中止HTTPS音频播放
// subv字幕消息在协议层跳过了on_bot_message_由此处直接处理
if (is_user && https_playback_active_.load() && !https_playback_abort_.load()) {
AbortHttpsPlayback("检测到用户说话(字幕)");
}
Schedule([this, display, msg, role_str = std::string(role)]() { Schedule([this, display, msg, role_str = std::string(role)]() {
display->SetChatMessage(role_str.c_str(), msg.c_str()); display->SetChatMessage(role_str.c_str(), msg.c_str());
}); });
@ -1999,16 +2020,23 @@ void Application::OnAudioOutput() {
auto opus = std::move(audio_decode_queue_.front()); auto opus = std::move(audio_decode_queue_.front());
audio_decode_queue_.pop_front(); audio_decode_queue_.pop_front();
// 在出队时捕获opus解码标志避免background_task异步执行时标志已变化
// 导致残留的Opus帧被当作PCM播放产生杂音
bool is_opus_frame = opus_playback_active_.load();
lock.unlock(); lock.unlock();
background_task_->Schedule([this, codec, opus = std::move(opus)]() mutable { background_task_->Schedule([this, codec, opus = std::move(opus), is_opus_frame]() mutable {
if (aborted_) { if (aborted_) {
return; return;
} }
// 跳过已中止的HTTPS opus残留帧出队时is_opus_frame=true但中止后opus_playback_active_=false
if (is_opus_frame && !opus_playback_active_.load()) {
return;
}
std::vector<int16_t> pcm; std::vector<int16_t> pcm;
bool decoded = false; bool decoded = false;
bool treat_as_pcm = (protocol_ && protocol_->downlink_is_pcm() && !ws_playback_active_.load()); bool treat_as_pcm = (protocol_ && protocol_->downlink_is_pcm() && !is_opus_frame);
if (!treat_as_pcm) { if (!treat_as_pcm) {
decoded = opus_decoder_->Decode(std::move(opus), pcm); decoded = opus_decoder_->Decode(std::move(opus), pcm);
} }
@ -2331,6 +2359,12 @@ void Application::AbortSpeaking(AbortReason reason) {
ESP_LOGI(TAG, "🔴 Abort speaking - immediate stop"); ESP_LOGI(TAG, "🔴 Abort speaking - immediate stop");
aborted_ = true; 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()); last_safe_operation_.store(std::chrono::steady_clock::now());
@ -2352,6 +2386,20 @@ void Application::AbortSpeaking(AbortReason reason) {
// ⚠️ 移除WaitForCompletion避免死锁让后台任务通过aborted_标志自然结束 // ⚠️ 移除WaitForCompletion避免死锁让后台任务通过aborted_标志自然结束
ESP_LOGI(TAG, "🔴 Audio queue cleared, background tasks will stop on next iteration"); ESP_LOGI(TAG, "🔴 Audio queue cleared, background tasks will stop on next iteration");
// 重启codec输出以清空I2S DMA缓冲区中残留音频确保扬声器立即静音
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限制 // 🔧 修复始终尝试发送中止消息以打断RTC下行不受IsSafeToOperate限制
if (protocol_) { if (protocol_) {
try { try {
@ -2382,24 +2430,681 @@ void Application::AbortSpeaking(AbortReason reason) {
is_aborting_.store(false); is_aborting_.store(false);
} }
// 发送讲故事请求 webscoket协议 // 中止HTTPS音频播放清空队列、重置解码器、清除标志、DMA flush
void Application::SendStoryRequest() { void Application::AbortHttpsPlayback(const char* reason) {
if (!websocket_protocol_) { ESP_LOGI(TAG, "🔴 %s中止HTTPS音频播放", reason);
InitializeWebsocketProtocol();// 初始化WebSocket协议 https_playback_abort_.store(true);
if (!websocket_protocol_) { {
ESP_LOGW(TAG, "WebSocket协议初始化失败"); std::lock_guard<std::mutex> lock(mutex_);
return; if (!audio_decode_queue_.empty()) {
ESP_LOGI(TAG, "清空HTTPS音频队列大小=%zu", audio_decode_queue_.size());
audio_decode_queue_.clear();
} }
} }
Schedule([this]() { ResetDecoder();
ws_downlink_enabled_.store(true); opus_playback_active_.store(false);
// 确保音频通道已打开 https_playback_active_.store(false);
if (!websocket_protocol_->IsAudioChannelOpened()) { ESP_LOGI(TAG, "🔴 HTTPS播放标志已清除RTC音频通道已打开");
websocket_protocol_->OpenAudioChannel();// 打开音频通道 // 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缓冲区已清空");
} }
websocket_protocol_->SendStoryRequest();// 发送故事请求 vTaskDelete(NULL);
ESP_LOGI(TAG, "通过WebSocket发送的故事请求"); }, "dma_flush", 4096, NULL, 10, NULL);
}); }
// 通过HTTPS下载JSON并播放音频故事/歌曲等)
void Application::HttpsPlaybackFromUrl(const std::string& url) {
// 防止重复启动
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标志覆盖HTTP请求阶段阻断RTC PCM
app.opus_playback_active_.store(true);
app.https_playback_abort_.store(false);
ESP_LOGI(TAG, "[HTTPS播放] 开始下载: %s", playback_url.c_str());
// 配置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
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;
}
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()) {
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_data.clear();
json_data.shrink_to_fit();
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* 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 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, 帧时长=%dms, 总帧数=%d",
sample_rate, frame_duration, frame_count);
app.SetDecodeSampleRate(sample_rate, frame_duration);
app.https_playback_active_.store(true);
// 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;
}
// 逐帧base64解码并入队播放
int enqueued = 0;
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) continue;
const char* b64 = frame_item->valuestring;
size_t b64_len = strlen(b64);
if (b64_len == 0) 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;
}
{
std::lock_guard<std::mutex> lock(app.mutex_);
app.audio_decode_queue_.emplace_back(std::move(opus_frame));
}
enqueued++;
// 控制入队速度:队列过大时等待消费
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));
}
if (enqueued % 100 == 0) {
ESP_LOGI(TAG, "[HTTPS播放] 进度: %d/%d 帧 (%.0f%%)",
enqueued, frame_count, enqueued * 100.0f / frame_count);
}
}
cJSON_Delete(root);
// 等待队列播放完毕(或被中止)
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);
}
// 通过故事API请求并播放故事intro标题 + body正文无缝衔接
void Application::SendStoryRequest() {
// 防止重复启动
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标志覆盖HTTP请求阶段阻断RTC PCM
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响应
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);
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);
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()) {
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()) {
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();
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;
}
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;
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) continue;
const char* b64 = fi->valuestring;
size_t b64_len = strlen(b64);
if (b64_len == 0) 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));
}
if (body_enqueued % 100 == 0) {
ESP_LOGI(TAG, "[故事API] body进度: %d/%d (%.0f%%)",
body_enqueued, body_count,
body_enqueued * 100.0f / body_count);
}
}
cJSON_Delete(opus_root);
ESP_LOGI(TAG, "[故事API] body入队完成: %d帧", body_enqueued);
// ========== 步骤6: 等待播放完毕 ==========
if (!app.https_playback_abort_.load()) {
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));
}
}
app.https_playback_active_.store(false);
app.https_playback_abort_.store(false);
app.opus_playback_active_.store(false);
ESP_LOGI(TAG, "[故事API] 播放结束, 堆: %lu",
(unsigned long)esp_get_free_heap_size());
vTaskDelete(NULL);
}, "story_play", 10240, NULL, 5, NULL);
} }
// 设置监听模式 // 设置监听模式

View File

@ -70,6 +70,8 @@ public:
void Alert(const char* status, const char* message, const char* emotion = "", const std::string_view& sound = "");// 警报管理 状态、消息、情感、声音 void Alert(const char* status, const char* message, const char* emotion = "", const std::string_view& sound = "");// 警报管理 状态、消息、情感、声音
void DismissAlert();// 关闭警报 void DismissAlert();// 关闭警报
void AbortSpeaking(AbortReason reason);// 打断语音播报 void AbortSpeaking(AbortReason reason);// 打断语音播报
void AbortHttpsPlayback(const char* reason);// 中止HTTPS音频播放并清空DMA
void HttpsPlaybackFromUrl(const std::string& url); // 通过HTTPS下载JSON并播放音频故事/歌曲等)
void SendStoryRequest(); // 发送讲故事 请求 void SendStoryRequest(); // 发送讲故事 请求
void ToggleChatState();// 切换聊天状态 void ToggleChatState();// 切换聊天状态
void ToggleListeningState();// 切换监听状态 void ToggleListeningState();// 切换监听状态
@ -146,6 +148,9 @@ private:
#endif #endif
std::atomic<bool> ws_downlink_enabled_{true};// 🌐 WebSocket下行通道是否启用 std::atomic<bool> ws_downlink_enabled_{true};// 🌐 WebSocket下行通道是否启用
std::atomic<bool> ws_playback_active_{false};// 🌐 WebSocket下行播放活跃标志 std::atomic<bool> ws_playback_active_{false};// 🌐 WebSocket下行播放活跃标志
std::atomic<bool> opus_playback_active_{false};// Opus解码播放活跃标志WS/HTTPS共用
std::atomic<bool> https_playback_active_{false};// HTTPS音频播放进行中标志
std::atomic<bool> https_playback_abort_{false};// HTTPS音频播放中止标志
bool aborted_ = false; bool aborted_ = false;
bool voice_detected_ = false; bool voice_detected_ = false;
bool audio_paused_ = false; // 音频暂停状态标志 bool audio_paused_ = false; // 音频暂停状态标志

View File

@ -24,6 +24,10 @@ void Protocol::OnNetworkError(std::function<void(const std::string& message)> ca
on_network_error_ = callback; on_network_error_ = callback;
} }
void Protocol::OnBotMessage(std::function<void()> callback) {
on_bot_message_ = callback;
}
void Protocol::SetError(const std::string& message) { void Protocol::SetError(const std::string& message) {
error_occurred_ = true; error_occurred_ = true;
if (on_network_error_ != nullptr) { if (on_network_error_ != nullptr) {

View File

@ -49,6 +49,7 @@ public:
void OnAudioChannelOpened(std::function<void()> callback); void OnAudioChannelOpened(std::function<void()> callback);
void OnAudioChannelClosed(std::function<void()> callback); void OnAudioChannelClosed(std::function<void()> callback);
void OnNetworkError(std::function<void(const std::string& message)> callback); void OnNetworkError(std::function<void(const std::string& message)> callback);
void OnBotMessage(std::function<void()> callback);
virtual void Start() = 0; virtual void Start() = 0;
virtual bool OpenAudioChannel() = 0; virtual bool OpenAudioChannel() = 0;
@ -76,6 +77,7 @@ protected:
std::function<void()> on_audio_channel_opened_; std::function<void()> on_audio_channel_opened_;
std::function<void()> on_audio_channel_closed_; std::function<void()> on_audio_channel_closed_;
std::function<void(const std::string& message)> on_network_error_; std::function<void(const std::string& message)> on_network_error_;
std::function<void()> on_bot_message_;
int server_sample_rate_ = 24000; int server_sample_rate_ = 24000;
int server_frame_duration_ = 60; int server_frame_duration_ = 60;

View File

@ -111,14 +111,14 @@ void VolcRtcProtocol::Start() {
iot_info_.device_name = (char*)CONFIG_VOLC_DEVICE_NAME; iot_info_.device_name = (char*)CONFIG_VOLC_DEVICE_NAME;
ESP_LOGI(TAG, "使用配置文件中的设备名称: %s", iot_info_.device_name); ESP_LOGI(TAG, "使用配置文件中的设备名称: %s", iot_info_.device_name);
} else { } else {
// 配置文件中的设备名称为空,使用MAC地址作为设备名称 // 配置文件中的设备名称为空,使用蓝牙MAC地址作为设备名称
std::string mac_address = SystemInfo::GetMacAddress(); std::string mac_address = SystemInfo::GetBleMacAddress();
// MAC地址中替换冒号为下划线,避免文件名中包含冒号 // MAC地址中替换冒号为下划线,避免文件名中包含冒号
std::replace(mac_address.begin(), mac_address.end(), ':', '_'); std::replace(mac_address.begin(), mac_address.end(), ':', '_');
char* mac_buffer = (char*)malloc(mac_address.length() + 1); char* mac_buffer = (char*)malloc(mac_address.length() + 1);
strcpy(mac_buffer, mac_address.c_str()); strcpy(mac_buffer, mac_address.c_str());
iot_info_.device_name = mac_buffer; iot_info_.device_name = mac_buffer;
ESP_LOGI(TAG, "使用Wi-Fi MAC地址作为设备名称(已替换冒号为下划线): %s", iot_info_.device_name); ESP_LOGI(TAG, "使用蓝牙MAC地址作为设备名称(已替换冒号为下划线): %s", iot_info_.device_name);
} }
Settings s("volc"); Settings s("volc");
@ -583,37 +583,45 @@ void VolcRtcProtocol::DataCallback(void* context, const void* data, size_t len,
} }
} }
protocol->ProcessAudioData(data, len);// 处理音频数据 protocol->ProcessAudioData(data, len);// 处理音频数据
} else if (info->type == VOLC_DATA_TYPE_MESSAGE) { } else if (info->type == VOLC_DATA_TYPE_MESSAGE) {
if (data && len > 0) { if (data && len > 0) {
const uint8_t* buf = static_cast<const uint8_t*>(data); const uint8_t* buf = static_cast<const uint8_t*>(data);
std::string json_text; std::string json_text;
if (info->info.message.is_binary && len >= 8) { bool is_subv = false;
bool is_ctrl = (memcmp(buf, "ctrl", 4) == 0);
bool is_conv = (memcmp(buf, "conv", 4) == 0); // 不依赖 is_binary 字段SDK始终返回false直接检测前缀
bool is_tool = (memcmp(buf, "tool", 4) == 0); if (len >= 8) {
bool is_subv = (memcmp(buf, "subv", 4) == 0); bool is_ctrl = (memcmp(buf, "ctrl", 4) == 0);
if (is_ctrl || is_conv || is_tool || is_subv) { bool is_conv = (memcmp(buf, "conv", 4) == 0);
uint32_t json_len = (uint32_t)((buf[4] << 24) | (buf[5] << 16) | (buf[6] << 8) | (buf[7])); bool is_tool = (memcmp(buf, "tool", 4) == 0);
if (json_len > 0 && (size_t)(8 + json_len) <= len) { is_subv = (memcmp(buf, "subv", 4) == 0);
json_text.assign(reinterpret_cast<const char*>(buf + 8), json_len); bool is_info = (memcmp(buf, "info", 4) == 0);
if (!protocol->suppress_incoming_message_log_) { if (is_ctrl || is_conv || is_tool || is_subv || is_info) {
if (is_subv) { uint32_t json_len = (uint32_t)((buf[4] << 24) | (buf[5] << 16) | (buf[6] << 8) | (buf[7]));
ESP_LOGI(TAG, "接收下行二进制消息(字幕)"); if (json_len > 0 && (size_t)(8 + json_len) <= len) {
} else { json_text.assign(reinterpret_cast<const char*>(buf + 8), json_len);
const char* prefix = is_ctrl ? "ctrl" : (is_conv ? "conv" : "tool"); if (!is_subv && !protocol->suppress_incoming_message_log_) {
ESP_LOGI(TAG, "接收下行二进制消息(%s): %.*s", prefix, (int)json_text.size(), json_text.c_str()); const char* prefix = is_ctrl ? "ctrl" : (is_conv ? "conv" : (is_tool ? "tool" : "info"));
} ESP_LOGI(TAG, "接收下行消息(%s): %.*s", prefix, (int)json_text.size(), json_text.c_str());
}
}
} }
} }
if (json_text.empty()) { }
json_text.assign(reinterpret_cast<const char*>(data), len); }
if (!protocol->suppress_incoming_message_log_) {
ESP_LOGI(TAG, "接收下行消息: %.*s", (int)json_text.size(), json_text.c_str()); if (json_text.empty()) {
} json_text.assign(reinterpret_cast<const char*>(data), len);
} if (!protocol->suppress_incoming_message_log_) {
cJSON* root = cJSON_Parse(json_text.c_str()); ESP_LOGI(TAG, "接收下行消息: %.*s", (int)json_text.size(), json_text.c_str());
}
}
// 非subv消息立即通知应用层中止HTTPS播放
// subv字幕消息由subtitle handler处理可区分USER/AI
if (!is_subv && protocol->on_bot_message_) {
protocol->on_bot_message_();
}
cJSON* root = cJSON_Parse(json_text.c_str());
if (root) { if (root) {
const char* sid_keys[] = {"sessionId", "session_id", "sid"}; const char* sid_keys[] = {"sessionId", "session_id", "sid"};
cJSON* sid = nullptr; cJSON* sid = nullptr;

View File

@ -14,7 +14,6 @@ CONFIG_SOC_GDMA_SUPPORTED=y
CONFIG_SOC_AHB_GDMA_SUPPORTED=y CONFIG_SOC_AHB_GDMA_SUPPORTED=y
CONFIG_SOC_GPTIMER_SUPPORTED=y CONFIG_SOC_GPTIMER_SUPPORTED=y
CONFIG_SOC_LCDCAM_SUPPORTED=y CONFIG_SOC_LCDCAM_SUPPORTED=y
CONFIG_SOC_LCDCAM_CAM_SUPPORTED=y
CONFIG_SOC_LCDCAM_I80_LCD_SUPPORTED=y CONFIG_SOC_LCDCAM_I80_LCD_SUPPORTED=y
CONFIG_SOC_LCDCAM_RGB_LCD_SUPPORTED=y CONFIG_SOC_LCDCAM_RGB_LCD_SUPPORTED=y
CONFIG_SOC_MCPWM_SUPPORTED=y CONFIG_SOC_MCPWM_SUPPORTED=y
@ -102,7 +101,7 @@ CONFIG_SOC_CPU_HAS_FPU=y
CONFIG_SOC_HP_CPU_HAS_MULTIPLE_CORES=y CONFIG_SOC_HP_CPU_HAS_MULTIPLE_CORES=y
CONFIG_SOC_CPU_BREAKPOINTS_NUM=2 CONFIG_SOC_CPU_BREAKPOINTS_NUM=2
CONFIG_SOC_CPU_WATCHPOINTS_NUM=2 CONFIG_SOC_CPU_WATCHPOINTS_NUM=2
CONFIG_SOC_CPU_WATCHPOINT_MAX_REGION_SIZE=0x40 CONFIG_SOC_CPU_WATCHPOINT_MAX_REGION_SIZE=64
CONFIG_SOC_SIMD_PREFERRED_DATA_ALIGNMENT=16 CONFIG_SOC_SIMD_PREFERRED_DATA_ALIGNMENT=16
CONFIG_SOC_DS_SIGNATURE_MAX_BIT_LEN=4096 CONFIG_SOC_DS_SIGNATURE_MAX_BIT_LEN=4096
CONFIG_SOC_DS_KEY_PARAM_MD_IV_LENGTH=16 CONFIG_SOC_DS_KEY_PARAM_MD_IV_LENGTH=16
@ -209,7 +208,7 @@ CONFIG_SOC_RTCIO_INPUT_OUTPUT_SUPPORTED=y
CONFIG_SOC_RTCIO_HOLD_SUPPORTED=y CONFIG_SOC_RTCIO_HOLD_SUPPORTED=y
CONFIG_SOC_RTCIO_WAKE_SUPPORTED=y CONFIG_SOC_RTCIO_WAKE_SUPPORTED=y
CONFIG_SOC_LP_IO_CLOCK_IS_INDEPENDENT=y CONFIG_SOC_LP_IO_CLOCK_IS_INDEPENDENT=y
CONFIG_SOC_SDM_GROUPS=1 CONFIG_SOC_SDM_GROUPS=y
CONFIG_SOC_SDM_CHANNELS_PER_GROUP=8 CONFIG_SOC_SDM_CHANNELS_PER_GROUP=8
CONFIG_SOC_SDM_CLK_SUPPORT_APB=y CONFIG_SOC_SDM_CLK_SUPPORT_APB=y
CONFIG_SOC_SPI_PERIPH_NUM=3 CONFIG_SOC_SPI_PERIPH_NUM=3
@ -370,9 +369,6 @@ CONFIG_SOC_BLE_DEVICE_PRIVACY_SUPPORTED=y
CONFIG_SOC_BLUFI_SUPPORTED=y CONFIG_SOC_BLUFI_SUPPORTED=y
CONFIG_SOC_ULP_HAS_ADC=y CONFIG_SOC_ULP_HAS_ADC=y
CONFIG_SOC_PHY_COMBO_MODULE=y CONFIG_SOC_PHY_COMBO_MODULE=y
CONFIG_SOC_LCDCAM_CAM_SUPPORT_RGB_YUV_CONV=y
CONFIG_SOC_LCDCAM_CAM_PERIPH_NUM=1
CONFIG_SOC_LCDCAM_CAM_DATA_WIDTH_MAX=16
CONFIG_IDF_CMAKE=y CONFIG_IDF_CMAKE=y
CONFIG_IDF_TOOLCHAIN="gcc" CONFIG_IDF_TOOLCHAIN="gcc"
CONFIG_IDF_TOOLCHAIN_GCC=y CONFIG_IDF_TOOLCHAIN_GCC=y
@ -569,6 +565,7 @@ CONFIG_PARTITION_TABLE_MD5=y
# #
CONFIG_OTA_VERSION_URL="https://xiaozhi-dev-web.goods.fun/xiaozhi/ota/" CONFIG_OTA_VERSION_URL="https://xiaozhi-dev-web.goods.fun/xiaozhi/ota/"
CONFIG_DEVICE_STATUS_REPORT_URL="http://192.168.124.8:8000/api/v1/devices/report-status" CONFIG_DEVICE_STATUS_REPORT_URL="http://192.168.124.8:8000/api/v1/devices/report-status"
CONFIG_STORY_API_URL="http://192.168.124.8:8000/api/v1/devices/stories/"
CONFIG_LANGUAGE_ZH_CN=y CONFIG_LANGUAGE_ZH_CN=y
# CONFIG_LANGUAGE_ZH_TW is not set # CONFIG_LANGUAGE_ZH_TW is not set
# CONFIG_LANGUAGE_EN_US is not set # CONFIG_LANGUAGE_EN_US is not set
@ -588,7 +585,7 @@ CONFIG_VOLC_INSTANCE_ID="68f0bc7611a5cf890711f2d0"
CONFIG_VOLC_PRODUCT_KEY="69080ba98219e1f34702d133" CONFIG_VOLC_PRODUCT_KEY="69080ba98219e1f34702d133"
CONFIG_VOLC_PRODUCT_SECRET="205b5c3d43f0d3e877908399" CONFIG_VOLC_PRODUCT_SECRET="205b5c3d43f0d3e877908399"
CONFIG_VOLC_BOT_ID="botCL63FJgWe" CONFIG_VOLC_BOT_ID="botCL63FJgWe"
CONFIG_VOLC_DEVICE_NAME="" CONFIG_VOLC_DEVICE_NAME="d0_cf_13_03_bb_f0"
# CONFIG_BOARD_TYPE_BREAD_COMPACT_WIFI is not set # CONFIG_BOARD_TYPE_BREAD_COMPACT_WIFI is not set
# CONFIG_BOARD_TYPE_BREAD_COMPACT_WIFI_LCD is not set # CONFIG_BOARD_TYPE_BREAD_COMPACT_WIFI_LCD is not set
# CONFIG_BOARD_TYPE_BREAD_COMPACT_ML307 is not set # CONFIG_BOARD_TYPE_BREAD_COMPACT_ML307 is not set
@ -1035,7 +1032,6 @@ CONFIG_BT_BLE_42_FEATURES_SUPPORTED=y
CONFIG_BT_BLE_42_DTM_TEST_EN=y CONFIG_BT_BLE_42_DTM_TEST_EN=y
CONFIG_BT_BLE_42_ADV_EN=y CONFIG_BT_BLE_42_ADV_EN=y
CONFIG_BT_BLE_42_SCAN_EN=y CONFIG_BT_BLE_42_SCAN_EN=y
CONFIG_BT_BLE_VENDOR_HCI_EN=y
# CONFIG_BT_BLE_HIGH_DUTY_ADV_INTERVAL is not set # CONFIG_BT_BLE_HIGH_DUTY_ADV_INTERVAL is not set
# CONFIG_BT_ABORT_WHEN_ALLOCATION_FAILS is not set # CONFIG_BT_ABORT_WHEN_ALLOCATION_FAILS is not set
# end of Bluedroid Options # end of Bluedroid Options
@ -1257,7 +1253,6 @@ CONFIG_ESP_TLS_USE_DS_PERIPHERAL=y
# CONFIG_ESP_TLS_SERVER_MIN_AUTH_MODE_OPTIONAL is not set # CONFIG_ESP_TLS_SERVER_MIN_AUTH_MODE_OPTIONAL is not set
# CONFIG_ESP_TLS_PSK_VERIFICATION is not set # CONFIG_ESP_TLS_PSK_VERIFICATION is not set
# CONFIG_ESP_TLS_INSECURE is not set # CONFIG_ESP_TLS_INSECURE is not set
CONFIG_ESP_TLS_DYN_BUF_STRATEGY_SUPPORTED=y
# end of ESP-TLS # end of ESP-TLS
# #
@ -1285,12 +1280,6 @@ CONFIG_ESP_ERR_TO_NAME_LOOKUP=y
CONFIG_ESP_ALLOW_BSS_SEG_EXTERNAL_MEMORY=y CONFIG_ESP_ALLOW_BSS_SEG_EXTERNAL_MEMORY=y
# end of Common ESP-related # end of Common ESP-related
#
# ESP-Driver:Camera Controller Configurations
#
# CONFIG_CAM_CTLR_DVP_CAM_ISR_CACHE_SAFE is not set
# end of ESP-Driver:Camera Controller Configurations
# #
# ESP-Driver:GPIO Configurations # ESP-Driver:GPIO Configurations
# #
@ -1608,11 +1597,8 @@ CONFIG_ESP_PHY_RF_CAL_PARTIAL=y
# CONFIG_ESP_PHY_RF_CAL_NONE is not set # CONFIG_ESP_PHY_RF_CAL_NONE is not set
# CONFIG_ESP_PHY_RF_CAL_FULL is not set # CONFIG_ESP_PHY_RF_CAL_FULL is not set
CONFIG_ESP_PHY_CALIBRATION_MODE=0 CONFIG_ESP_PHY_CALIBRATION_MODE=0
CONFIG_ESP_PHY_PLL_TRACK_PERIOD_MS=1000
# CONFIG_ESP_PHY_PLL_TRACK_DEBUG is not set # CONFIG_ESP_PHY_PLL_TRACK_DEBUG is not set
# CONFIG_ESP_PHY_RECORD_USED_TIME is not set # CONFIG_ESP_PHY_RECORD_USED_TIME is not set
CONFIG_ESP_PHY_IRAM_OPT=y
# CONFIG_ESP_PHY_DEBUG is not set
# end of PHY # end of PHY
# #
@ -2283,7 +2269,6 @@ CONFIG_MBEDTLS_DYNAMIC_BUFFER=y
# CONFIG_MBEDTLS_X509_TRUSTED_CERT_CALLBACK is not set # CONFIG_MBEDTLS_X509_TRUSTED_CERT_CALLBACK is not set
# CONFIG_MBEDTLS_SSL_CONTEXT_SERIALIZATION is not set # CONFIG_MBEDTLS_SSL_CONTEXT_SERIALIZATION is not set
# CONFIG_MBEDTLS_SSL_KEEP_PEER_CERTIFICATE is not set # CONFIG_MBEDTLS_SSL_KEEP_PEER_CERTIFICATE is not set
# CONFIG_MBEDTLS_SSL_KEYING_MATERIAL_EXPORT is not set
CONFIG_MBEDTLS_PKCS7_C=y CONFIG_MBEDTLS_PKCS7_C=y
# end of mbedTLS v3.x related # end of mbedTLS v3.x related
@ -3314,7 +3299,6 @@ CONFIG_BT_NIMBLE_COEX_PHY_CODED_TX_RX_TLIM_DIS=y
CONFIG_SW_COEXIST_ENABLE=y CONFIG_SW_COEXIST_ENABLE=y
CONFIG_ESP32_WIFI_SW_COEXIST_ENABLE=y CONFIG_ESP32_WIFI_SW_COEXIST_ENABLE=y
CONFIG_ESP_WIFI_SW_COEXIST_ENABLE=y CONFIG_ESP_WIFI_SW_COEXIST_ENABLE=y
# CONFIG_CAM_CTLR_DVP_CAM_ISR_IRAM_SAFE is not set
# CONFIG_MCPWM_ISR_IN_IRAM is not set # CONFIG_MCPWM_ISR_IN_IRAM is not set
# CONFIG_EVENT_LOOP_PROFILING is not set # CONFIG_EVENT_LOOP_PROFILING is not set
CONFIG_POST_EVENTS_FROM_ISR=y CONFIG_POST_EVENTS_FROM_ISR=y