Rdzleo 3dc6cadf49 diag(rtc-only): Phase 8 - 音频卡顿根因诊断埋点 + 数据采集报告
数字人 RTC 模式音频卡顿根因定位。通过 4 类 ESP_LOGW 埋点采集运行时
数据,对照表格判定根因,输出 Phase 9 实施分支决策。

埋点实现(main/application.cc,PHASE8_DIAG_ENABLE 宏开关,关闭后零开销):
- DIAG-1 queue 深度:3 处(出队 + WebSocket 入队 + RTC 入队),50ms 节流
- DIAG-2 codec->OutputData 写入耗时:>15ms 阈值告警
- DIAG-3 WiFi RSSI:OnClockTimer 1Hz
- DIAG-4 heap 快照 + 碎片率:OnClockTimer 1Hz

实测结论(见 DIAG_REPORT.md):用户感知卡顿 = 两个独立根因
- A. 开机播报阶段 ③' codec init 时序缺陷(ES7210 I2C 失败 +
  126 次 write_slow 集中在 2-13s)
- B. RTC 对话阶段 ⑤ Opus/WebSocket 应用层帧到达抖动
  (queue 突发堆积 19 + queue=0 出现 58 次,但 codec 写入 0 次 slow)

完全排除:① CPU 争抢、② PSRAM 带宽、④ WiFi 丢包(RSSI -24~-33dBm
极强)、⑥ 内存碎片(heap 全程稳定)

Phase 9 推荐分支 B'(双线修复,原 A/C 的 EAF 方案不适用):
- 9.1 应用层 jitter buffer(fill-threshold + drain)—— 解 B
- 9.2 开机 codec init 时序修复(ES7210 reset + ready 等待)—— 解 A
- 估时 1 天

ROADMAP 同步:Phase 7 矫正为 battery_psm(实际状态)、Phase 8 新增
诊断、Phase 9 占位待 Phase 8 决策、原"集成测试"挪到 Phase 10。
新增 .planning/STATE.md 记录 roadmap evolution。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 11:40:42 +08:00

19 KiB
Raw Blame History

ROADMAP — 数字人 RTC 项目

10 个阶段,按依赖关系串行。每个阶段产生原子 commit可独立 revert。

阶段总览

Phase 1 (Kconfig 屏蔽吧唧)  ──┐
                              ├─→ Phase 3 (GIF 资源准备) ──┐
Phase 2 (分区表调整)        ──┘                            │
                                                           ├─→ Phase 4 (情绪→GIF 映射)
                                                           │
                                                           └─→ Phase 5 (字幕恢复)
                                                                       │
                                                            Phase 6 (RTC 空闲超时联动)
                                                                       │
                                                            Phase 7 (电量保护 + 低功耗重构)
                                                                       │
                                                            Phase 8 (音频卡顿根因诊断)
                                                                       │
                                                            Phase 9 (音频卡顿实施优化 - 待定)
                                                                       │
                                                            Phase 10 (集成测试 + 推送)

Phase 1: Kconfig 屏蔽电子吧唧模式 ⚠️ 结构性变更(条件编译,不删源码)

目标:通过 Kconfig 开关 + CMakeLists 条件编译 + 调用点 #ifdef 保护,让吧唧模式代码不进固件但保留在仓库中。Rtc_AIavatar 分支默认 CONFIG_BAJI_BADGE_MODE=nmain 分支默认 =y 保持双模式可恢复。

1.1 新增 Kconfig 开关

修改 main/Kconfig.projbuild,新增:

menu "Baji RTC Toy Configuration"

config BAJI_BADGE_MODE
    bool "Enable electronic badge mode (双模式电子吧唧)"
    default n
    help
        启用电子吧唧模式图片浏览、APP传图、设备间分享、KEY2按键等。
        关闭后仅保留 AI 对话 + 数字人 RTC 功能,节省固件体积。
        源代码不会被删除,可随时重新启用。

endmenu

并在 Rtc_AIavatar 分支的 sdkconfig.defaults 中追加:

CONFIG_BAJI_BADGE_MODE=n

1.2 CMakeLists.txt 条件化

