实机验证通过后,按 Kapi 无屏底座路线补齐 Pendant RTC 吊坠项目的迁移修复。 1. BLE 配网与资源隔离 - sdkconfig.defaults 开启 BT 优先 PSRAM 分配,并将 LWIP socket 上限提升到 20 - sdkconfig.defaults.esp32s3 允许 BSS/NOINIT 放入 PSRAM,释放内部 SRAM 给 BLE/WiFi/RTC - 配网模式 codec 使用 StartOutputOnly(),跳过麦克风 RX DMA 和 ES7210 输入链路 - ResetWifiConfiguration() 改为独立 wifi_reset task,避免在 iot_button/esp_timer 回调中阻塞延时 - WifiBoard 增加 IsWifiConfigMode(),供启动阶段判断是否走配网资源隔离路径 2. 音频底噪与 DMA 残留修复 - AudioCodec 增加 StartOutputOnly(),支持仅启动扬声器输出 - RTC 音频通道打开后灌入 200ms silence PCM,覆盖 I2S DMA 残留数据 - 软退出进入待命前重启 codec output 并再次灌静音,减少待命音/欢迎语前杂音 - box_audio_codec 在无硬件回采时使用 channel_mask=0,避免 I2S slot mask 被错误污染 3. 软件 loopback AEC - 引入 esp_aec 底层同步 API,使用 DAC 输出复制构建 ref ring - 上行 mic PCM 与延迟 ref 做同步消回声,适配无屏无硬件回采的 Pendant 形态 - AEC 采用 lazy init,减少启动阶段对 WiFi/BLE 内部 SRAM 的压力 - ref 静音时直接 passthrough,避免 AI 静音后误压制用户语音 - 在 player_pipeline_write 和 codec->OutputData 两条下行路径都追加 ref hook 4. RTC 连接稳定性与软退出 - VolcRtcProtocol 增加 LeaveRoom(bool notify_closed),支持 stop 房间但保留 rtc_handle - hibernate 路径使用 LeaveRoom(false),避免关闭回调顺手关掉 codec output 导致待命音无声 - LeaveRoom/ForceRebuildEngine 重置 downlink_is_pcm_ 和首包标志,避免本地 Opus 音效被当 PCM 播成杂音 - OpenAudioChannel 连续失败 3 次后 ForceRebuildEngine,清理 RTC SDK 内部异常状态 - 加入 DIAG-RTC socket/heap/PSRAM/RSSI 日志,便于定位 ICE socket 和内存问题 5. Dialog watchdog 与 BOOT 唤醒 - Dialog watchdog 到期不再写 reboot_dlg_idle 后 esp_restart - 新增 EnterIdleHibernate():软退房、清空残留音频队列、关闭麦克风、播放待命音后静默 - 新增 WakeFromHibernate():BOOT 唤醒后复用 RTC engine 并通过 ToggleChatState() 重连 RTC - BOOT 单击优先判断 IsHibernating(),异步唤醒,避免走普通按键状态机 - hibernate 期间禁止 PowerSaveTimer 进入 Light Sleep,保护 I2C/codec 总线 6. 文档与衍生项目沉淀 - 更新石头光源属性检测方案文档 - 将 Pendant 实测通过的软退出、AEC、BLE 配网隔离经验同步到衍生项目移植规则
100 lines
3.7 KiB
C++
100 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;
|
||
// RTC 软退出:默认兼容旧协议,RTC 协议可重写为仅退房不销毁 engine。
|
||
virtual void LeaveRoom(bool notify_closed = true) { (void)notify_closed; 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
|