Baji_Rtc_Toy/main/application.h
Rdzleo 6b166f4463 feat: add EAF RTC and badge dual mode
1. 固定 RTC 数字人链路使用 ai_chat_ui_eaf,双模式开启后不再回退 LVGL/GIF 旧 RTC UI。

2. 保留电子吧唧 LVGL/SquareLine UI,只在电子吧唧运行模式启动 LVGL,避免与 EAF 抢同一 LCD flush。

3. 拆分 dzbj_hw_display_init 与 dzbj_display_init,AI/配网只初始化 LCD Touch 硬件,电子吧唧再启动 LVGL UI。

4. 配网模式使用 EAF 最小显示栈显示中文提示,请使用APP 蓝牙配网,不加载数字人资源和动画。

5. 开启 CONFIG_BAJI_BADGE_MODE,形成 RTC 数字人对话与电子吧唧图片显示双模式固件。

6. 电子吧唧图片扫描跳过 Background_360x360.jpg,避免 RTC 数字人背景进入吧唧图片列表。

7. BLE 图传在 BLE 5.0 关闭时跳过 2M PHY API,保持 legacy 1M PHY 兼容配网和图传。

8. sdkconfig.defaults 同步 BLE 内存优化,限制连接数和缓存,保留 GATT 与扫描能力。

9. 移除 ota.cc 编译和 app_update 直接依赖,双模式固件不创建 OTA 检查任务。

10. Ota 接口改为禁用 stub,保留接口兼容但不执行升级和版本检查。

11. Board 上报 JSON 的 OTA label 改为 disabled,避免依赖 OTA 运行分区。

12. partitions.csv 改为 factory 单 app 分区,扩大 app 到 0x900000,并扩大 storage 到 0x6F0000。

13. application 去除 OTA 任务句柄和服务器时间依赖,减少运行时资源占用。

14. system_info 去除 esp_ota_ops 依赖,配合无 OTA 分区配置。

15. 同步最新烧录运行日志,记录本轮双模式与配网测试结果。
2026-06-02 13:16:39 +08:00

243 lines
12 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters

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.

