按 GSD 框架 .planning/milestones/digital_human_rtc/phases/phase_06_idle_hibernate/ 规划完成 Phase 6 软退出 RTC 机制。替代旧的"40s 硬重启退出"方案。 ## 核心变更 ### 1. 倒计时刷新(B+C 双源方案) | 方案 | 监听源 | 实施位置 | 状态 | |------|--------|---------|------| | A 扬声器流 | I2S/PCM 输出 | application.cc audio output 3 处 | **宏关闭**(PHASE6_ENABLE_AUDIO_FALLBACK) | | **B 字幕监听** | RTC subtitle 消息 | application.cc:1300 subtitle 分支 | **启用** | | **C 智能体状态** | RTC conv_status 消息 | application.cc:1260 conv_status 分支 | **启用** | 复用现有 DIALOG_IDLE_COUNTDOWN_SECONDS=40 不新增常量。 ### 2. 真退出 RTC 房间(释放 License) - 新增 Protocol 基类虚函数 LeaveRoom(默认回退到 CloseAudioChannel) - VolcRtcProtocol::LeaveRoom 覆写:volc_rtc_stop + volc_rtc_destroy - 火山官方文档明确:真退房必须 leaveRoom + destroyRTCEngine - CloseAudioChannel 只 stop 不够(真人仍在房间继续计费) - 服务端 AI 任务在 180s 内自动清理(火山平台机制) ### 3. EnterIdleHibernate / WakeFromHibernate EnterIdleHibernate 流程(严格顺序): 1. protocol_->LeaveRoom() # 真退房 2. codec->EnableInput/Output(false) # 重置 codec 状态机 3. recorder_pipeline_close() 4. hibernating_.store(true) # 关键:先设标志阻止 PowerSaveTimer 5. esp_pm_configure(light_sleep=false) # 双保险禁用 Light Sleep 6. SetDeviceState(kDeviceStateIdle) 7. idle_cycles_++ + NVS 持久化 8. 字幕"已自动退出RTC对话,按BOOT键重新连接RTC"(5 次重试间隔 200ms) WakeFromHibernate 流程: 1. 检查 idle_cycles_ >= 50 → 硬重启清理碎片(兜底) 2. 清空字幕 3. ToggleChatState → OpenAudioChannel → 自动重建 rtc_handle_ 4. RTC 重新加入房间(实测 2-3s 完成) ### 4. CanEnterSleepMode 加 hibernating 检查 防止 hibernate 期间 PowerSaveTimer 触发 esp_pm_configure(light_sleep=true) 导致 I2C 总线进入低功耗 → 唤醒后 ES7210/ES8311 通信失败 abort。 ### 5. Dialog Watchdog 触发动作改造 旧:esp_restart() 整机重启(黑屏 15-25s + WiFi 重连) 新:Schedule(EnterIdleHibernate) 软退房(不熄屏 + 字幕提示) ### 6. BOOT 唤醒走 WakeFromHibernate 路径 iot_button 回调中检测 IsHibernating(),派发到独立 task 执行 WakeFromHibernate(避免阻塞 esp_timer 任务,CLAUDE.md 经验)。 ### 7. OpenAudioChannel 适配重建 LeaveRoom 销毁 rtc_handle_ 后,OpenAudioChannel 头部检测 NULL 触发 Start() 异步重建,轮询 5s 等待就绪。NVS 缓存 device_secret 所以重建通常 100ms 完成。 ## 实测验证(用户协作) | 阶段 | 时间 | |------|------| | 40s 触发软休眠 | ✅ | | LeaveRoom 真退房 | ✅ "✓ 已真退出 RTC 房间(leaveRoom + destroyRTCEngine)" | | 屏幕保持 + 字幕显示 | ✅ "已自动退出RTC对话,按BOOT键重新连接RTC" | | BOOT 按键唤醒 | ✅ | | RTC 实例重建 | ✅ 100ms | | RTC 重新加入房间 | ✅ 2-3s | | 连续 2 次软休眠+唤醒 | ✅ 无 abort/I2C 失败 | | 时间对比 | 旧硬重启 15-25s → 软休眠 3-5s(省 80%) | ## 6 个关键踩坑修复(详见 HIBERNATE_REPORT.md) 1. codec 状态机未重置 → 唤醒后 I2C abort 2. PowerSaveTimer Light Sleep 干扰 I2C 总线 3. hibernating_ 设置时序错误 4. dynamic_cast 在 -fno-rtti 下编译失败 → 改基类虚函数 5. LeaveRoom 后 OpenAudioChannel 直接失败 → 加重建逻辑 6. 字幕 LVGL 锁竞争 → 推迟到最后 + 5 次重试 ## 文档产出(同时提交) - .planning/.../phase_06_idle_hibernate/PLAN.md(含实施变更记录 V1-V6) - .planning/.../phase_06_idle_hibernate/HIBERNATE_REPORT.md(验证报告) - .planning/.../ROADMAP.md(Phase 1-5 ✅ + Phase 6 进行中状态更新) - docs/Rtc_AIavatar/数字人表情渲染方案_云端预渲染+BLE+OTA.md 新增第 19 章 RTC 空闲倒计时方案选型与软退出(9 小节) - docs/Rtc_AIavatar/RTC软退出方案_移植参考.md 完整移植参考(10 章 + 3 附录,可移植到其他火山 RTC 项目) - docs/Rtc_AIavatar/音频卡顿_全局资源分析.md 全局资源分析 + 13 项优化建议(不改代码)
75 lines
3.0 KiB
C++
75 lines
3.0 KiB
C++
#ifndef _VOLC_RTC_PROTOCOL_H_
|
||
#define _VOLC_RTC_PROTOCOL_H_
|
||
|
||
#include "protocol.h"
|
||
#include "volc_rtc.h"
|
||
#include "base/volc_device_manager.h"
|
||
#include <freertos/FreeRTOS.h>
|
||
#include <freertos/event_groups.h>
|
||
#include <mutex>
|
||
#include <vector>
|
||
|
||
class VolcRtcProtocol : public Protocol {
|
||
public:
|
||
VolcRtcProtocol();
|
||
~VolcRtcProtocol();
|
||
|
||
void Start() override;
|
||
void SendAudio(const std::vector<uint8_t>& data) override;// 🔊 发送音频数据到RTC
|
||
void SendPcm(const std::vector<uint8_t>& data) override;// 🔊 发送PCM音频数据到RTC
|
||
void SendG711A(const std::vector<uint8_t>& data) override;// 🔊 发送G711A音频数据到RTC
|
||
bool OpenAudioChannel() override;// 🔊 打开音频通道
|
||
void CloseAudioChannel() override;// 🔊 关闭音频通道(仅 stop 媒体流,不退出房间)
|
||
|
||
// Phase 6: 真退出 RTC 房间 = volc_rtc_stop + volc_rtc_destroy(释放 License)
|
||
// 与 CloseAudioChannel 区别:CloseAudioChannel 只停媒体流,房间仍占用
|
||
void LeaveRoom() override;
|
||
|
||
bool IsAudioChannelOpened() const override;// 🔊 检查音频通道是否已打开
|
||
void SendAbortSpeaking(AbortReason reason) override;// 🔊 发送中止通话请求
|
||
void SendStartListening(ListeningMode mode) override;// 🔊 发送开始监听请求
|
||
void SendTextMessage(const std::string& text) override;// 🔊 发送文本消息到RTC
|
||
void SendFunctionResult(const std::string& tool_call_id, const std::string& content) override;// 🔊 发送函数调用结果到RTC
|
||
|
||
/**
|
||
* @brief 设置Agent配置参数(如音色、提示词等)
|
||
* @param params JSON格式的配置参数字符串
|
||
*/
|
||
void SetAgentConfig(const std::string& params);
|
||
|
||
private:
|
||
EventGroupHandle_t event_group_handle_;
|
||
volc_rtc_t rtc_handle_ = nullptr;
|
||
std::mutex rtc_mutex_;
|
||
std::string extra_params_; // 存储额外的Agent配置参数
|
||
|
||
bool is_connected_ = false;
|
||
bool is_audio_channel_opened_ = false;
|
||
bool iot_ready_ = false;
|
||
volc_iot_info_t iot_info_ = {};
|
||
size_t opus_bytes_accum_ = 0;
|
||
size_t pcm_bytes_accum_ = 0;
|
||
size_t g711a_bytes_accum_ = 0;
|
||
size_t down_pcm_bytes_accum_ = 0;
|
||
size_t down_opus_bytes_accum_ = 0;
|
||
int opus_frames_accum_ = 0;
|
||
int pcm_frames_accum_ = 0;
|
||
int g711a_frames_accum_ = 0;
|
||
uint64_t uplink_last_log_us_ = 0;
|
||
std::vector<uint8_t> pcm_pending_;
|
||
std::vector<uint8_t> g711a_pending_;
|
||
bool first_downlink_logged_ = false;
|
||
|
||
static void MessageCallback(void* context, volc_msg_t* message);
|
||
static void DataCallback(void* context, const void* data, size_t len, volc_data_info_t* info);
|
||
|
||
void ParseServerMessage(const char* message);
|
||
void ProcessAudioData(const void* data, int size);
|
||
void SendText(const std::string& text) override;
|
||
void LogUplinkStatsMaybe();// 打印上传统计信息
|
||
void SendCtrl(const std::string& json);// 🔊 发送控制指令到RTC
|
||
void SendFunc(const std::string& json);// 🔊 发送函数调用指令到RTC
|
||
};
|
||
|
||
#endif
|