修改 main/CMakeLists.txt,将吧唧专属 srcs 包裹在条件块中:

# AI 对话 + 数字人 RTC 核心(始终编译)
set(srcs
    "main.cc"
    "application.cc"
    "ota.cc"
    "bluetooth_provisioning.cc"
    # ... RTC 协议、Opus、I2S、LCD、字幕等
    "dzbj/lcd.c"
    "dzbj/ai_chat_ui.c"
    "dzbj/bg_gif_demo.c"
    "dzbj/dual_gif_demo.c"
    "dzbj/sprite_demo.c"
)

# 电子吧唧模式专属(条件编译)
if(CONFIG_BAJI_BADGE_MODE)
    list(APPEND srcs
        "dzbj/device_mode.c"
        "dzbj/dzbj_ble.c"
        "dzbj/ble_transfer.c"
        "dzbj/dzbj_button.c"        # KEY2 部分BOOT 单键回调在公共模块
        "dzbj/pages.c"
        "dzbj/fatfs.c"
        "dzbj/pages_pwm.c"
        "dzbj/dzbj_battery.c"
        "dzbj/dzbj_init.c"
        "dzbj/sleep_mgr.c"
        # UI Screens
        "ui/screens/ui_ScreenHome.c"
        "ui/screens/ui_ScreenImg.c"
        "ui/screens/ui_ScreenSet.c"
        "ui/screens/ui_ScreenPeiwang.c"
        "ui/screens/ui_ScreenImageShar.c"
        "ui/screens/ui_ScreenImageReception.c"
        "ui/screens/ui_ScreenSharing.c"
        "ui/screens/ui_ScreenReceiving.c"
        "ui/screens/ui_ScreenUpdate.c"
    )
endif()

1.3 调用点 #ifdef 保护

