按 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 项优化建议(不改代码)
101 lines
3.7 KiB
C++
101 lines
3.7 KiB
C++
#ifndef PROTOCOL_H
|
||
#define PROTOCOL_H
|
||
|
||
#include <cJSON.h>
|
||
#include <string>
|
||
#include <functional>
|
||
#include <chrono>
|
||
|
||
struct BinaryProtocol3 {
|
||
uint8_t type;
|
||
uint8_t reserved;
|
||
uint16_t payload_size;
|
||
uint8_t payload[];
|
||
} __attribute__((packed));
|
||
|
||
enum AbortReason {
|
||
kAbortReasonNone,
|
||
kAbortReasonWakeWordDetected,
|
||
kAbortReasonVoiceInterrupt
|
||
//kAbortReasonNewStory // websocket推送新故事时中断当前播放
|
||
};
|
||
|
||
enum ListeningMode {
|
||
kListeningModeAutoStop,
|
||
kListeningModeManualStop,
|
||
kListeningModeRealtime // 需要 AEC 支持
|
||
};
|
||
|
||
class Protocol {
|
||
public:
|
||
virtual ~Protocol() = default;
|
||
|
||
inline int server_sample_rate() const {
|
||
return server_sample_rate_;
|
||
}
|
||
inline int server_frame_duration() const {
|
||
return server_frame_duration_;
|
||
}
|
||
inline bool downlink_is_pcm() const {
|
||
return downlink_is_pcm_;
|
||
}
|
||
inline const std::string& session_id() const {
|
||
return session_id_;
|
||
}
|
||
inline void SetSuppressIncomingMessageLog(bool v) { suppress_incoming_message_log_ = v; }
|
||
|
||
void OnIncomingAudio(std::function<void(std::vector<uint8_t>&& data)> callback);
|
||
void OnIncomingJson(std::function<void(const cJSON* root)> callback);
|
||
void OnAudioChannelOpened(std::function<void()> callback);
|
||
void OnAudioChannelClosed(std::function<void()> callback);
|
||
void OnNetworkError(std::function<void(const std::string& message)> callback);
|
||
void OnBotMessage(std::function<void()> callback);
|
||
|
||
virtual void Start() = 0;
|
||
virtual bool OpenAudioChannel() = 0;
|
||
virtual void CloseAudioChannel() = 0;
|
||
// Phase 6: 真退出 RTC 房间(释放 License),默认回退到 CloseAudioChannel
|
||
// VolcRtcProtocol 覆写:调用 volc_rtc_stop + volc_rtc_destroy
|
||
virtual void LeaveRoom() { CloseAudioChannel(); }
|
||
virtual bool IsAudioChannelOpened() const = 0;
|
||
virtual void SendAudio(const std::vector<uint8_t>& data) = 0;
|
||
virtual void SendPcm(const std::vector<uint8_t>& data) {}
|
||
virtual void SendG711A(const std::vector<uint8_t>& data) {}
|
||
virtual void SendWakeWordDetected(const std::string& wake_word);
|
||
virtual void SendStartListening(ListeningMode mode);
|
||
virtual void SendStopListening();
|
||
virtual void SendAbortSpeaking(AbortReason reason);
|
||
virtual void SendTextMessage(const std::string& text);
|
||
virtual void SendStoryRequest(); // 声明 发送讲故事请求 【新增】
|
||
virtual void SendIotDescriptors(const std::string& descriptors);
|
||
virtual void SendIotStates(const std::string& states);
|
||
virtual void SendFunctionResult(const std::string& tool_call_id, const std::string& content) {
|
||
(void)tool_call_id;
|
||
SendTextMessage(content);
|
||
}
|
||
|
||
protected:
|
||
std::function<void(const cJSON* root)> on_incoming_json_;
|
||
std::function<void(std::vector<uint8_t>&& data)> on_incoming_audio_;
|
||
std::function<void()> on_audio_channel_opened_;
|
||
std::function<void()> on_audio_channel_closed_;
|
||
std::function<void(const std::string& message)> on_network_error_;
|
||
std::function<void()> on_bot_message_;
|
||
|
||
int server_sample_rate_ = 24000;
|
||
int server_frame_duration_ = 60;
|
||
bool downlink_is_pcm_ = false;// 是否是PCM格式
|
||
bool error_occurred_ = false;
|
||
std::string session_id_;
|
||
bool start_listening_pending_ = false;// 是否有待处理的监听请求
|
||
ListeningMode pending_listening_mode_ = kListeningModeRealtime;// 待处理的监听模式
|
||
std::chrono::time_point<std::chrono::steady_clock> last_incoming_time_;
|
||
bool suppress_incoming_message_log_ = false;
|
||
|
||
virtual void SendText(const std::string& text) = 0;
|
||
virtual void SetError(const std::string& message);
|
||
virtual bool IsTimeout() const;
|
||
};
|
||
|
||
#endif // PROTOCOL_H
|