#ifndef _APPLICATION_H_
#define _APPLICATION_H_
#include <freertos/FreeRTOS.h>
#include <freertos/event_groups.h>
#include <freertos/task.h>
#include <esp_timer.h>
#include <string>
#include <mutex>
#include <list>
#include <atomic>
#include <opus_encoder.h>
#include <opus_decoder.h>
#include <opus_resampler.h>
#include "protocol.h"
#include "websocket_protocol.h"
#include "background_task.h"
#include "audio/simple_pipeline.h"
// #include "ble_service.h" // BLE JSON Service 暂不使用
#if CONFIG_USE_WAKE_WORD_DETECT
#include "wake_word_detect.h"
#elif CONFIG_USE_CUSTOM_WAKE_WORD
#include "custom_wake_word.h"
#endif
#if CONFIG_USE_AUDIO_PROCESSOR
#include "audio_processor.h"
#endif
#define SCHEDULE_EVENT (1 << 0)
#define AUDIO_INPUT_READY_EVENT (1 << 1)
#define AUDIO_OUTPUT_READY_EVENT (1 << 2)
// 未知状态、启动中、WiFi配网模式、空闲待命、连接服务器、语音监听中、语音播报中、固件升级中、设备激活中、致命错误
enum DeviceState {
kDeviceStateUnknown,
kDeviceStateStarting,
kDeviceStateWifiConfiguring,
kDeviceStateIdle,
kDeviceStateConnecting,
kDeviceStateListening,
kDeviceStateSpeaking,
kDeviceStateDialog,
kDeviceStateUpgrading,
kDeviceStateActivating,
kDeviceStateFatalError
};
// OPUS音频帧时长60ms
#define OPUS_FRAME_DURATION_MS 60
// 应用程序主类(单例模式)
class Application {
public:
static Application& GetInstance() {
static Application instance;
return instance;
}
// 删除拷贝构造函数和赋值运算符
Application(const Application&) = delete;
Application& operator=(const Application&) = delete;
void Start(); // 启动应用程序
DeviceState GetDeviceState() const { return device_state_; } // 获取当前状态
bool IsVoiceDetected() const { return voice_detected_; } // 语音检测状态
void Schedule(std::function<void()> callback); // 任务调度
void SetDeviceState(DeviceState state); // 状态变更
void Alert(const char* status, const char* message, const char* emotion = "", const std::string_view& sound = "");// 警报管理 状态、消息、情感、声音
void DismissAlert();// 关闭警报
void AbortSpeaking(AbortReason reason);// 打断语音播报
void AbortHttpsPlayback(const char* reason);// 中止HTTPS音频播放并清空DMA
void HttpsPlaybackFromUrl(const std::string& url); // 通过HTTPS下载JSON并播放音频故事/歌曲等)
void SendStoryRequest(); // 通过HTTPS故事API请求并播放故事
void SendMusicRequest(); // 通过HTTPS音乐API请求并播放音乐
void ToggleChatState();// 切换聊天状态
void ToggleListeningState();// 切换监听状态
void StartListening();// 开始监听
void StopListening();// 停止监听
void SendTextMessage(const std::string& text);// 发送文本消息
void UpdateIotStates();// 更新IOT设备状态
void Reboot();// 系统重启
void WakeWordInvoke(const std::string& wake_word);// 唤醒词回调
void PlaySound(const std::string_view& sound);// 播放声音
void WaitForAudioPlayback();// 等待音频播报完成
bool IsAudioQueueEmpty(); // 检查音频队列是否为空
void ClearAudioQueue(); // 清空音频播放队列
bool CanEnterSleepMode();// 检查是否可以进入睡眠模式
void StopAudioProcessor();// 停止音频处理器
void ResetDecoder();// 重置解码器状态(用于修复音频播放问题)
bool IsSafeToOperate(); // 🔧 检查当前是否可以安全执行操作
void AbortSpeakingAndReturnToIdle(); // 🔴 专门处理从说话状态到空闲状态的切换
void AbortSpeakingAndReturnToListening(); // 🔵 专门处理从说话状态到聆听状态的切换
void PauseAudioPlayback(); // ⏸️ 暂停音频播放
void ResumeAudioPlayback(); // ▶️ 恢复音频播放
void SuppressNextIdleSound(); // 🔇 抑制下一个空闲状态的声音播放
void SetLowBatteryTransition(bool value);
bool IsLowBatteryTransition() const;
void InitializeWebsocketProtocol(); // 🌐 初始化WebSocket协议RTC连接成功后调用
// void SendTextViaWebsocket(const std::string& text);// 🌐 通过WebSocket发送文本消息
// 姿态传感器接口
bool IsImuSensorAvailable(); // 检查IMU传感器是否可用
bool GetImuData(float* acc_x, float* acc_y, float* acc_z,
float* gyro_x, float* gyro_y, float* gyro_z,
float* temperature); // 获取IMU传感器数据
void OnMotionDetected(); // 运动检测事件处理
bool IsAudioPaused() const { return audio_paused_; } // 检查音频是否暂停
bool ShouldSkipDialogIdleSession() const { return skip_dialog_idle_session_; }// 是否跳过对话待机会话
void ClearDialogIdleSkipSession();// 清除对话待机会话标志位
bool IsDialogUploadEnabled() const { return dialog_upload_enabled_; }// 是否启用对话上传
void SetDialogUploadEnabled(bool enabled);// 设置对话上传状态
// Phase 6: 空闲休眠相关
void EnterIdleHibernate(); // 进入空闲休眠(字幕+真退房+熄屏)
void WakeFromHibernate(); // 从休眠唤醒(亮屏+重连)
bool IsHibernating() const { return hibernating_.load(); }
// // BLE JSON 命令处理(暂不使用)
// void HandleBleJsonCommand(const std::string& cmd, int msg_id, cJSON* data, BleJsonService& service);
private:
Application();// 构造函数
~Application();// 析构函数
// 配置使用唤醒词检测
#if CONFIG_USE_WAKE_WORD_DETECT
WakeWordDetect wake_word_detect_;
#elif CONFIG_USE_CUSTOM_WAKE_WORD
CustomWakeWord wake_word_detect_;
#endif
// 音频处理器
#if CONFIG_USE_AUDIO_PROCESSOR
AudioProcessor audio_processor_;
#endif
std::mutex mutex_;
std::list<std::function<void()>> main_tasks_;
std::unique_ptr<Protocol> protocol_;
std::unique_ptr<WebsocketProtocol> websocket_protocol_; // 🌐 WebSocket协议实例RTC连接后初始化
EventGroupHandle_t event_group_ = nullptr;
esp_timer_handle_t clock_timer_handle_ = nullptr;
volatile DeviceState device_state_ = kDeviceStateUnknown;
std::atomic<bool> is_aborting_{false}; // 🔧 原子标志:防止重复中止操作
std::atomic<std::chrono::steady_clock::time_point> last_safe_operation_; // 🔧 最后安全操作时间戳
std::atomic<bool> is_switching_to_listening_{false}; // 🔵 标志:正在主动切换到聆听状态
std::atomic<bool> is_low_battery_transition_{false};
// Phase 6: 空闲休眠状态
std::atomic<bool> hibernating_{false}; // 是否处于熄屏休眠状态
int idle_cycles_ = 0; // 累计休眠循环次数NVS 持久化)
static constexpr int IDLE_CYCLES_REBOOT_THRESHOLD = 50; // 累计 50 次触发硬重启清碎片
void SaveIdleCyclesToNvs();
void LoadIdleCyclesFromNvs();
void ResetIdleCyclesNvs();
ListeningMode listening_mode_ = kListeningModeAutoStop;
#if CONFIG_USE_REALTIME_CHAT
bool realtime_chat_enabled_ = true;
#else
bool realtime_chat_enabled_ = false;
#endif
std::atomic<bool> ws_downlink_enabled_{true};// 🌐 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音频播放中止标志
std::atomic<int> post_abort_debug_frames_{0};// HTTPS中止后诊断日志计数追踪前N帧音频
int audio_channel_retry_count_ = 0;// RTC 偶发连接失败重试计数 (方案 B: 失败 3 次后销毁 + 重建 engine)
bool aborted_ = false;
bool voice_detected_ = false;
bool audio_paused_ = false; // 音频暂停状态标志
float current_speaker_volume_ = 0.0f; // 当前扬声器音量,用于语音打断判断
bool provisioning_mode_ = false; // 配网模式标志缓存避免重复读NVS
bool first_idle_location_checked_ = false;// 是否首次查询城市天气
bool send_pcm_uplink_ = true; // 是否发送PCM音频数据到服务器由SDK内部转码为G711A
bool send_g711a_uplink_ = false;// 是否直接发送G711A音频数据到服务器
std::chrono::time_point<std::chrono::steady_clock> last_audio_input_time_;
std::chrono::time_point<std::chrono::steady_clock> last_audible_output_time_; // 最后一次有声音输出的时间点
bool skip_dialog_idle_session_; // 是否跳过对话待机会话标志
bool dialog_upload_enabled_ = true; // 对话上传状态标志
bool dialog_watchdog_running_; // 对话看门狗运行标志
int dialog_watchdog_last_logged_; // 对话看门狗上次记录的日志时间
TaskHandle_t dialog_watchdog_task_handle_; // 对话看门狗任务句柄
int clock_ticks_;
TaskHandle_t main_loop_task_handle_;
// Audio encode / decode
TaskHandle_t audio_loop_task_handle_;
BackgroundTask* background_task_;
std::chrono::steady_clock::time_point last_output_time_;
std::list<std::vector<uint8_t>> audio_decode_queue_;
std::unique_ptr<OpusEncoderWrapper> opus_encoder_;
std::unique_ptr<OpusDecoderWrapper> opus_decoder_;
OpusResampler input_resampler_;// 输入音频采样器
OpusResampler reference_resampler_;// 参考音频采样器
OpusResampler output_resampler_;// 输出音频采样器
OpusResampler uplink_resampler_;// 上传音频采样器
player_pipeline_handle_t player_pipeline_ = nullptr;
recorder_pipeline_handle_t recorder_pipeline_ = nullptr;
// 路径 D'' AEC: esp_aec.h 底层同步 API + 软件 loopback ref
// codec 保持 baseline 1ch 16-bit (MIC1|MIC2 ES7210 内部混合 mono)
// DAC 输出 PCM 同步复制到 ref_ring_buf, ReadAudio 调 aec_process(mic, delayed_ref) → clean
// ⚠️ portMUX (spinlock) 会禁用本核中断, 与 WiFi 协议栈 pm_coex_set_reconnect_policy 冲突
// 实测引发 IllegalInstruction panic。改用 FreeRTOS mutex (不禁中断, 仅 task 间互斥)
void *aec_handle_ = nullptr; // aec_handle_t* (避免暴露 esp_aec.h 类型)
int aec_chunk_size_ = 0; // aec_get_chunksize 返回 (16k 通常 256 samples = 16ms)
int16_t *ref_ring_buf_ = nullptr; // PSRAM 上分配 ~200ms ref ring buffer
int ref_ring_capacity_ = 0;
int ref_ring_write_idx_ = 0;
int ref_ring_filled_ = 0;
int aec_ref_delay_samples_ = 800; // 延迟补偿 samples (默认 50ms @16kHz, 调优范围 30-80ms)
SemaphoreHandle_t ref_ring_mutex_ = nullptr;
void InitAec();
void DeinitAec();
void AppendRefSamples(const int16_t *pcm, int samples); // OnAudioOutput 调用, DAC PCM 推入 ring buffer
void GetDelayedRef(int16_t *ref_out, int samples); // ApplyAEC 内部使用, 取延迟后 ref
void ApplyAEC(std::vector<int16_t>& mic_inout); // ReadAudio 调用, in-place 处理 mic → clean
void MainLoop();// 主事件循环
void OnAudioInput();// 音频输入回调
void OnAudioOutput();// 音频输出回调
void ReadAudio(std::vector<int16_t>& data, int sample_rate, int samples);// 读取音频数据
void SetDecodeSampleRate(int sample_rate, int frame_duration);// 设置解码采样率
void CheckNewVersion();// 检查新固件版本
void ShowActivationCode();// 显示激活码
void OnClockTimer();// 时钟定时器回调
void SetListeningMode(ListeningMode mode);// 设置监听模式
void AudioLoop();// 音频处理循环
bool suppress_next_idle_sound_ = false;// 标志:是否抑制下一个空闲状态的声音播放
void StartDialogWatchdog();// 启动对话看门狗
void StopDialogWatchdog(); // 停止对话看门狗
void HttpsApiPlayback(const char* api_url_base, const char* tag, const char* task_name); // HTTPS API音频播放通用实现
const char* DeviceStateToString(DeviceState state); // 状态枚举转字符串
};
#endif // _APPLICATION_H_