在所有引用吧唧符号的位置加保护,源代码不删除

  • main/application.cc

    #ifdef CONFIG_BAJI_BADGE_MODE
    #include "dzbj/device_mode.h"
    #endif
    
    void Application::Start() {
        // ... 公共代码 ...
    #ifdef CONFIG_BAJI_BADGE_MODE
        if (device_mode_get() == MODE_BADGE) {
            InitBadgeMode();
            return;
        }
    #endif
        InitAiMode();
    }
    
  • main/boards/movecall-moji-esp32s3/movecall_moji_esp32s3.cc:所有 dzbj header include、初始化、BOOT+KEY2 组合键回调全部用 #ifdef CONFIG_BAJI_BADGE_MODE 包裹

  • main/dzbj/dzbj_button.cBOOT 按键回调代码本身保留可编译公共功能KEY2 处理代码块用 #ifdef CONFIG_BAJI_BADGE_MODE 包裹

  • main/dzbj/ai_chat_ui.c:清理对吧唧界面的跳转(用 #ifdef 保护,不删代码)

1.4 头文件 stub 处理

对于条件编译后未链接的吧唧模块,其他保留模块若有引用:

  • 头文件本身仍存在(包含 prototype
  • 若调用点未用 #ifdef 保护就会链接报错
  • 解决:在调用点全部加 #ifdef(首选);或在 .h 内提供 stub 实现(次选)

1.5 任务清单

  1. 修改 main/Kconfig.projbuild 新增 CONFIG_BAJI_BADGE_MODE 开关
  2. 修改 main/CMakeLists.txt 把吧唧 srcs 包裹在 if(CONFIG_BAJI_BADGE_MODE)
  3. 修改 main/application.cc#ifdef 保护
  4. 修改 main/boards/movecall-moji-esp32s3/movecall_moji_esp32s3.cc#ifdef 保护
  5. 修改 main/dzbj/ai_chat_ui.c 跳转点加 #ifdef 保护
  6. 修改 main/dzbj/dzbj_button.c KEY2 代码块加 #ifdef 保护
  7. 修改 main/dzbj/sleep_mgr.c 整体用 #ifdef CONFIG_BAJI_BADGE_MODE 包裹Phase 6 改造为 RTC 联动版)
  8. 修改 sdkconfig.defaults(或 sdkconfig.ci)确保 Rtc_AIavatar 默认 CONFIG_BAJI_BADGE_MODE=n
  9. 产出 .planning/milestones/digital_human_rtc/BADGE_MODE_ISOLATION_MAP.md,列出所有 #ifdef 边界位置

完成标志

  • CONFIG_BAJI_BADGE_MODE=nidf.py build 编译通过
  • CONFIG_BAJI_BADGE_MODE=yidf.py build 也编译通过(G7 验收,可恢复双模式
  • 烧录 CONFIG_BAJI_BADGE_MODE=n 版本:开机直接进入 AI 对话界面(无模式选择)
  • main/dzbj/ 下所有源文件仍然存在(未删除)
  • BADGE_MODE_ISOLATION_MAP.md 已生成

风险点

  • C++ 类成员函数无法用 #ifdef 完全屏蔽(如 Application 类的吧唧成员变量),需要把成员变量也用 #ifdef 包裹
  • 头文件相互 include 可能导致循环 #ifdef,必要时改用 forward declaration

产出 commitfeat(kconfig): 引入 CONFIG_BAJI_BADGE_MODE 开关 - 吧唧模式可条件编译屏蔽


Phase 2: 分区表调整

目标:扩容 SPIFFS 到 6MB 装下 3 个 GIF + 背景图。

任务

  1. 修改 partitions.csv
# Name,   Type, SubType,  Offset,   Size,    Flags
nvs,      data, nvs,      0x9000,   0x4000
otadata,  data, ota,      0xD000,   0x2000
phy_init, data, phy,      0xF000,   0x1000
ota_0,    app,  ota_0,    0x10000,  0x400000   # 4MB从 6.5MB 缩)
ota_1,    app,  ota_1,    0x410000, 0x400000   # 4MB
storage,  data, spiffs,   0x810000, 0x600000   # 6MB从 2.875MB 扩)
                                    # 删除 64KB model 分区(暂未用)
  1. 验证:idf.py partition-table 总和 = 16MB 减去引导区
  2. 烧录后:heap_caps_get_free_size(MALLOC_CAP_SPIRAM) 不变SPIFFS 显示 6MB

完成标志

  • idf.py partition-table 总和不超 Flash 大小
  • 编译后固件 .bin < 4MB确认应用分区够装
  • 烧录后 esp_spiffs_info("storage", &total, &used) 返回 total ≈ 6MB

产出 commitchore(partitions): app 双 OTA 4MB + SPIFFS 6MB为数字人 GIF 扩容)


Phase 3: GIF 资源准备

目标:准备 m03/m06/m07 三个 hiyori GIFgifsicle 处理 + 居中裁剪。

任务

  1. 源 GIF 位置:docs/Rtc_AIavatar/Resources/hiyori_free_zh/Export/m{03,06,07}/hiyori_m{03,06,07}.gif
  2. 用 PIL 遍历所有帧找 bbox吸取 PoC 经验):
# tools/sprite_poc/prepare_hiyori_3gifs.py新写
# 对每个 GIF
# 1. PIL ImageSequence.Iterator 找全帧 bbox
# 2. 计算裁剪框(含全部动作幅度,宽 240高从脚底向上 320
# 3. gifsicle --crop X,Y+WxH --resize 240x320 -O3 --colors 256
# 4. 输出到 spiffs_image/hiyori_m{03,06,07}.gif
  1. 验证每个 GIF
    • 文件大小(合计 ≤ 5.5MB 留余量)
    • gifsicle --info 检查每帧有 transparent 索引
    • 设备烧录单独测试每个 GIF临时改 bg_gif_demo_start 参数)

完成标志

  • spiffs_image/hiyori_m{03,06,07}.gif 三个文件存在
  • 每个文件 < 2MB三个合计 < 5.5MB
  • 设备烧录后三个 GIF 都能透明显示,无锯齿

产出 commitfeat(assets): 准备 hiyori 三表情 GIFm03/m06/m07+ Python 处理脚本


Phase 4: 情绪 → GIF 映射

目标22 种情绪标签 → 3 个 GIF 的映射表RTC 字幕情绪自动切换 GIF。

任务

  1. main/dzbj/ai_chat_ui.c 设计映射表:
