plan(rtc-only): Phase 9 取消 + Phase 10/11/12 规划(LVGL → esp_emote_gfx)
Phase 9 三轮增量优化(jitter buffer / codec init / Core 1 绑定)效果不 明显,用户决策完整切 EAF 验证 GIF 抢资源假设。 Phase 9 → CANCELLED: - v1 jitter buffer device_state 判错(漏 kDeviceStateDialog) - v1 ES7210 重试破坏 ES8311 init 导致开机播报无声 - v2 修正 device_state 后 jitter 工作但仍卡 - v3 background_task 绑 Core 1 + DIAG-5 未硬件验证 - 所有代码改动 git restore 回滚(无 commit),Phase 8 DIAG 埋点保留 - CANCELLED.md 记录教训 Phase 10 新增(数字人模式 LVGL → esp_emote_gfx 完整切换): - 添加 espressif2022/esp_emote_gfx ~3.0.5 依赖(已 reconfigure 拉取) - API 风险扫清:GFX_LABEL_LONG_WRAP 支持中文换行、 gfx_font_lv_load_from_binary 兼容 LVGL bitmap font - 双轨编译:CONFIG_BAJI_BADGE_MODE=y 保 LVGL,=n 走 EAF - PLAN.md 含 10 个子任务从依赖到完整 UI 切换 - 预估 3-5 天 Phase 11 占位:LVGL 释放的 ~40KB DRAM + ~80KB PSRAM 投到 WiFi 缓冲扩容(STATIC_RX 10→16、DYN_RX/TX 32→48、RX_BA_WIN 6→16)+ Opus/RTC SDK jitter buffer 扩容 Phase 12 占位:原 Phase 10 集成测试 + 推送,重编号 ROADMAP 同步更新,依赖关系矫正。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
3dc6cadf49
commit
3a1111e99d
@ -379,29 +379,78 @@ static const emotion_gif_map_t emotion_gif_table[] = {
|
||||
|
||||
---
|
||||
|
||||
## Phase 9: 音频卡顿实施优化(待 Phase 8 数据决策)
|
||||
## Phase 9: ❌ 已取消 — 音频卡顿增量优化尝试
|
||||
|
||||
**目标**:根据 Phase 8 `DIAG_REPORT.md` 的根因判定,按预案分支实施优化。**具体方案在 Phase 8 完成后细化为 PLAN.md**。
|
||||
**取消原因**:用户决策(2026-05-15)— 增量修补效果不明显(v1 引入 ES8311 dev 30 regression,v2 jitter buffer 工作但仍卡,v3 未来得及实测),改为方案 C 完整切 EAF(见 Phase 10)。
|
||||
|
||||
**分支预案**:
|
||||
| 分支 | 触发根因 | 实施动作 | 预估工时 |
|
||||
|---|---|---|---|
|
||||
| **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** | 组合 ①+④⑤ | 数字人模式完整切 EAF(`CONFIG_BAJI_BADGE_MODE=n` 分支弃用 LVGL)+ 释放的 ~40KB DRAM + ~80KB PSRAM 投到 WiFi/Opus/RTC jitter buffer 扩容 | 3-5 天 |
|
||||
| **D** | DMA/I2S | 取消 EAF 方案,转 DMA 路径排查 | 视具体问题 |
|
||||
**保留**:[phase_09_audio_jitter_codecinit/CANCELLED.md](phases/phase_09_audio_jitter_codecinit/CANCELLED.md) 记录 v1/v2/v3 实验教训供未来参考。
|
||||
|
||||
**完成标志**:
|
||||
- ✅ 选定分支落地代码 + 编译通过
|
||||
- ✅ 卡顿复现场景下听感主观验证:抖动消失或显著降低
|
||||
- ✅ Phase 8 埋点指标改善(`queue` 抖动、`write_slow` 频率均下降)
|
||||
- ✅ 内存/CPU 监控 30 分钟稳定
|
||||
|
||||
**产出 commit**:(按选定分支命名)`perf(rtc-only): Phase 9 - {A/B/C/D 描述}`
|
||||
**回滚**:Phase 9 所有代码改动通过 `git restore` 已回滚,Phase 8 DIAG 埋点保留作为 Phase 10 验证基准。
|
||||
|
||||
---
|
||||
|
||||
## Phase 10: 集成测试 + 推送
|
||||
## Phase 10: 数字人模式 LVGL → esp_emote_gfx 完整切换
|
||||
|
||||
**目标**:仅在 `CONFIG_BAJI_BADGE_MODE=n` 分支下弃用 LVGL,采用乐鑫 esp_emote_gfx 框架 + EAF 动画格式。Phase 8 数据排除了"调度"问题(codec write 0 慢),用户决策直接验证 EAF 显示效果。释放 ~40KB DRAM + ~80KB PSRAM 留作 Phase 11 资源再分配。
|
||||
|
||||
**驱动**:用户假设 "GIF 与 RTC 抢占资源 + WiFi 缓冲不够",方案 C 是该假设的最彻底验证 —— LVGL/GIF 完全消失后看显示效果与音频感知。
|
||||
|
||||
**关键改动范围**:
|
||||
1. 添加 `esp_emote_gfx` 组件依赖(idf_component.yml)
|
||||
2. EAF Packer 工具链:hiyori_m{03,06,07}.gif → .eaf(4-bit + Heatshrink + 透明)
|
||||
3. 重写 `main/display/lcd_display.cc` 数字人分支:接管 display 改为 `gfx_emote_add_disp`
|
||||
4. 重写 `main/dzbj/ai_chat_ui.c`(458 行):`lv_obj` → `gfx_obj`,`lv_gif` → `gfx_anim`,`lv_label` → `gfx_label`
|
||||
5. CMakeLists 条件编译切换(仅 `CONFIG_BAJI_BADGE_MODE=n` 走 EAF)
|
||||
6. 字体接驳:`font_puhui_20_4.c` 复用(EAF 官方支持 LVGL bitmap font)
|
||||
7. 触摸路径:cst816s → `gfx_touch`(如数字人模式需要)
|
||||
|
||||
**预估工时**:3-5 天
|
||||
|
||||
**完成标志**:
|
||||
- ✅ `CONFIG_BAJI_BADGE_MODE=n` 编译通过
|
||||
- ✅ 烧录后数字人 GIF 动画正常显示(hiyori 三个表情切换)
|
||||
- ✅ 字幕显示(中文 + 自动换行 + 双行居中)
|
||||
- ✅ RTC 对话听感主观验证:扬声器卡顿是否消失(这是核心 PoC 目的)
|
||||
- ✅ `idf.py size` 对比:Flash/DRAM/PSRAM 变化
|
||||
- ✅ DRAM 净结余 ≥ 30KB 用于 Phase 11
|
||||
|
||||
**产出 commit**:`feat(ui): Phase 10 - 数字人模式 LVGL → esp_emote_gfx 完整迁移`
|
||||
|
||||
**关键风险**:
|
||||
- `gfx_label` 中文自动换行 + 双行居中支持验证(实施前先跑 PoC)
|
||||
- 数字人模式如有触摸交互,需重写
|
||||
- 资源转 EAF 工具链稳定性
|
||||
- `ui_ScreenUpdate`(吧唧模式 BLE 收图 UI)共用 lv_gif,方案 C 仅影响数字人模式,吧唧模式保持 LVGL(双轨编译)
|
||||
|
||||
---
|
||||
|
||||
## Phase 11: 内存优化 + WiFi 缓冲扩容
|
||||
|
||||
**目标**:把 Phase 10 释放的 ~40KB DRAM + ~80KB PSRAM 重新分配到 WiFi RX/TX 缓冲、Opus jitter buffer、RTC SDK jitter buffer,最大化 RTC 体验。
|
||||
|
||||
**关键改动**:
|
||||
1. `sdkconfig.defaults`:
|
||||
- `CONFIG_ESP_WIFI_STATIC_RX_BUFFER_NUM`: 10 → 16
|
||||
- `CONFIG_ESP_WIFI_DYNAMIC_RX_BUFFER_NUM`: 32 → 48
|
||||
- `CONFIG_ESP_WIFI_DYNAMIC_TX_BUFFER_NUM`: 32 → 48
|
||||
- `CONFIG_ESP_WIFI_RX_BA_WIN`: 6 → 16(AMPDU 窗口扩大减少重传)
|
||||
2. Opus decode pool 上限扩容(如 SDK 暴露)
|
||||
3. 火山 RTC SDK jitter buffer 配置扩容(如 SDK 暴露)
|
||||
4. `heap_caps_print_heap_info` 前后对比
|
||||
|
||||
**预估工时**:1 天
|
||||
|
||||
**完成标志**:
|
||||
- ✅ sdkconfig 改动编译通过且烧录稳定
|
||||
- ✅ DRAM 投入 ≤ 15KB 后剩余可用 ≥ Phase 8 baseline
|
||||
- ✅ Phase 8 DIAG 埋点对比:queue=0 次数下降、WiFi 重传减少
|
||||
- ✅ 用户主观:扬声器流畅度提升
|
||||
|
||||
**产出 commit**:`perf(rtc-only): Phase 11 - WiFi 缓冲扩容 + jitter buffer 强化`
|
||||
|
||||
---
|
||||
|
||||
## Phase 12: 集成测试 + 推送
|
||||
|
||||
**目标**:端到端验证 MILESTONE.md 第 6 节全部验收项,推送到 gitea + GitHub。
|
||||
|
||||
@ -419,7 +468,7 @@ static const emotion_gif_map_t emotion_gif_table[] = {
|
||||
|
||||
**完成标志**:
|
||||
- ✅ MILESTONE.md 第 6 节成功标准全部 ✓
|
||||
- ✅ Phase 8/9 音频卡顿问题已解决
|
||||
- ✅ Phase 10/11 音频卡顿问题已解决
|
||||
- ✅ gitea + GitHub 远程已同步
|
||||
- ✅ 文档更新完成
|
||||
|
||||
@ -436,10 +485,12 @@ static const emotion_gif_map_t emotion_gif_table[] = {
|
||||
- Phase 6 依赖 Phase 1(清理 sleep_mgr 调用点)
|
||||
- Phase 7 依赖 Phase 6(PowerSaveTimer 状态机重写需 Phase 6 守卫到位)
|
||||
- **Phase 8 依赖 Phase 6(卡顿症状在 Phase 6 收尾发现,需要 RTC 链路稳定)**
|
||||
- **Phase 9 依赖 Phase 8(实施策略由诊断报告决定)**
|
||||
- Phase 10 必须最后(依赖 Phase 9 卡顿解决)
|
||||
- **Phase 9 已取消**(增量优化效果不明显,改走 Phase 10 完整切 EAF)
|
||||
- **Phase 10 依赖 Phase 8**(数据排除调度问题后用户决策完整切 EAF 验证显示效果)
|
||||
- **Phase 11 依赖 Phase 10**(用 Phase 10 释放的 DRAM/PSRAM 做资源再分配)
|
||||
- Phase 12 必须最后(依赖 Phase 11 完成)
|
||||
|
||||
**建议串行执行顺序**:1 → 2 → 3 → 4 → 5 → 6 → 7 → 8 → 9 → 10
|
||||
**建议串行执行顺序**:1 → 2 → 3 → 4 → 5 → 6 → 7 → 8 → ~~9~~ → 10 → 11 → 12
|
||||
|
||||
---
|
||||
|
||||
@ -454,6 +505,8 @@ static const emotion_gif_map_t emotion_gif_table[] = {
|
||||
| Phase 5 | ✅ 完成(commit `f2be992`) |
|
||||
| Phase 6 | ✅ 完成(commit `b8a5fe9` + `4b7b194` 收尾) |
|
||||
| Phase 7 | 🔄 进行中([phase_07_battery_psm](phases/phase_07_battery_psm/README.md) 规格已写,实施待启动) |
|
||||
| **Phase 8** | **⏳ 待启动(音频卡顿诊断埋点,新增)** |
|
||||
| **Phase 9** | **⏸️ 阻塞中(待 Phase 8 数据,分支预案 A/B/C/D 已列)** |
|
||||
| Phase 10 | ⏳ 待启动(集成测试 + 推送,原 Phase 7 重编号) |
|
||||
| Phase 8 | ✅ 完成(commit `3dc6cad`,4 类 DIAG 埋点 + 根因报告) |
|
||||
| **Phase 9** | **❌ 已取消**(v1/v2/v3 增量优化未解决,CANCELLED.md 记录教训) |
|
||||
| **Phase 10** | **⏳ 待启动**(数字人模式 LVGL→EAF 完整切换,新增) |
|
||||
| **Phase 11** | **⏸️ 阻塞中**(内存优化 + WiFi 缓冲扩容,依赖 Phase 10 完成) |
|
||||
| Phase 12 | ⏳ 待启动(集成测试 + 推送,原 Phase 10 重编号) |
|
||||
|
||||
@ -0,0 +1,71 @@
|
||||
# Phase 9 — 已取消
|
||||
|
||||
**取消日期**:2026-05-15
|
||||
**取消决策方**:用户
|
||||
**取消原因**:增量修补策略(jitter buffer + codec init 调整)效果不明显,用户改为方案 C 完整切 EAF(Phase 10)以验证 GIF/LVGL 是否真是元凶。
|
||||
|
||||
---
|
||||
|
||||
## 三轮尝试教训
|
||||
|
||||
### v1(初版)
|
||||
**改动**:
|
||||
- 9.1 jitter buffer:`OnAudioOutput` 加 FILLING/DRAINING 状态机(PREBUF=4 / OVERFLOW=12)
|
||||
- 9.2.1 ES7210 init 3 次重试 + 失败降级
|
||||
- 9.2.2 PlaySound 前 vTaskDelay(150ms)
|
||||
|
||||
**实测问题**:
|
||||
1. **jitter buffer 完全没生效** — 日志中 0 处 "Jitter buffer 蓄水完成"
|
||||
- 根因:项目 RTC 对话状态用 `kDeviceStateDialog`(不是 Speaking/Listening)
|
||||
- 我的判定漏了 Dialog → 在对话期完全不进 jitter 分支
|
||||
2. **开机播报听不到声音** — 新 regression
|
||||
- 根因:ES7210 重试逻辑改变 I2C 总线时序 → ES8311 (`dev 30`) 写入失败
|
||||
- Phase 8 baseline 时 ES7210 失败会触发 assert reboot 自愈;v1 让 ES7210 不失败反而破坏 ES8311 init
|
||||
|
||||
### v2(hotfix)
|
||||
**改动**:
|
||||
- jitter buffer 判定加 `kDeviceStateDialog`
|
||||
- 回退 9.2.1(恢复 Phase 8 baseline 的 assert 模式)
|
||||
- 保留 9.2.2 (150ms 延迟)
|
||||
|
||||
**实测**:
|
||||
- jitter buffer 工作了(15+ 次 "蓄水完成开始消费 (q=4)")✓
|
||||
- 对话期 `write_slow=0` ✓
|
||||
- 开机播报恢复正常 ✓
|
||||
- **但用户主观仍然非常卡顿** ✗
|
||||
|
||||
**核心数据**:
|
||||
- 对话期 (30s+) queue 采样 859 次
|
||||
- 平均 3.6 / 最大 22 / queue=0 出现 52 次 / queue≥12 出现 28 次(远超 OVERFLOW)
|
||||
|
||||
### v3(深挖)
|
||||
**新假设**:`background_task` 使用 `xTaskCreate` **未绑核** → 可能跑 Core 0 与 LVGL/GIF 抢调度。
|
||||
|
||||
**改动**:
|
||||
- `background_task.cc` 改用 `xTaskCreatePinnedToCore(..., 1)` 强制 Core 1
|
||||
- 加 DIAG-5 `bg_lag` 埋点测调度延迟
|
||||
- jitter buffer 调大 PREBUF 4→6, OVERFLOW 12→24
|
||||
|
||||
**结果**:未来得及硬件实测 — 用户决策放弃增量修补改走 Phase 10。
|
||||
|
||||
---
|
||||
|
||||
## 关键教训
|
||||
|
||||
1. **device_state 名字不能凭印象写**,务必先 grep 项目实际使用
|
||||
2. **修改一个 codec init 链路时,要预想对邻近 codec 的连锁影响**(共享 I2C 总线 / 时钟)
|
||||
3. **DIAG-2 测 codec write 耗时是不够的** — 测不到 background_task schedule 延迟
|
||||
4. **增量优化策略容易越改越乱**,当假设需要双线/三线修复时,应考虑是否方向本身不对
|
||||
5. **用户的物理直觉值得尊重** — "kapi 项目底座 RTC 流畅 → 数字人模式卡顿" 这个对比本身就是强证据,指向新增的 GIF/LVGL
|
||||
|
||||
---
|
||||
|
||||
## 回滚记录
|
||||
|
||||
- 所有代码改动通过 `git restore` 回滚(无 commit)
|
||||
- 影响文件:
|
||||
- `main/application.cc/h`
|
||||
- `main/audio_codecs/box_audio_codec.cc`
|
||||
- `main/background_task.cc`
|
||||
- Phase 8 commit `3dc6cad` 保留(DIAG 埋点继续作为 Phase 10 验证基准)
|
||||
- 本目录 `phase_09_audio_jitter_codecinit/` 全部文档(PLAN.md / IMPL_REPORT 模板 / 多版本日志)保留供回溯
|
||||
@ -0,0 +1,361 @@
|
||||
# Phase 9 PLAN — 音频卡顿双线修复(jitter buffer + codec init 时序)
|
||||
|
||||
> 里程碑: `digital_human_rtc`
|
||||
> 阶段目标: 基于 Phase 8 [DIAG_REPORT.md](../phase_08_audio_glitch_diag/DIAG_REPORT.md) 双根因判定 ③'+⑤,双线修复 RTC 对话期 jitter + 开机 codec init 时序。
|
||||
> 性质: **实施 phase**,纯软件改动,不引入 esp_emote_gfx / 不调整 sdkconfig。
|
||||
|
||||
---
|
||||
|
||||
## 0. 决策依据(DIAG_REPORT 摘要)
|
||||
|
||||
- 主观感知:A. 开机"卡卡在呢"声音抖 + B. RTC 对话期 AI 回答断续
|
||||
- **A 根因 ③'**:ES7210 I2C 0x80 写入失败 → 2-13s 集中 126 次 `write_slow`(50-58ms/帧)
|
||||
- **B 根因 ⑤**:WebSocket Opus 帧应用层突发到达(queue=19 突发 + queue=0 出现 58 次 @ 40s+)
|
||||
- 排除:CPU 争抢 / PSRAM 带宽 / WiFi 物理层 / 内存碎片
|
||||
|
||||
---
|
||||
|
||||
## 1. 设计方案
|
||||
|
||||
### 1.1 子任务 9.1 — 应用层 jitter buffer(解 B)
|
||||
|
||||
**位置**: [main/application.cc](../../../../main/application.cc) `Application::OnAudioOutput()`
|
||||
|
||||
**机制**(fill-threshold + drain + overflow drop):
|
||||
```
|
||||
状态机:
|
||||
┌─ FILLING ──q ≥ PREBUF ───▶ DRAINING ─q = 0─▶ FILLING ─┐
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
|
||||
参数:
|
||||
PREBUF = 4 帧 (60ms × 4 = 240ms 预蓄水)
|
||||
OVERFLOW = 12 帧 (60ms × 12 = 720ms 上限,超过丢最老 1 帧)
|
||||
|
||||
行为:
|
||||
FILLING: audio_decode_queue_.empty() 一直 return; 等填满 PREBUF 帧
|
||||
DRAINING: 正常 pop_front + 解码 + 输出
|
||||
DRAINING + size > OVERFLOW: 主动 pop_front 丢老帧(防止永久堆积)
|
||||
DRAINING + queue empty: 切回 FILLING(重新蓄水)
|
||||
```
|
||||
|
||||
**仅在 RTC 对话期生效**:device_state == kDeviceStateSpeaking 或 kDeviceStateListening
|
||||
- 开机播报 PlaySound 路径 **不走** jitter buffer(一次性灌入数据,预蓄水反而拖累首帧)
|
||||
- 通过 protocol 状态或 device_state 判断
|
||||
|
||||
**关键参数推导**(基于 Phase 8 DIAG 数据):
|
||||
- queue=19 突发观察到 4 帧(45039/45089/45199 enq 集中)
|
||||
- PREBUF=4 留出 240ms 抗抖,能吞下一次"突发-消化"周期
|
||||
- OVERFLOW=12 防止极端情况(≥720ms 堆积明显延迟,丢帧重新对齐)
|
||||
|
||||
### 1.2 子任务 9.2 — 开机 codec init 时序加固(解 A)
|
||||
|
||||
**位置**:
|
||||
- [main/audio_codecs/box_audio_codec.cc](../../../../main/audio_codecs/box_audio_codec.cc) ES7210 init 重试 + 错误降级
|
||||
- [main/application.cc](../../../../main/application.cc) `Application::Start()` 中 PlaySound 前等待 codec 稳定
|
||||
|
||||
**改动 1 — ES7210 init 失败重试**(BoxAudioCodec 构造函数 line 73-82):
|
||||
```cpp
|
||||
// 当前:
|
||||
in_codec_if_ = es7210_codec_new(&es7210_cfg);
|
||||
assert(in_codec_if_ != NULL); // ✗ assert 失败直接 abort
|
||||
|
||||
// 改造为:3 次重试 + 失败降级
|
||||
for (int retry = 0; retry < 3; retry++) {
|
||||
in_codec_if_ = es7210_codec_new(&es7210_cfg);
|
||||
if (in_codec_if_ != NULL) break;
|
||||
ESP_LOGW(TAG, "ES7210 init failed, retry %d/3", retry + 1);
|
||||
vTaskDelay(pdMS_TO_TICKS(50));
|
||||
}
|
||||
if (in_codec_if_ == NULL) {
|
||||
ESP_LOGE(TAG, "ES7210 init permanently failed, fallback to output-only");
|
||||
// 不 abort,置为 output_only 等价状态
|
||||
}
|
||||
```
|
||||
|
||||
**改动 2 — Application::Start 中 codec ready 等待**(PlaySound "开机播报语音" 之前):
|
||||
```cpp
|
||||
// 当前:
|
||||
codec->EnableInput(true);
|
||||
codec->EnableOutput(true);
|
||||
PlaySound(...); // ✗ 立即播报,可能 codec 仍在 init
|
||||
|
||||
// 改造为:等 codec 稳定后再播报
|
||||
codec->EnableInput(true);
|
||||
codec->EnableOutput(true);
|
||||
vTaskDelay(pdMS_TO_TICKS(150)); // 给 I2S DMA + codec 稳定时间
|
||||
PlaySound(...);
|
||||
```
|
||||
|
||||
**改动 3 - 不引入新的 codec ready 标志**(不动 audio_codec.h 接口):
|
||||
- 150ms 是经验值,足以让 ES8311 → I2S DMA → ES7210 stabilize
|
||||
- 如果用户测下来仍卡,再升级到 codec ready callback 机制
|
||||
|
||||
---
|
||||
|
||||
## 2. 任务清单
|
||||
|
||||
### 任务 9.1.1 — 在 Application 类中加 jitter buffer 状态成员
|
||||
|
||||
**文件**: [main/application.h](../../../../main/application.h)
|
||||
|
||||
**读取参考**:
|
||||
- 现有 private 成员变量布局
|
||||
- audio_decode_queue_ 声明
|
||||
|
||||
**改动**: 在 audio_decode_queue_ 附近加:
|
||||
```cpp
|
||||
// Phase 9.1: 应用层 jitter buffer(解 RTC 对话期 Opus 帧到达抖动)
|
||||
enum class JitterState : uint8_t { FILLING = 0, DRAINING = 1 };
|
||||
JitterState jitter_state_ = JitterState::FILLING;
|
||||
static constexpr int kJitterPrebufFrames = 4; // 预蓄水阈值
|
||||
static constexpr int kJitterOverflowFrames = 12; // 上限丢帧阈值
|
||||
```
|
||||
|
||||
**验收**:
|
||||
- `grep -nE "jitter_state_|kJitterPrebufFrames|kJitterOverflowFrames" main/application.h` 各返回 ≥ 1
|
||||
- 编译通过
|
||||
|
||||
---
|
||||
|
||||
### 任务 9.1.2 — OnAudioOutput 中插入 jitter buffer 状态机
|
||||
|
||||
**文件**: [main/application.cc](../../../../main/application.cc) 的 `OnAudioOutput()`
|
||||
|
||||
**读取参考**:
|
||||
- 当前 `if (audio_decode_queue_.empty()) return;` 分支(约 2126 行)
|
||||
- 当前 `auto opus = std::move(audio_decode_queue_.front());` 出队点(约 2167 行)
|
||||
- DIAG-1 出队埋点位置
|
||||
|
||||
**改动**(在出队前插入状态机判定):
|
||||
|
||||
```cpp
|
||||
// 在 audio_decode_queue_.empty() 处理之后、device_state 检查之前插入
|
||||
|
||||
// Phase 9.1: 应用层 jitter buffer 状态机(仅对话期生效,开机播报不走)
|
||||
bool is_rtc_audio = (device_state_ == kDeviceStateSpeaking ||
|
||||
device_state_ == kDeviceStateListening) &&
|
||||
opus_playback_active_.load() == false; // 不与 HTTPS 抢
|
||||
if (is_rtc_audio) {
|
||||
int q = (int)audio_decode_queue_.size();
|
||||
if (jitter_state_ == JitterState::FILLING) {
|
||||
if (q < kJitterPrebufFrames) {
|
||||
return; // 蓄水中,不消费
|
||||
}
|
||||
jitter_state_ = JitterState::DRAINING;
|
||||
ESP_LOGI(TAG, "Jitter buffer 蓄水完成开始消费 (q=%d)", q);
|
||||
} else { // DRAINING
|
||||
if (q > kJitterOverflowFrames) {
|
||||
// 上限保护:丢最老 1 帧
|
||||
audio_decode_queue_.pop_front();
|
||||
ESP_LOGW(TAG, "Jitter buffer 超限丢帧 (q=%d)", q);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 出队(原有逻辑)
|
||||
auto opus = std::move(audio_decode_queue_.front());
|
||||
audio_decode_queue_.pop_front();
|
||||
// ...
|
||||
```
|
||||
|
||||
**重置点**(empty 分支末尾,准备返回前):
|
||||
```cpp
|
||||
if (audio_decode_queue_.empty()) {
|
||||
// ... 原有逻辑 ...
|
||||
|
||||
// Phase 9.1: 队列变空 → 切回 FILLING 重新蓄水
|
||||
if (jitter_state_ == JitterState::DRAINING) {
|
||||
jitter_state_ = JitterState::FILLING;
|
||||
ESP_LOGD(TAG, "Jitter buffer 重置为 FILLING");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
```
|
||||
|
||||
**验收**:
|
||||
- `grep -c "Jitter buffer" main/application.cc` ≥ 3
|
||||
- 编译通过
|
||||
|
||||
---
|
||||
|
||||
### 任务 9.1.3 — 状态切换时复位 jitter(避免转入对话即受历史影响)
|
||||
|
||||
**文件**: [main/application.cc](../../../../main/application.cc)
|
||||
|
||||
**读取参考**:
|
||||
- `SetDeviceState(kDeviceStateSpeaking)` / `SetDeviceState(kDeviceStateListening)` 等转换点
|
||||
|
||||
**改动**: 在转入 Listening 或 Speaking 状态时显式置 FILLING:
|
||||
- 找 `void Application::SetDeviceState(DeviceState state)` 入口
|
||||
- 在状态切换时若新状态是 Speaking/Listening,置 `jitter_state_ = JitterState::FILLING`
|
||||
|
||||
**验收**:
|
||||
- 切换 RTC 对话开始时日志能看到一次 "Jitter buffer 蓄水完成开始消费"
|
||||
- 编译通过
|
||||
|
||||
---
|
||||
|
||||
### 任务 9.2.1 — ES7210 init 重试 + 不 abort
|
||||
|
||||
**文件**: [main/audio_codecs/box_audio_codec.cc](../../../../main/audio_codecs/box_audio_codec.cc)
|
||||
|
||||
**读取参考**:
|
||||
- line 73-82: 当前 ES7210 init 路径
|
||||
- `assert(in_codec_if_ != NULL)` 失败行为
|
||||
|
||||
**改动**: 将 line 76 `in_codec_if_ = es7210_codec_new(&es7210_cfg);` + `assert` 替换为重试循环(详细代码见 §1.2 改动 1)
|
||||
|
||||
**注意**:
|
||||
- 重试 3 次仍失败时**不 abort**,但应该把 `input_dev_` 置为 nullptr 避免后续 `esp_codec_dev_open(input_dev_)` 崩
|
||||
- 同样把 line 80 `dev_cfg.codec_if = in_codec_if_;` + line 82 `assert(input_dev_ != NULL);` 也要相应处理(in_codec_if_ 为 null 时不创建 input_dev_)
|
||||
|
||||
**验收**:
|
||||
- `grep -c "ES7210 init failed" main/audio_codecs/box_audio_codec.cc` = 1
|
||||
- 编译通过
|
||||
- 烧录后即使 ES7210 init 失败也不会 reboot 循环(保留向后兼容)
|
||||
|
||||
---
|
||||
|
||||
### 任务 9.2.2 — PlaySound 前等 codec 稳定
|
||||
|
||||
**文件**: [main/application.cc](../../../../main/application.cc) `Application::Start()`
|
||||
|
||||
**读取参考**:
|
||||
- `AudioCodec: 将运行时输出音量设置为:80` 之后到 `PlaySound` 之前
|
||||
- 当前路径:"设备启动完成,播放开机播报语音"
|
||||
|
||||
**改动**: 在 PlaySound 调用之前添加 150ms 延迟。具体定位关键字 "设备启动完成" 附近。
|
||||
|
||||
**验收**:
|
||||
- `grep -nE "vTaskDelay.*150.*codec|codec 稳定等待" main/application.cc` 返回 1 行
|
||||
- 编译通过
|
||||
- 烧录后日志:开机播报阶段 `write_slow` 次数应显著减少(从 126 次降到 <20 次)
|
||||
|
||||
---
|
||||
|
||||
### 任务 9.3 — 编译 + 烧录 + 验证(用 Phase 8 DIAG 埋点做对比)
|
||||
|
||||
**前置**: 9.1.x + 9.2.x 全部任务完成
|
||||
|
||||
**步骤**:
|
||||
```bash
|
||||
source ~/esp/esp-idf/export.sh
|
||||
idf.py build flash monitor 2>&1 | tee .planning/milestones/digital_human_rtc/phases/phase_09_audio_jitter_codecinit/phase_09_diag.log
|
||||
```
|
||||
|
||||
测试场景与 Phase 8 一致:5 分钟 RTC 对话 + 主动 GIF 切换。
|
||||
|
||||
**关键对比指标**(vs Phase 8 baseline):
|
||||
|
||||
| 指标 | Phase 8 baseline | Phase 9 目标 |
|
||||
|---|---|---|
|
||||
| 开机播报 write_slow (2-13s) | 126 次 | **< 20 次** |
|
||||
| 对话期 queue=0 次数 | 58 (40s+) | **< 15** |
|
||||
| 对话期 queue max | 19 | **< 8** |
|
||||
| 对话期 write_slow | 0 | 保持 0 |
|
||||
| 用户主观:开机"卡卡在呢"听感 | 抖 | **不抖** |
|
||||
| 用户主观:对话期 AI 声音 | 断续 | **连贯** |
|
||||
|
||||
**Jitter buffer 工作日志样本**(应能看到):
|
||||
```
|
||||
I Jitter buffer 蓄水完成开始消费 (q=4)
|
||||
... 几十秒正常消费 ...
|
||||
W Jitter buffer 超限丢帧 (q=13) ← 偶发但合理
|
||||
D Jitter buffer 重置为 FILLING ← AI 说话间隙
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 任务 9.4 — 产出 IMPL_REPORT.md + commit
|
||||
|
||||
**文件**: 新建 `.planning/milestones/digital_human_rtc/phases/phase_09_audio_jitter_codecinit/IMPL_REPORT.md`
|
||||
|
||||
**结构**:
|
||||
```markdown
|
||||
# Phase 9 — 实施报告
|
||||
|
||||
## 1. 改动概览
|
||||
- 9.1 jitter buffer:3 处代码 + N 行
|
||||
- 9.2 codec init:2 处代码 + N 行
|
||||
|
||||
## 2. 实测对比表(Phase 8 vs Phase 9)
|
||||
| 指标 | Phase 8 | Phase 9 | 改善 |
|
||||
|
||||
## 3. 用户主观验证
|
||||
- 开机播报听感:☐ 不抖 / ☐ 仍抖
|
||||
- 对话期 AI 声音:☐ 连贯 / ☐ 仍断续
|
||||
|
||||
## 4. 残留问题(若有)
|
||||
...
|
||||
|
||||
## 5. PHASE8_DIAG_ENABLE 是否关闭
|
||||
☐ 已关 / ☐ 保留待 Phase 10 集成测试
|
||||
```
|
||||
|
||||
**commit message**:
|
||||
```
|
||||
perf(rtc-only): Phase 9 - 音频卡顿双线修复(jitter buffer + codec init)
|
||||
|
||||
9.1 应用层 jitter buffer(解决 ⑤ Opus 帧到达抖动):
|
||||
- OnAudioOutput 加 FILLING/DRAINING 状态机
|
||||
- PREBUF=4 帧(240ms 预蓄水)+ OVERFLOW=12 帧(720ms 上限丢帧)
|
||||
- 仅 RTC 对话期生效,开机播报路径绕过
|
||||
|
||||
9.2 开机 codec init 时序加固(解决 ③' ES7210 I2C 失败):
|
||||
- ES7210 init 失败 3 次重试 + 失败降级不 abort
|
||||
- PlaySound 前 150ms codec 稳定等待
|
||||
|
||||
实测对比(详 IMPL_REPORT.md):
|
||||
- 开机 write_slow 126→{N}
|
||||
- 对话期 queue=0 出现 58→{M}
|
||||
- 对话期 queue max 19→{X}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 任务顺序
|
||||
|
||||
```
|
||||
9.1.1 (header 成员) → 9.1.2 (OnAudioOutput) → 9.1.3 (状态切换重置)
|
||||
↓
|
||||
9.2.1 (ES7210 重试) → 9.2.2 (PlaySound 等待) ──┴→ 9.3 (build+flash 验证) → 9.4 (commit)
|
||||
```
|
||||
|
||||
9.1 / 9.2 可并行编辑,统一在 9.3 build。
|
||||
|
||||
---
|
||||
|
||||
## 4. 风险与回滚
|
||||
|
||||
| 风险 | 触发 | 缓解 |
|
||||
|---|---|---|
|
||||
| PREBUF=4 蓄水延迟首字 240ms | 用户感觉 AI"反应慢" | 这是设计权衡:换抖动减少。240ms 远小于 AI 思考延迟(秒级),不会显著感知 |
|
||||
| OVERFLOW=12 丢帧导致句子缺词 | 网络长时间堆积 | 720ms 才丢,且只丢老帧(用户最早感知的"音频已过期"),不丢最新 |
|
||||
| ES7210 重试 150ms 拖慢开机 | 用户感觉开机变慢 | 50ms × 3 = 最多 150ms,可接受 |
|
||||
| PlaySound 等 150ms 让开机播报变慢 | 用户感觉开机变慢 | 150ms 不可感知 |
|
||||
| jitter buffer 干扰 HTTPS 音频播放 | HTTPS(故事/音乐)走 audio_decode_queue_ 同一通道 | `is_rtc_audio` 判定加 `opus_playback_active_ == false` 排除 HTTPS |
|
||||
| 回滚 | 9.1 或 9.2 任一引入 regression | 单独 `git revert` 即可(独立子任务) |
|
||||
|
||||
---
|
||||
|
||||
## 5. Phase 9 完成验收清单
|
||||
|
||||
- [ ] 9.1.1-9.2.2 五个子任务全部代码就位
|
||||
- [ ] `idf.py build` 通过
|
||||
- [ ] 烧录 + RTC 对话 ≥ 5 分钟
|
||||
- [ ] 开机 `write_slow` 次数 < 20(vs baseline 126)
|
||||
- [ ] 对话期 `queue=0` 次数 < 15(vs baseline 58)
|
||||
- [ ] 用户主观:开机"卡卡在呢"不抖 + 对话 AI 声音连贯
|
||||
- [ ] IMPL_REPORT.md 填实数据
|
||||
- [ ] commit 推送到 Rtc_AIavatar
|
||||
|
||||
---
|
||||
|
||||
## 6. Phase 9 不做的事
|
||||
|
||||
- ❌ 不引入 esp_emote_gfx(Phase 8 数据否决了 CPU 假设)
|
||||
- ❌ 不动 `CONFIG_ESP_WIFI_*_BUFFER_NUM`(物理层 OK)
|
||||
- ❌ 不深挖 ES7210 寄存器级 init 问题(重试 + 降级足够,深层修复留待后续)
|
||||
- ❌ 不关闭 PHASE8_DIAG_ENABLE(保留埋点便于 Phase 9 验证)
|
||||
- ❌ 不动 LVGL/GIF 相关代码
|
||||
@ -0,0 +1,351 @@
|
||||
# Phase 10 PLAN — 数字人模式 LVGL → esp_emote_gfx 完整切换
|
||||
|
||||
> 里程碑: `digital_human_rtc`
|
||||
> 阶段目标: `CONFIG_BAJI_BADGE_MODE=n` 时数字人模式完全弃用 LVGL,采用乐鑫 esp_emote_gfx + EAF 动画格式,验证 GIF 抢资源假设是否成立。吧唧模式保持 LVGL。
|
||||
> 预估工时: 3-5 天(含 PoC 显示效果验证 + 完整 UI 切换)
|
||||
|
||||
---
|
||||
|
||||
## 0. 调研结论
|
||||
|
||||
### 0.1 esp_emote_gfx 核心 API(已实地调查)
|
||||
|
||||
- 组件名: `espressif2022/esp_emote_gfx` v3.0.5
|
||||
- 入口: `gfx_emote_init() / gfx_emote_add_disp()` 接管显示
|
||||
- 控件: `gfx_obj` 基类 + `gfx_anim / gfx_img / gfx_label / gfx_button / gfx_qrcode`
|
||||
- EAF 格式: magic `0x5A5A`, 4/8/24-bit 调色板, RLE/Huffman/Heatshrink/RAW 多种编码,block-based decode,原生透明(调色板 idx 0)
|
||||
- 文档: `espressif2022.github.io/esp_emote_gfx/zh_CN/index.html`
|
||||
- 工具: ESP Emote GFX Packer `emote-gfx-gen-tool-dev.pages.dev`
|
||||
|
||||
### 0.2 项目数字人模式 UI 边界(已确认)
|
||||
|
||||
数字人模式下 UI 只有 [main/dzbj/ai_chat_ui.c](../../../../main/dzbj/ai_chat_ui.c) (458 行) 和 [main/display/lcd_display.cc](../../../../main/display/lcd_display.cc) 数字人分支,**7 个 LVGL 对象**:
|
||||
|
||||
| 控件 | EAF 对应 | 难度 |
|
||||
|---|---|---|
|
||||
| `ai_screen` (lv_obj 根容器) | `gfx_obj`(disp 根)| 低 |
|
||||
| `gif_emotion` (lv_gif 主数字人) | `gfx_anim` + EAF 资源 | 低 |
|
||||
| `gif_icon` (lv_gif 叠加图标) | `gfx_anim` + EAF 资源 | 低 |
|
||||
| `emoji_img` (lv_img 静态) | `gfx_img` + RGB565A8 | 低 |
|
||||
| `status_label` (lv_label) | `gfx_label` | 低 |
|
||||
| `chat_container` (lv_obj 字幕背景) | `gfx_obj` 容器 | 低 |
|
||||
| `chat_label` (lv_label 字幕 312×48 + 自动换行 + 居中) | `gfx_label` | **⚠️ 中文换行风险点** |
|
||||
|
||||
### 0.3 关键风险(必须先 PoC 验证)
|
||||
|
||||
1. **gfx_label 中文自动换行 + 双行居中**:未在 esp_emote_gfx 公开文档中明确说明,需 PoC
|
||||
2. **font_puhui_20_4.c 复用**:8.5MB LVGL bitmap font,EAF 文档说支持 LVGL bitmap font,但实际兼容性需验证
|
||||
3. **display 接管冲突**:数字人模式下 LCD panel 必须只由 esp_emote_gfx 接管(移除 lvgl_port)
|
||||
|
||||
### 0.4 双轨架构
|
||||
|
||||
| 编译条件 | UI 框架 | 涉及文件 |
|
||||
|---|---|---|
|
||||
| `CONFIG_BAJI_BADGE_MODE=y` | LVGL(原有) | `lcd_display.cc` + `ui/screens/*` + `ai_chat_ui.c` (LVGL 版本) |
|
||||
| `CONFIG_BAJI_BADGE_MODE=n` | **esp_emote_gfx** | 新增 `eaf_display.cc/h` + 新版 ai_chat_ui (EAF) |
|
||||
|
||||
---
|
||||
|
||||
## 1. 设计方案
|
||||
|
||||
### 1.1 文件结构(新增)
|
||||
|
||||
```
|
||||
main/display/
|
||||
display.h 保留(虚基类,接口不变)
|
||||
lcd_display.cc/h 保留(LVGL,吧唧模式专用)
|
||||
eaf_display.cc/h ← 新增(esp_emote_gfx,数字人模式专用)
|
||||
|
||||
main/dzbj/
|
||||
ai_chat_ui.c 保留(LVGL 版本,吧唧模式用)
|
||||
ai_chat_ui_eaf.c ← 新增(EAF 版本,数字人模式用)
|
||||
|
||||
main/boards/movecall-moji-esp32s3/
|
||||
movecall_moji_esp32s3.cc 修改:根据 BAJI_BADGE_MODE 实例化 LcdDisplay 或 EafDisplay
|
||||
|
||||
spiffs_image/
|
||||
hiyori_m03.gif → hiyori_m03.eaf ← 离线工具转换
|
||||
hiyori_m06.gif → hiyori_m06.eaf
|
||||
hiyori_m07.gif → hiyori_m07.eaf
|
||||
```
|
||||
|
||||
### 1.2 CMakeLists 条件编译
|
||||
|
||||
```cmake
|
||||
# main/CMakeLists.txt
|
||||
if(CONFIG_BAJI_BADGE_MODE)
|
||||
list(APPEND srcs
|
||||
"display/lcd_display.cc"
|
||||
"dzbj/ai_chat_ui.c"
|
||||
# ... LVGL UI screens ...
|
||||
)
|
||||
else()
|
||||
# 数字人模式:EAF 路径
|
||||
list(APPEND srcs
|
||||
"display/eaf_display.cc"
|
||||
"dzbj/ai_chat_ui_eaf.c"
|
||||
)
|
||||
endif()
|
||||
```
|
||||
|
||||
### 1.3 Display 接口适配
|
||||
|
||||
`display.h` 现有虚函数接口(`SetStatus / SetEmotion / SetChatMessage / ...`)**不动**。
|
||||
`EafDisplay : public Display` 实现这些函数,内部用 gfx_label / gfx_anim 等。
|
||||
|
||||
### 1.4 实施策略:分阶段 PoC
|
||||
|
||||
为了快速看到显示效果,分两步:
|
||||
- **PoC 阶段(10.1-10.3)**:最小可行 —— 加依赖 + 转一个 GIF + 写最小 EafDisplay 只显示 hiyori GIF。**烧录看显示**。
|
||||
- **完整阶段(10.4-10.8)**:扩展字幕、状态、emoji、CMakeLists 切换。
|
||||
|
||||
---
|
||||
|
||||
## 2. 任务清单
|
||||
|
||||
### 任务 10.1 — 添加 esp_emote_gfx 组件依赖
|
||||
|
||||
**文件**: `main/idf_component.yml`
|
||||
|
||||
**改动**: 在 dependencies 中追加:
|
||||
```yaml
|
||||
## Phase 10: 数字人模式 EAF UI(替代 LVGL)
|
||||
espressif2022/esp_emote_gfx: "~3.0.5"
|
||||
```
|
||||
|
||||
**验收**:
|
||||
- `idf.py reconfigure` 成功拉取组件
|
||||
- `managed_components/espressif2022__esp_emote_gfx/` 目录存在
|
||||
- 头文件 `core/gfx_emote.h` 可被引用
|
||||
|
||||
---
|
||||
|
||||
### 任务 10.2 — gfx_label 中文换行 PoC
|
||||
|
||||
**目的**: 验证最大风险点 —— gfx_label 是否支持中文自动换行 + 双行居中。**如失败需重新设计字幕方案**。
|
||||
|
||||
**手段**: 不动主代码,单独写小测试:
|
||||
1. 拉 `managed_components/espressif2022__esp_emote_gfx/test_apps/` 看官方测试
|
||||
2. 找 `gfx_label_set_long_mode` / `gfx_label_set_text_align` 等 API
|
||||
3. 短上下文 PoC:创建 gfx_label,文本 "(happy)今天天气真好这是一段需要换行测试的字幕",宽度 312px,看是否自动换行 + 居中
|
||||
|
||||
**验收**:
|
||||
- 找到 gfx_label 的换行 API(如 `gfx_label_set_long_mode(obj, GFX_LABEL_LONG_WRAP)`)
|
||||
- 文本能自动换行(至少 2 行)
|
||||
- 文本居中显示
|
||||
|
||||
**降级方案**(如换行不支持): 主代码端手动按字符宽度切分 + 创建两个 gfx_label 分别显示上下行
|
||||
|
||||
---
|
||||
|
||||
### 任务 10.3 — EAF Packer 转换 hiyori_m06.gif(PoC 阶段)
|
||||
|
||||
**前置**: ESP Emote GFX Packer 工具
|
||||
- 在线: `emote-gfx-gen-tool-dev.pages.dev`
|
||||
- 或 GitHub Releases 下载 CLI
|
||||
|
||||
**步骤**:
|
||||
1. 上传 `spiffs_image/hiyori_m06.gif`(PoC 先转一个)
|
||||
2. 配置: 4-bit palette + Heatshrink + 保留透明
|
||||
3. 下载 `hiyori_m06.eaf`,放到 `spiffs_image/`
|
||||
|
||||
**验收**:
|
||||
- `spiffs_image/hiyori_m06.eaf` 存在
|
||||
- 文件大小 < 原 gif(应该明显更小)
|
||||
- `head -c 4 hiyori_m06.eaf` 显示 magic `0x5A 0x5A`
|
||||
|
||||
---
|
||||
|
||||
### 任务 10.4 — 最小 EafDisplay PoC(只显示 hiyori_m06.eaf)
|
||||
|
||||
**新建**: `main/display/eaf_display.h` + `main/display/eaf_display.cc`
|
||||
|
||||
**最小内容**:
|
||||
```cpp
|
||||
class EafDisplay : public Display {
|
||||
public:
|
||||
EafDisplay(esp_lcd_panel_io_handle_t panel_io,
|
||||
esp_lcd_panel_handle_t panel,
|
||||
int width, int height,
|
||||
int offset_x, int offset_y,
|
||||
bool mirror_x, bool mirror_y, bool swap_xy);
|
||||
~EafDisplay() override;
|
||||
|
||||
// 暂时只实现最小接口
|
||||
void SetStatus(const char* status) override { /* TODO */ }
|
||||
void SetEmotion(const char* emotion) override; // 切换 EAF 动画
|
||||
|
||||
private:
|
||||
gfx_handle_t gfx_handle_;
|
||||
gfx_disp_t* disp_;
|
||||
gfx_obj_t* anim_obj_;
|
||||
uint8_t* eaf_data_ = nullptr;
|
||||
size_t eaf_size_ = 0;
|
||||
};
|
||||
```
|
||||
|
||||
构造时:
|
||||
1. `gfx_emote_init(&gfx_handle_)`
|
||||
2. `gfx_emote_add_disp(gfx_handle_, &disp_cfg)` —— disp_cfg 用 LCD panel 信息
|
||||
3. 加载 `/spiflash/hiyori_m06.eaf` 到 `eaf_data_`(PSRAM)
|
||||
4. `anim_obj_ = gfx_anim_create(disp_)`
|
||||
5. `gfx_anim_set_src(anim_obj_, eaf_data_, eaf_size_)`
|
||||
6. `gfx_anim_set_segment(anim_obj_, 0, total_frames - 1, 20, true)`(20fps 循环)
|
||||
7. `gfx_anim_start(anim_obj_)`
|
||||
|
||||
**验收**:
|
||||
- 编译通过
|
||||
- 烧录后 LCD 显示 hiyori_m06 动画循环播放
|
||||
- **听 RTC 对话扬声器卡顿是否改善**(核心 PoC 目的)
|
||||
|
||||
---
|
||||
|
||||
### 任务 10.5 — board 工厂条件编译
|
||||
|
||||
**文件**: `main/boards/movecall-moji-esp32s3/movecall_moji_esp32s3.cc`
|
||||
|
||||
**改动**: 找到 LcdDisplay 实例化位置,加 #ifdef 分支:
|
||||
```cpp
|
||||
#ifdef CONFIG_BAJI_BADGE_MODE
|
||||
display_ = new LcdDisplay(...);
|
||||
#else
|
||||
display_ = new EafDisplay(...);
|
||||
#endif
|
||||
```
|
||||
|
||||
**CMakeLists 改动**: 数字人模式下移除 `lcd_display.cc` 编译,加入 `eaf_display.cc`
|
||||
|
||||
**验收**:
|
||||
- 双向编译都通过(`=y` 和 `=n`)
|
||||
- 数字人模式固件不含 LVGL 符号(`nm` 检查无 lv_obj_create)
|
||||
|
||||
---
|
||||
|
||||
### 任务 10.6 — EafDisplay 完整接口实现
|
||||
|
||||
扩展 EafDisplay 实现剩余 `Display` 虚函数:
|
||||
|
||||
- `SetStatus(const char*)` — gfx_label 显示连接状态
|
||||
- `SetChatMessage(role, content)` — gfx_label 显示字幕(中文换行)
|
||||
- `SetEmotion(emotion)` — 切换 EAF 动画(emotion → eaf 路径映射)
|
||||
- `SetIcon(icon)` — gfx_img 显示叠加图标
|
||||
- `Lock / Unlock` — esp_emote_gfx 锁机制接驳
|
||||
- `Update()` — 强制刷新
|
||||
|
||||
复用 `ai_chat_ui.c` 的 emotion → gif 映射表逻辑,改为 emotion → eaf 路径。
|
||||
|
||||
**验收**:
|
||||
- 数字人模式下 application.cc 调用 Display::SetXxx 都正常工作
|
||||
- 编译通过
|
||||
|
||||
---
|
||||
|
||||
### 任务 10.7 — 字体接驳
|
||||
|
||||
**前置**: `main/fonts/font_puhui_20_4.c`(8.5MB CJK bitmap font)
|
||||
|
||||
**步骤**:
|
||||
1. 查 esp_emote_gfx 字体 API:`gfx_font_t*` / `gfx_label_set_font`
|
||||
2. 验证是否能直接传 `&font_puhui_20_4` (lv_font_t) → 应该不行,需要适配
|
||||
3. 如不兼容,用 esp_emote_gfx 工具重新生成同字符集 EAF font 资源
|
||||
|
||||
**降级方案**: 如果字体重新生成太麻烦,用内置默认字体或英文字体先 PoC
|
||||
|
||||
**验收**:
|
||||
- 字幕中文显示正常
|
||||
- 字幕单字符宽度合理(非乱码)
|
||||
|
||||
---
|
||||
|
||||
### 任务 10.8 — 触摸路径接驳
|
||||
|
||||
**前提确认**: 数字人模式是否需要触摸交互?
|
||||
- 看 [main/boards/movecall-moji-esp32s3/movecall_moji_esp32s3.cc](../../../../main/boards/movecall-moji-esp32s3/movecall_moji_esp32s3.cc) cst816s 是否在 `CONFIG_BAJI_BADGE_MODE=n` 下注册了 LVGL indev
|
||||
|
||||
**如不需要触摸**: 跳过此任务
|
||||
**如需要**: 用 `gfx_touch` 接驳 cst816s,重写 LVGL_INDEV 路径
|
||||
|
||||
---
|
||||
|
||||
### 任务 10.9 — 编译 + 烧录 + 显示效果验证
|
||||
|
||||
```bash
|
||||
source ~/esp/esp-idf/export.sh
|
||||
idf.py build
|
||||
idf.py flash monitor 2>&1 | tee .planning/milestones/digital_human_rtc/phases/phase_10_lvgl_to_eaf/phase_10_diag.log
|
||||
```
|
||||
|
||||
**测试场景**:
|
||||
1. 开机:是否正常显示数字人 GIF?
|
||||
2. RTC 连接:状态文字是否显示?
|
||||
3. 对话:字幕中文换行 + 居中是否正确?
|
||||
4. **听感**:扬声器卡顿是否消失?(**核心 PoC 验证目的**)
|
||||
5. 情绪切换:AI 说带情绪标签的话时 GIF 是否切换?
|
||||
|
||||
**指标对比**(vs Phase 8 baseline):
|
||||
|
||||
| 指标 | Phase 8 baseline | Phase 10 目标 |
|
||||
|---|---|---|
|
||||
| `idf.py size` DRAM | baseline | -30~40 KB |
|
||||
| `idf.py size` Flash | baseline | -80 KB |
|
||||
| `heap_caps_get_free_size(INTERNAL)` | baseline | +30 KB |
|
||||
| `heap_caps_get_free_size(SPIRAM)` | baseline | +80 KB |
|
||||
| 用户主观 RTC 对话听感 | 卡 | **不卡(核心目标)** |
|
||||
|
||||
---
|
||||
|
||||
### 任务 10.10 — 产出 IMPL_REPORT.md + commit
|
||||
|
||||
报告核心: PoC 显示效果 + 听感主观验证 + 资源对比,决定 Phase 11 是否启动。
|
||||
|
||||
---
|
||||
|
||||
## 3. 任务顺序
|
||||
|
||||
```
|
||||
10.1 加依赖 → 10.2 gfx_label 中文换行 PoC(关键风险点)
|
||||
↓ 通过
|
||||
10.3 EAF Packer 转 hiyori_m06 → 10.4 最小 EafDisplay PoC(一个 GIF 显示)→ 编译烧录看效果
|
||||
↓ 显示效果 OK
|
||||
10.5 board 工厂 → 10.6 EafDisplay 完整接口 → 10.7 字体 → 10.8 触摸 → 10.9 编译烧录验证 → 10.10 commit
|
||||
```
|
||||
|
||||
**关键 GO/NO-GO 决策点**:
|
||||
- 10.2 后: gfx_label 中文换行不支持 → 评估降级方案
|
||||
- 10.4 后: 显示效果 OK + 听感改善 → 继续完整切换;显示坏 → 中止并回退
|
||||
|
||||
---
|
||||
|
||||
## 4. 风险与回滚
|
||||
|
||||
| 风险 | 缓解 |
|
||||
|---|---|
|
||||
| esp_emote_gfx 文档不全 / API 不稳定 | 参考 `managed_components/.../test_apps/` 官方示例 |
|
||||
| 字体兼容失败 | 降级英文字体先 PoC,字幕中文文档化为已知限制 |
|
||||
| 中文换行不支持 | 手动切分两个 gfx_label |
|
||||
| 显示效果坏 | 任何阶段 commit 前都可 `git reset --hard HEAD` 回滚 |
|
||||
| 听感未改善 | 数据证明假设 1 错误,需重新审视 → 继续 Phase 11 直接 WiFi 缓冲扩容验证假设 2 |
|
||||
|
||||
---
|
||||
|
||||
## 5. Phase 10 完成验收清单
|
||||
|
||||
- [ ] esp_emote_gfx 组件已添加且可拉取
|
||||
- [ ] gfx_label 中文换行已验证(或降级方案就绪)
|
||||
- [ ] 至少一个 EAF 动画文件已生成且能在设备显示
|
||||
- [ ] EafDisplay 实现 Display 全部虚函数
|
||||
- [ ] CMakeLists 双轨编译双向通过
|
||||
- [ ] 烧录数字人模式 + RTC 对话 5 分钟
|
||||
- [ ] 听感主观:扬声器卡顿是否消失(PoC 核心目标)
|
||||
- [ ] 资源对比 `idf.py size` + heap 数据已记录
|
||||
- [ ] IMPL_REPORT.md 给出 Phase 11 GO/NO-GO 决策
|
||||
|
||||
---
|
||||
|
||||
## 6. Phase 10 不做的事
|
||||
|
||||
- ❌ 不动吧唧模式 LVGL UI(`CONFIG_BAJI_BADGE_MODE=y` 路径完全不变)
|
||||
- ❌ 不删除 LVGL/lvgl_port 组件(吧唧模式仍需要)
|
||||
- ❌ 不动 audio_codec / RTC 协议(这是上游问题)
|
||||
- ❌ 不做内存优化和 WiFi 扩容(留给 Phase 11)
|
||||
- ❌ 不动 ScreenUpdate / ui/screens(吧唧模式专属)
|
||||
@ -60,7 +60,7 @@ dependencies:
|
||||
type: service
|
||||
version: 0.5.3
|
||||
espressif/dl_fft:
|
||||
component_hash: 7dadbd644c0d7ba4733cc3726ec4cff6edf27b043725e1115861dec1609a3d28
|
||||
component_hash: ced3cf28cc70452b7859c06f4e5059215167254a2047e34c893d6f501ccd6ea2
|
||||
dependencies:
|
||||
- name: idf
|
||||
require: private
|
||||
@ -68,7 +68,7 @@ dependencies:
|
||||
source:
|
||||
registry_url: https://components.espressif.com
|
||||
type: service
|
||||
version: 0.3.1
|
||||
version: 0.4.0
|
||||
espressif/esp-dsp:
|
||||
component_hash: 619639efc18cfa361a9e423739b9b0ffc14991effc6c027f955c2f2c3bf1754b
|
||||
dependencies:
|
||||
@ -169,6 +169,35 @@ dependencies:
|
||||
registry_url: https://components.espressif.com/
|
||||
type: service
|
||||
version: 2.5.0
|
||||
espressif/esp_new_jpeg:
|
||||
component_hash: 98823384f51ca298e2c9bebacd1c629148e528ed0902d18b16556df151519e68
|
||||
dependencies: []
|
||||
source:
|
||||
registry_url: https://components.espressif.com
|
||||
type: service
|
||||
targets:
|
||||
- esp32
|
||||
- esp32s2
|
||||
- esp32s3
|
||||
- esp32s31
|
||||
- esp32p4
|
||||
- esp32c2
|
||||
- esp32c3
|
||||
- esp32c5
|
||||
- esp32c6
|
||||
- esp32c61
|
||||
- esp32h4
|
||||
version: 1.0.1
|
||||
espressif/freetype:
|
||||
component_hash: a4169cdd22b3572342b2d640d7082405b8895e3214539283601c03412589b65d
|
||||
dependencies:
|
||||
- name: idf
|
||||
require: private
|
||||
version: '>=4.4'
|
||||
source:
|
||||
registry_url: https://components.espressif.com
|
||||
type: service
|
||||
version: 2.14.2
|
||||
espressif/knob:
|
||||
component_hash: a389d980693ad195b2160de22a72f3391694230188ab16b8f3c7ec4410a7c417
|
||||
dependencies:
|
||||
@ -193,10 +222,54 @@ dependencies:
|
||||
registry_url: https://components.espressif.com/
|
||||
type: service
|
||||
version: 2.5.5
|
||||
espressif2022/esp_emote_gfx:
|
||||
component_hash: a06a58c74f7deb4186460f27f5e6db52fda4c254d7e03c3e05e7987aaf73de1a
|
||||
dependencies:
|
||||
- name: espressif/cmake_utilities
|
||||
registry_url: https://components.espressif.com
|
||||
require: private
|
||||
version: 0.*
|
||||
- name: espressif/esp_lcd_touch
|
||||
registry_url: https://components.espressif.com
|
||||
require: public
|
||||
version: '>=1.0'
|
||||
- name: espressif/esp_new_jpeg
|
||||
registry_url: https://components.espressif.com
|
||||
require: public
|
||||
version: 1.*
|
||||
- name: espressif/freetype
|
||||
registry_url: https://components.espressif.com
|
||||
require: private
|
||||
version: 2.*
|
||||
- name: idf
|
||||
require: private
|
||||
version: '>=5.0'
|
||||
- name: laride/heatshrink
|
||||
registry_url: https://components.espressif.com
|
||||
require: private
|
||||
version: ^0.4.1
|
||||
- name: lvgl/lvgl
|
||||
registry_url: https://components.espressif.com
|
||||
require: public
|
||||
version: '*'
|
||||
source:
|
||||
registry_url: https://components.espressif.com/
|
||||
type: service
|
||||
version: 3.0.5
|
||||
idf:
|
||||
source:
|
||||
type: idf
|
||||
version: 5.4.2
|
||||
laride/heatshrink:
|
||||
component_hash: 0828b0fea3f0754f8404a5279e883c52fe27494bbe1762e38d5cd96c99229e47
|
||||
dependencies:
|
||||
- name: idf
|
||||
require: private
|
||||
version: '>=5'
|
||||
source:
|
||||
registry_url: https://components.espressif.com
|
||||
type: service
|
||||
version: 0.4.1
|
||||
lvgl/lvgl:
|
||||
component_hash: 948bff879a345149b83065535bbc4a026ce9f47498a22881e432a264b9098015
|
||||
dependencies: []
|
||||
@ -219,8 +292,9 @@ direct_dependencies:
|
||||
- espressif/esp_lvgl_port
|
||||
- espressif/knob
|
||||
- espressif/led_strip
|
||||
- espressif2022/esp_emote_gfx
|
||||
- idf
|
||||
- lvgl/lvgl
|
||||
manifest_hash: 567fb06fed7b7df9c9bbd2a0615df5b600cd13d08df4b38a71d28971feaec792
|
||||
manifest_hash: 90544e3d787e63c288feeb33cf16100755d3ed90c47270526fd2fb5754eba469
|
||||
target: esp32s3
|
||||
version: 2.0.0
|
||||
|
||||
@ -17,6 +17,8 @@ dependencies:
|
||||
esp_lcd_touch_cst816s: "1.1.0"
|
||||
## JPEG 解码(dzbj 图片显示)
|
||||
esp_jpeg: "*"
|
||||
## Phase 10: 数字人模式 UI 框架(替代 LVGL,仅 CONFIG_BAJI_BADGE_MODE=n 时使用)
|
||||
espressif2022/esp_emote_gfx: "~3.0.5"
|
||||
## Required IDF version
|
||||
idf:
|
||||
version: ">=5.3"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user