typedef struct {
    const char *emotion;   // RTC 协议情绪标签
    const char *gif_path;  // SPIFFS GIF 路径
} emotion_gif_map_t;

static const emotion_gif_map_t emotion_gif_table[] = {
    // 默认/积极 → m06 轻松
    {"neutral",   "/spiflash/hiyori_m06.gif"},
    {"happy",     "/spiflash/hiyori_m06.gif"},
    {"laughing",  "/spiflash/hiyori_m06.gif"},
    {"funny",     "/spiflash/hiyori_m06.gif"},
    {"cool",      "/spiflash/hiyori_m06.gif"},
    {"loving",    "/spiflash/hiyori_m06.gif"},
    {"relaxed",   "/spiflash/hiyori_m06.gif"},
    {"delicious", "/spiflash/hiyori_m06.gif"},
    {"silly",     "/spiflash/hiyori_m06.gif"},
    {"winking",   "/spiflash/hiyori_m06.gif"},
    {"kissy",     "/spiflash/hiyori_m06.gif"},
    {"confident", "/spiflash/hiyori_m06.gif"},

    // 思考/疲倦 → m07 睡眠
    {"sleepy",    "/spiflash/hiyori_m07.gif"},
    {"thinking",  "/spiflash/hiyori_m07.gif"},
    {"confused",  "/spiflash/hiyori_m07.gif"},
    {"embarrassed","/spiflash/hiyori_m07.gif"},

    // 负面/严肃 → m03 中等
    {"sad",       "/spiflash/hiyori_m03.gif"},
    {"crying",    "/spiflash/hiyori_m03.gif"},
    {"angry",     "/spiflash/hiyori_m03.gif"},
    {"surprised", "/spiflash/hiyori_m03.gif"},
    {"shocked",   "/spiflash/hiyori_m03.gif"},
    {"serious",   "/spiflash/hiyori_m03.gif"},
};
  1. 实现 ai_chat_set_emotion(const char *emotion)
    • 查表 → 调用 bg_gif_demo_switch_gif(path)
    • 静态变量 last_gif_path 去重避免重复加载
  2. bg_gif_demo.cswitch_gif() 接口:
    • 释放旧 GIF PSRAM
    • 加载新 GIF
    • lv_gif_set_src(g_gif_obj, &g_gif_dsc)
    • 重新设置定时器周期 20ms避免恢复默认 10ms
  3. application.cc / volc_rtc_protocol.cc 字幕回调中调用 ai_chat_set_emotion()
  4. 字幕到达时立即触发(不等 is_finallast_subtitle_emotion 去重

完成标志

  • AI 回复"happy你好"时 GIF 切到 m06
  • AI 回复"sad抱歉"时 GIF 切到 m03
  • 切换间无内存泄漏(连续切 50 次 PSRAM 不持续减少)

产出 commitfeat(emotion): 情绪标签 → hiyori GIF 映射 + bg_gif_demo 切换接口


Phase 5: RTC 字幕恢复

目标:屏幕底部半透明字幕显示,不遮挡数字人。

任务

  1. 修改 main/dzbj/ai_chat_ui.c
    • 第 165 行删除 lv_obj_add_flag(chat_label, LV_OBJ_FLAG_HIDDEN)
    • 第 342 行删除 if (USE_BG_GIF_POC) return
    • 调整 chat_label 创建参数:
      • lv_obj_align(chat_label, LV_ALIGN_BOTTOM_MID, 0, -70)
      • 宽度 300pxwrap 模式
      • 字体 font_puhui_20_4,颜色 0xFFFFFF白色背景半透明更显眼
      • 父容器:半透明黑色 boxlv_obj_set_style_bg_opa(LV_OPA_50)rounded cornerpadding 10px
  2. 创建顺序确保层级:
    • lv_img_create(scr) 背景图(最底层)
    • lv_gif_create(scr) 数字人 GIF
    • lv_obj_create(scr) 字幕容器(最上层)
  3. 字幕长文本自动换行 + 滚动(>3 行截断)

完成标志

  • AI 回复时字幕实时显示在屏幕底部,半透明背景
  • 字幕不遮挡数字人头部
  • 长文本超过 3 行时合理截断或滚动

产出 commitfeat(subtitle): RTC 字幕恢复 - 屏幕底部半透明,避让数字人


Phase 6: RTC 空闲超时联动

目标60s 无对话 → 自动断 RTC + 熄屏;旧 sleep_mgr 代码用 #ifdef CONFIG_BAJI_BADGE_MODE 保留可恢复。

任务

  1. main/dzbj/sleep_mgr.c 整体用 #ifdef CONFIG_BAJI_BADGE_MODE 包裹Phase 1 已做)—— 代码保留可参考
  2. 不删除 CMakeLists.txt 中对应 srcsPhase 1 已包裹在 if(CONFIG_BAJI_BADGE_MODE) 内)
  3. main/application.cc新增RTC 空闲超时逻辑(不依赖 sleep_mgr
    • 复用现有 listening_idle_ticks_ 机制
    • 60s 阈值触发时:
      • 调用 CloseAudioChannel()(断 RTC
      • 调用 pwm_set_brightness(0) 熄屏
      • 暂停 LVGL 刷新
      • 设置 rtc_screen_off_ = true新变量,避免与吧唧 sleep_mgr 全局状态冲突)
  4. 唤醒路径:
    • BOOT 按键回调 → 检查 rtc_screen_off_ → 恢复亮度 + 重连 RTC
    • 长按可选:触发 WiFi 重置(与配网逻辑不冲突)
  5. 字幕/GIF 状态在熄屏前清空(避免唤醒后残留)

RTC 空闲超时逻辑与吧唧 sleep_mgr 的隔离

  • 吧唧的 sleep_mgr_init/notify_activity/is_screen_off 全部在 #ifdef CONFIG_BAJI_BADGE_MODE
  • 新增的 RTC 空闲超时逻辑在 application.cc 中独立实现,使用独立的状态变量
  • 这样两种模式的低功耗机制完全独立,互不干扰,将来如果再启用吧唧模式不会冲突

完成标志

  • 60s 无 RTC 交互 → 自动断开 + 熄屏
  • BOOT 单击 → 屏幕亮起 + 重连 RTC数字人 GIF 重新加载)
  • 系统稳定运行 30 分钟无内存累积
  • sleep_mgr.c 源代码仍在仓库中(可通过 Kconfig 重新启用)

产出 commitfeat(idle): 新增 RTC 空闲超时联动熄屏(保留 sleep_mgr 源码可恢复)


Phase 7: 电量保护 + 低功耗管理重构

目标:把开机电量保护异步化 + 屏幕低电 UI + PowerSaveTimer 状态机重写 + esp_pm_configure 收口受守卫保护,重构成连贯系统而非局部打补丁。

详细规格:见 phases/phase_07_battery_psm/README.md

完成标志

  • 开机不再被电池采样 6 秒阻塞
  • 屏幕分级低电 UI 提示(>25% / 15-25% / <15% / <5%
  • PowerSaveTimer in_sleep_mode_ 状态机无边角
  • esp_pm_configure 调用统一收口到 callback 内部

产出 commitrefactor(power): Phase 7 - 电量保护异步化 + 低功耗状态机重写


Phase 8: 数字人 RTC 音频卡顿根因诊断

目标:通过 4 类 ESP_LOGW 日志埋点采集运行时数据,定位 RTC 音频卡顿真实根因CPU 争抢 / PSRAM 带宽 / DMA / WiFi / Opus 抖动 / 内存碎片),让数据驱动 Phase 9 的实施策略决策。

详细规格:见 phases/phase_08_audio_glitch_diag/README.md

完成标志

  • 4 处日志埋点编译通过并正常输出
  • 实际复现一次卡顿,采集到包含卡顿瞬间的日志
  • 产出 DIAG_REPORT.md 明确根因判定
  • 给出 Phase 9 实施分支推荐A/B/C/D 之一)

产出 commitdiag(rtc-only): Phase 8 - 音频卡顿根因诊断埋点 + 数据采集报告


Phase 9: 音频卡顿实施优化(待 Phase 8 数据决策)

目标:根据 Phase 8 DIAG_REPORT.md 的根因判定,按预案分支实施优化。具体方案在 Phase 8 完成后细化为 PLAN.md

分支预案

分支 触发根因 实施动作 预估工时
A 仅 CPU 争抢 eaf_dec_* 解码器旁路替换 lv_gif,保留 LVGL 框架 1-2 天
B 仅 WiFi/网络 WiFi 缓冲扩容STATIC_RX 10→16、DYN_RX/TX 32→48、RX_BA_WIN 6→16 0.5 天
C 组合 ①+④⑤ 数字人模式完整切 EAFCONFIG_BAJI_BADGE_MODE=n 分支弃用 LVGL+ 释放的 ~40KB DRAM + ~80KB PSRAM 投到 WiFi/Opus/RTC jitter buffer 扩容 3-5 天
D DMA/I2S 取消 EAF 方案,转 DMA 路径排查 视具体问题

完成标志

  • 选定分支落地代码 + 编译通过
  • 卡顿复现场景下听感主观验证:抖动消失或显著降低
  • Phase 8 埋点指标改善(queue 抖动、write_slow 频率均下降)
  • 内存/CPU 监控 30 分钟稳定

产出 commit:(按选定分支命名)perf(rtc-only): Phase 9 - {A/B/C/D 描述}


Phase 10: 集成测试 + 推送

目标:端到端验证 MILESTONE.md 第 6 节全部验收项,推送到 gitea + GitHub。

任务

  1. 整机端到端测试(按 MILESTONE.md 成功标准清单逐项验证)
  2. 内存/CPU 监控:
    • heap_caps_print_heap_info(MALLOC_CAP_INTERNAL)
    • heap_caps_print_heap_info(MALLOC_CAP_SPIRAM)
    • 30 分钟持续对话压测
  3. idf.py size 对比阉割前后固件大小
  4. 更新 docs/Rtc_AIavatar/数字人表情渲染方案_云端预渲染+BLE+OTA.md 章节:阉割成果汇报
  5. 提交 + 推送:
    • git push origin Rtc_AIavatargitea
    • git push https://github.com/Leo-z8/Baji_Rtc_Toy.git Rtc_AIavatar

完成标志

  • MILESTONE.md 第 6 节成功标准全部 ✓
  • Phase 8/9 音频卡顿问题已解决
  • gitea + GitHub 远程已同步
  • 文档更新完成

产出 commitdocs(milestone): 数字人 RTC 项目完成 - 验收报告 + 性能数据


阶段依赖与并行性

  • Phase 1 ⊥ Phase 2独立可并行做
  • Phase 3 依赖 Phase 2需要 6MB SPIFFS
  • Phase 4/5 依赖 Phase 1dzbj 模块清理完成)+ Phase 3GIF 资源就位)
  • Phase 4 ⊥ Phase 5情绪映射和字幕显示独立可并行
  • Phase 6 依赖 Phase 1清理 sleep_mgr 调用点)
  • Phase 7 依赖 Phase 6PowerSaveTimer 状态机重写需 Phase 6 守卫到位)
  • Phase 8 依赖 Phase 6卡顿症状在 Phase 6 收尾发现,需要 RTC 链路稳定)
  • Phase 9 依赖 Phase 8实施策略由诊断报告决定
  • Phase 10 必须最后(依赖 Phase 9 卡顿解决)

建议串行执行顺序1 → 2 → 3 → 4 → 5 → 6 → 7 → 8 → 9 → 10


当前状态

Phase 状态
Phase 1 完成commit 672506e,已推送 gitea + GitHub
Phase 2 完成commit ce7a3aa
Phase 3 完成commit 7d1c7dc
Phase 4 完成commit 497c1b4
Phase 5 完成commit f2be992
Phase 6 完成commit b8a5fe9 + 4b7b194 收尾)
Phase 7 🔄 进行中(phase_07_battery_psm 规格已写,实施待启动)
Phase 8 待启动(音频卡顿诊断埋点,新增)
Phase 9 ⏸️ 阻塞中(待 Phase 8 数据,分支预案 A/B/C/D 已列)
Phase 10 待启动(集成测试 + 推送,原 Phase 7 重编号)