chore: archive diagnostics and project documentation

This commit is contained in:
chenjintao
2026-09-01 14:25:08 +08:00
parent 3912843869
commit c4a1da9d8f
35 changed files with 19843 additions and 3260 deletions
@@ -0,0 +1,810 @@
# LittleFS 资源构建、烧录与挂载流程
本文说明项目中的 LCD 图片和提示音资源如何从源码目录进入设备 Flash,以及设备运行时如何通过 LittleFS 访问这些资源。同时给出新增、替换和排查资源问题的完整操作步骤。
## 1. 先建立整体认识
项目不会把每一张图片分别写入 Flash,也不会在设备启动时从电脑复制资源。
实际流程是:构建系统先将需要的资源整理成一个临时目录,再将整个目录制作成 LittleFS 文件系统镜像 `gifpack.bin`,最后把这个镜像作为一个独立分区烧录到设备。设备启动后,将该分区挂载到 `/littlefs`,应用程序便可以像读取普通文件一样读取图片和音频。
```text
源码资源
components/platform/assets/
|
| CMake 选择并复制资源
v
构建临时目录
build/esp-idf/platform/asset_image/
|
| 制作 LittleFS 文件系统镜像
v
build/gifpack.bin
|
| idf.py flash
v
设备 Flash 中的 gifpack 分区
|
| esp_vfs_littlefs_register()
v
设备文件路径 /littlefs/
```
需要区分以下三个概念:
| 名称 | 所在位置 | 作用 |
| --- | --- | --- |
| 源码资源目录 | `components/platform/assets/` | Git 管理的原始待烧录资源 |
| 构建临时目录 | `build/esp-idf/platform/asset_image/` | 生成镜像前整理出来的目录树 |
| LittleFS 镜像 | `build/gifpack.bin` | 最终烧入设备 `gifpack` 分区的文件系统镜像 |
临时目录只存在于开发电脑上。设备真正挂载的是 Flash 中的 `gifpack.bin` 内容,不是电脑上的 `build` 目录。
## 2. 相关文件在哪里
| 文件或目录 | 作用 |
| --- | --- |
| `components/platform/assets/lcd/` | LCD 的 `.rle` 图片资源 |
| `components/platform/assets/audio_prompts/` | `.opus` 提示音资源 |
| `components/platform/CMakeLists.txt` | 选择资源、整理临时目录、生成 LittleFS 镜像 |
| `components/platform/idf_component.yml` | 声明 `joltwallet/littlefs` 组件依赖 |
| `managed_components/joltwallet__littlefs/project_include.cmake` | 定义 `littlefs_create_partition_image()` |
| `16m.csv` | 定义 `gifpack` 分区的类型、大小和位置 |
| `components/platform/src/platform_behavior_stubs.c` | 运行时挂载 `gifpack` 分区 |
| `components/platform/src/lcd_direct_assets.c` | 将 LCD 资源名映射到 `/littlefs/lcd/*.rle` |
| `components/platform/src/lcd_direct.c` | 打开、读取、解码并绘制 RLE 文件 |
| `components/domain/src/runtime/domain_runtime_adapters.c` | 将业务表达式映射为 LCD 资源播放请求 |
## 3. CMake 如何选中资源
### 3.1 定义资源目录
`components/platform/CMakeLists.txt` 中定义:
```cmake
set(platform_audio_prompt_src_dir "${CMAKE_CURRENT_SOURCE_DIR}/assets/audio_prompts")
set(platform_lcd_asset_src_dir "${CMAKE_CURRENT_SOURCE_DIR}/assets/lcd")
set(platform_asset_image_dir "${CMAKE_CURRENT_BINARY_DIR}/asset_image")
```
在当前组件中,这些变量最终大致对应:
```text
platform_audio_prompt_src_dir
-> components/platform/assets/audio_prompts
platform_lcd_asset_src_dir
-> components/platform/assets/lcd
platform_asset_image_dir
-> build/esp-idf/platform/asset_image
```
### 3.2 LCD 使用显式白名单
LCD 资源由 `platform_lcd_flash_assets` 明确列出:
```cmake
set(platform_lcd_flash_assets
buyunqian1
jichubiaoqing0
jichubiaoqing1
jichubiaoqing2
muyu0
muyu1
muyu2
# ...
)
```
列表中不写 `.rle` 后缀。后续循环会自动拼出完整文件名:
```cmake
"${platform_lcd_asset_src_dir}/${lcd_rle_asset}.rle"
```
例如白名单中的:
```text
jichubiaoqing0
```
最终对应:
```text
components/platform/assets/lcd/jichubiaoqing0.rle
```
因此,把新 `.rle` 文件放进 `assets/lcd/` 并不代表它会自动进入 Flash。只有加入 `platform_lcd_flash_assets` 的 LCD 文件才会进入 `gifpack.bin`。
### 3.3 音频使用自动扫描
音频资源采用另一种策略:
```cmake
file(GLOB_RECURSE platform_audio_prompt_files CONFIGURE_DEPENDS
"${platform_audio_prompt_src_dir}/*.opus"
)
```
这会递归选中 `assets/audio_prompts/` 下的全部 `.opus` 文件。因此:
- 新增 LCD 文件时,需要手动加入 CMake 白名单;
- 新增 `.opus` 文件时,一般不需要修改资源白名单。
## 4. CMake 如何形成临时目录
构建时,CMake 会先清理旧的临时目录:
```cmake
COMMAND ${CMAKE_COMMAND} -E rm -rf "${platform_asset_image_dir}"
COMMAND ${CMAKE_COMMAND} -E make_directory "${platform_asset_image_dir}"
```
这样可以避免已经从白名单删除的旧资源继续残留在镜像中。
随后,LCD 文件被复制到临时目录的 `lcd/` 子目录:
```cmake
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${platform_lcd_asset_src_dir}/${lcd_rle_asset}.rle"
"${platform_asset_image_dir}/lcd/${lcd_rle_asset}.rle"
```
音频文件被复制到 `audio_prompts/` 子目录。完成后,临时目录大致如下:
```text
build/esp-idf/platform/asset_image/
├── audio_prompts/
│ ├── boot.opus
│ ├── network_connect_success.opus
│ └── ...
└── lcd/
├── jichubiaoqing0.rle
├── jichubiaoqing1.rle
├── muyu0.rle
└── ...
```
这个目录结构会原样成为 LittleFS 镜像内部的目录结构。
资源文件还被加入 `DEPENDS`:
```cmake
DEPENDS
${platform_audio_prompt_files}
${platform_lcd_flash_asset_files}
```
因此,白名单内的资源发生变化时,构建系统能够重新整理目录并生成镜像。
## 5. `littlefs_create_partition_image()` 从哪里来
项目在 `components/platform/idf_component.yml` 中声明:
```yaml
dependencies:
joltwallet/littlefs: 1.20.4
```
ESP-IDF Component Manager 因此引入:
```text
managed_components/joltwallet__littlefs/
```
这个组件包含特殊文件:
```text
managed_components/joltwallet__littlefs/project_include.cmake
```
ESP-IDF 构建系统会自动加载各组件的 `project_include.cmake`。该文件定义了:
```cmake
function(littlefs_create_partition_image partition base_dir)
# ...
endfunction()
```
所以 `littlefs_create_partition_image()`:
- 不是标准 CMake 内置函数;
- 也不是本项目自行定义的普通函数;
- 它是 `joltwallet/littlefs` 组件提供并由 ESP-IDF 自动导入的构建扩展。
## 6. 如何生成 `gifpack.bin`
项目调用:
```cmake
littlefs_create_partition_image(
gifpack
"${platform_asset_image_dir}"
FLASH_IN_PROJECT
DEPENDS "${platform_asset_stage_stamp}"
)
```
参数含义如下:
| 参数 | 含义 |
| --- | --- |
| `gifpack` | 要匹配的分区名称 |
| `platform_asset_image_dir` | 要制作成文件系统镜像的目录 |
| `FLASH_IN_PROJECT` | 将镜像加入项目的完整 `flash` 目标 |
| `DEPENDS` | 确保资源整理完成后再生成镜像 |
该函数会根据分区名查询分区表:
```cmake
partition_table_get_partition_info(size "--partition-name ${partition}" "size")
partition_table_get_partition_info(offset "--partition-name ${partition}" "offset")
```
随后调用 `littlefs-python`,大致执行:
```text
littlefs-python create \
build/esp-idf/platform/asset_image \
build/gifpack.bin \
--fs-size=<gifpack分区大小> \
--block-size=4096
```
得到:
```text
build/gifpack.bin
```
`gifpack.bin` 不是简单压缩包,而是一个包含目录、文件名、文件内容和文件系统元数据的完整 LittleFS 镜像。
## 7. 分区表如何决定烧录位置
项目通过 `sdkconfig.defaults` 选择自定义分区表:
```text
CONFIG_PARTITION_TABLE_CUSTOM=y
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="16m.csv"
```
`16m.csv` 中定义:
```csv
gifpack, data, littlefs, , 0xa80000,
```
字段含义:
| 字段 | 当前值 | 含义 |
| --- | --- | --- |
| Name | `gifpack` | 分区标签,构建和挂载都依赖它 |
| Type | `data` | 数据分区 |
| SubType | `littlefs` | LittleFS 文件系统分区 |
| Offset | 空 | 由 ESP-IDF 根据前面的分区自动计算 |
| Size | `0xa80000` | 分区大小,约 10.5 MiB |
当前生成结果中,`gifpack` 的起始地址是 `0x10000`。由于分区 Offset 没有写死,如果前面的分区布局发生变化,实际地址也可能变化;应以当前构建生成的分区表和 `build/flasher_args.json` 为准。
下面三处名称必须一致:
```text
CMake:
littlefs_create_partition_image(gifpack ...)
分区表:
gifpack, data, littlefs, ...
运行时:
.partition_label = "gifpack"
```
任何一处名称不一致,都可能导致镜像生成失败、烧错位置或运行时找不到分区。
## 8. `FLASH_IN_PROJECT` 如何让镜像随项目烧录
`FLASH_IN_PROJECT` 会让 LittleFS 组件执行类似以下注册:
```cmake
esptool_py_flash_target_image(
flash
"${partition}"
"${offset}"
"${image_file}"
)
```
它的含义是:把 `gifpack.bin` 加入 ESP-IDF 的完整 `flash` 目标。
当前 `build/flasher_args.json` 中可看到类似内容:
```json
"0x10000": "gifpack.bin"
```
所以执行:
```bash
idf.py flash
```
时,ESP-IDF 会分别烧录 bootloader、分区表、应用固件和 `gifpack.bin`。`gifpack.bin` 并没有编入应用程序 `.bin`,它始终是一个独立的分区镜像。
需要特别区分:
| 命令 | 对资源分区的影响 |
| --- | --- |
| `idf.py build` | 生成或更新 `gifpack.bin`,不写设备 |
| `idf.py flash` | 按完整烧录清单写入 `gifpack.bin` |
| `idf.py app-flash` | 通常只更新应用分区,不更新 `gifpack` |
| 普通应用 OTA | 通常只更新 OTA 应用分区,不更新 `gifpack` |
因此,修改资源后只执行 `app-flash` 或普通应用 OTA,设备中的资源可能仍然是旧版本。
## 9. 设备运行时如何挂载
挂载函数位于 `components/platform/src/platform_behavior_stubs.c`:
```c
#define VOICE_PROMPT_LITTLEFS_BASE_PATH "/littlefs"
#define VOICE_PROMPT_LITTLEFS_PARTITION "gifpack"
esp_vfs_littlefs_conf_t conf = {
.base_path = VOICE_PROMPT_LITTLEFS_BASE_PATH,
.partition_label = VOICE_PROMPT_LITTLEFS_PARTITION,
.partition = NULL,
.format_if_mount_failed = false,
.dont_mount = false,
};
esp_err_t err = esp_vfs_littlefs_register(&conf);
```
### 9.1 `partition_label = "gifpack"`
LittleFS 驱动根据标签在运行时分区表中查找 `gifpack`,得到该分区的实际 Flash 地址和大小。
### 9.2 `partition = NULL`
调用方没有直接传入 `esp_partition_t`,由 LittleFS 驱动根据 `partition_label` 自动查找分区。
### 9.3 `base_path = "/littlefs"`
将 LittleFS 注册到 ESP-IDF VFS 的 `/littlefs` 路径。挂载成功后,可以使用标准文件 API:
```c
open("/littlefs/lcd/jichubiaoqing0.rle", O_RDONLY);
fopen("/littlefs/audio_prompts/boot.opus", "rb");
```
### 9.4 `format_if_mount_failed = false`
挂载失败时不格式化分区。这对预置资源分区非常重要,因为格式化会清空已经烧录的图片和音频。
### 9.5 `dont_mount = false`
注册后立即执行挂载,而不是只注册驱动。
### 9.6 VFS 路径如何对应到 LittleFS 内部路径
临时目录中的:
```text
asset_image/lcd/jichubiaoqing0.rle
```
进入 LittleFS 后对应:
```text
/lcd/jichubiaoqing0.rle
```
挂载到 `/littlefs` 后,应用程序看到:
```text
/littlefs/lcd/jichubiaoqing0.rle
```
应用调用 `open()` 时,ESP-IDF VFS 识别 `/littlefs` 前缀,并将剩余的 `/lcd/jichubiaoqing0.rle` 交给 LittleFS 驱动读取。
## 10. 当前启动时机和隐式依赖
当前 `platform_mount_littlefs()` 不是由 `platform_init()` 直接调用,而是在 `voice_prompt_play_file()` 播放提示音之前调用:
```c
esp_err_t mount_err = platform_mount_littlefs();
```
当前启动配置启用了 `boot.opus`,启动顺序大致是:
```text
准备运行时存储
-> 初始化音频
-> 播放 boot.opus
-> platform_mount_littlefs()
-> 挂载 gifpack 到 /littlefs
-> 初始化 LCD
-> LCD 渲染线程读取 /littlefs/lcd/*.rle
```
这意味着当前 LCD 成功读取资源,隐式依赖“开机音流程已经触发 LittleFS 挂载”。如果以后禁用开机音,或者在调用 `voice_prompt_play_file()` 之前就失败,LCD 可能在尚未挂载时访问 `/littlefs/lcd/`。
更稳妥的长期方案是把 `platform_mount_littlefs()` 作为独立启动阶段,在音频和 LCD 使用资源之前显式调用。修改启动流程前,应保持现有分层规则,并由 `app_composition` 编排启动顺序。
## 11. LCD RLE 文件格式
当前 `lcd_direct` 驱动读取的 `.rle` 是原始二元组序列:
```c
typedef struct {
uint16_t color;
uint16_t count;
} lcd_direct_rle_run_t;
```
每个 run 占 4 字节:
```text
2 字节 RGB565 color,little-endian
2 字节重复像素数 count,little-endian
```
全屏图片解压后的像素总数应与屏幕绘制区域匹配。当前横屏画布通常为:
```text
320 x 240 = 76800 像素
```
图标资源则应与 `lcd_direct_icon_t` 中配置的宽高匹配。
### 11.1 `run_count` 必须与文件一致
`components/platform/src/lcd_direct_assets.c` 不仅保存文件路径,还保存 `run_count`:
```c
{
.path = LCD_ASSET_BASE_PATH "jichubiaoqing0.rle",
.run_count = 5411U,
},
```
对于当前原始格式:
```text
run_count = 文件字节数 / 4
```
替换资源后,即使文件名没有变化,只要编码后的 run 数发生变化,就必须同步更新 `run_count`。否则驱动可能少读、读到文件末尾失败或显示异常。
可以用下面的方法检查:
```bash
python3 -c 'from pathlib import Path; p=Path("components/platform/assets/lcd/jichubiaoqing0.rle"); print(p.stat().st_size // 4)'
```
还应确认文件大小能被 4 整除。
### 11.2 不要误用 `tools/png_to_rle.py`
当前仓库的 `tools/png_to_rle.py` 使用 Pillow 生成标准 TGA RLE 文件;而 `lcd_direct.c` 读取的是上面描述的原始 `color + count` 二元组格式。
两种格式不兼容。不要直接用 `tools/png_to_rle.py` 的输出替换 `components/platform/assets/lcd/*.rle`,除非先统一转换工具和驱动格式。
`tools/extract_lcd_assets.py` 能写出当前原始二元组格式,但其中包含旧工程的固定 Windows 路径和固定资源映射,使用前必须检查和调整,不能假设它在所有开发环境中可直接运行。
## 12. 替换现有 LCD 资源
适用场景:资源名、帧数和业务行为不变,只替换画面内容。
以替换 `jichubiaoqing0.rle` 为例:
1. 使用与当前驱动兼容的原始 RLE 转换流程生成新文件。
2. 确认解压像素总数与目标画布一致。
3. 用新文件替换:
```text
components/platform/assets/lcd/jichubiaoqing0.rle
```
4. 计算新文件的 `run_count`。
5. 更新 `components/platform/src/lcd_direct_assets.c` 中对应条目的 `run_count`。
6. 因为文件名没有变化且已经在 CMake 白名单中,一般不需要修改 `platform_lcd_flash_assets`。
7. 执行完整构建和烧录:
```bash
eim run "idf.py build" v5.5.4
eim run "idf.py flash" v5.5.4
```
8. 在串口日志中确认 LittleFS 挂载成功,并检查 LCD 是否有 `open` 或 `read` 失败日志。
如果当前环境没有 `eim`,按照项目 `AGENTS.md` 中规定的 ESP-IDF v5.5.4 激活流程执行相同命令。
## 13. 给已有动画新增一帧
适用场景:例如已有 `muyu0.rle`、`muyu1.rle`、`muyu2.rle`,现在增加 `muyu3.rle`。
### 第一步:添加文件
放到:
```text
components/platform/assets/lcd/muyu3.rle
```
### 第二步:加入 CMake 白名单
在 `components/platform/CMakeLists.txt` 的 `platform_lcd_flash_assets` 中加入:
```cmake
muyu3
```
否则文件不会进入 `gifpack.bin`。
### 第三步:加入平台资源映射
在 `components/platform/src/lcd_direct_assets.c` 的对应数组中增加:
```c
{
.path = LCD_ASSET_BASE_PATH "muyu3.rle",
.run_count = <实际 run 数>,
},
```
平台通过数组下标解释 `frame_index`,所以新条目在数组中的位置就是它的帧索引。
### 第四步:确认业务播放顺序
如果希望业务流程实际播放新帧,还需要检查 `components/domain/src/runtime/domain_runtime_adapters.c` 中的帧顺序,例如:
```c
static const uint8_t s_muyu_frame_order[] = {0, 1, 2, 3, 2, 1};
```
只增加平台资源映射但不修改播放顺序,新帧可能已经烧入设备,却永远不会被请求。
### 第五步:完整构建并烧录
```bash
eim run "idf.py build" v5.5.4
eim run "idf.py flash" v5.5.4
```
## 14. 新增一组 LCD 资源
适用场景:增加一个新的动画组,例如:
```text
happy0.rle
happy1.rle
happy2.rle
```
需要同时完成以下工作:
1. 将所有文件放入 `components/platform/assets/lcd/`。
2. 将 `happy0`、`happy1`、`happy2` 加入 `platform_lcd_flash_assets`。
3. 在 `components/platform/src/lcd_direct_assets.c` 中建立资源数组,填写每帧路径和 `run_count`。
4. 在 `lcd_direct_asset_frame()` 中增加资源名到数组的查找映射,使 `asset_dir = "happy"` 能找到对应帧。
5. 如果这是新的业务表达式,按照 `docs/lcd-expressions.md` 的分层要求:
- 在 `domain` 公共契约中增加语义表达式 ID;
- 在 `domain_runtime_adapters.c` 中映射到 `asset_dir = "happy"`;
- 由 `control_plane` 决定何时请求该表达式;
- 不要让 `control_plane` 直接调用 `platform_lcd_*`。
6. 构建并完整烧录。
7. 在硬件上验证帧序、帧间隔、中断优先级和播放结束后的返回行为。
这里涉及两个彼此独立的问题:
```text
资源是否进入 Flash
-> 由 CMake 白名单决定
资源是否会被业务播放
-> 由 platform 资源映射和 domain 表达式映射决定
```
文件成功烧入 Flash,不代表业务代码一定能找到或播放它。
## 15. 新增或替换音频资源
### 15.1 替换已有音频
如果文件名和调用路径不变:
1. 替换 `components/platform/assets/audio_prompts/` 下对应的 `.opus` 文件。
2. 执行 `idf.py build` 重新生成 `gifpack.bin`。
3. 执行完整 `idf.py flash` 更新资源分区。
一般不需要修改 CMake,因为 `.opus` 文件会被自动扫描。
### 15.2 新增音频
1. 将 `.opus` 文件加入 `components/platform/assets/audio_prompts/`。
2. 确认播放代码引用的路径正确:
```text
/littlefs/audio_prompts/<文件名>.opus
```
3. 如果传给 `voice_prompt_play_file()` 的是相对文件名,它会自动补上 `/littlefs/audio_prompts/`。
4. 完整构建并烧录。
## 16. 推荐验证流程
资源修改后,建议按以下顺序验证。
### 16.1 检查源码资源
确认:
- 文件名和大小写正确;
- LCD 文件已加入白名单;
- LCD `run_count` 与文件一致;
- 资源映射路径与文件名一致。
### 16.2 构建镜像
```bash
eim run "idf.py build" v5.5.4
```
检查是否生成:
```text
build/gifpack.bin
```
也可检查临时目录中是否存在目标文件:
```text
build/esp-idf/platform/asset_image/lcd/<资源名>.rle
build/esp-idf/platform/asset_image/audio_prompts/<资源名>.opus
```
### 16.3 检查烧录清单
检查 `build/flasher_args.json` 中是否包含:
```json
"gifpack": {
"offset": "...",
"file": "gifpack.bin"
}
```
不要把 `0x10000` 永久当作固定值,应以当前分区表生成结果为准。
### 16.4 完整烧录
```bash
eim run "idf.py flash" v5.5.4
```
不要只执行 `app-flash`。
### 16.5 检查运行日志
挂载成功时应出现类似日志:
```text
LittleFS mounted at /littlefs, used=... total=...
```
LCD 资源异常时重点关注:
```text
open LCD asset failed
read LCD asset failed
LCD presentation draw failed
```
音频异常时重点关注:
```text
Mount LittleFS failed
Open prompt failed
```
## 17. 常见问题排查
### 17.1 源码目录里有文件,但设备提示找不到
按顺序检查:
1. LCD 文件是否加入 `platform_lcd_flash_assets`;
2. 是否重新执行了 `idf.py build`;
3. 是否执行了完整 `idf.py flash`;
4. `lcd_direct_assets.c` 中路径是否完全一致;
5. `/littlefs` 是否已经挂载。
### 17.2 替换图片后还是显示旧图
最常见原因是只执行了 `app-flash` 或 OTA,只更新了应用分区,没有更新 `gifpack`。
重新执行完整烧录:
```bash
eim run "idf.py flash" v5.5.4
```
### 17.3 能打开文件,但读取失败
检查:
- `run_count` 是否与新文件一致;
- 文件大小是否能被 4 整除;
- 文件是否为当前驱动要求的原始二元组 RLE;
- 是否错误使用了 TGA RLE 文件;
- RLE 解压后的像素数量是否匹配目标画布。
### 17.4 构建时报分区空间不足
说明临时目录中的总资源超过 `gifpack` 容量。处理方式包括:
- 删除未使用资源并从白名单移除;
- 优化资源编码;
- 在确认整个 Flash 布局允许后调整 `16m.csv`;
- 调整分区表时同时检查 OTA、模型和日志分区是否仍有足够空间。
不要只增大 `gifpack` 而忽略后续分区的地址和总 Flash 容量。
### 17.5 挂载失败
检查:
1. 是否已经完整烧录 `gifpack.bin`;
2. CMake、分区表、运行时使用的分区名是否都是 `gifpack`;
3. 分区表是否和当前固件一起烧录;
4. 镜像大小是否与当前分区表一致;
5. 是否使用旧构建目录生成的镜像;
6. 必要时执行 `idf.py fullclean` 后重新构建和完整烧录。
由于资源分区设置了 `format_if_mount_failed = false`,挂载失败不会自动格式化,这可以避免资源被意外清空。
## 18. 修改资源时的最终检查清单
### 只替换现有 LCD 文件
- [ ] 替换 `components/platform/assets/lcd/<name>.rle`
- [ ] 确认文件格式为原始 `uint16_t color + uint16_t count`
- [ ] 更新 `lcd_direct_assets.c` 中的 `run_count`
- [ ] 执行 `idf.py build`
- [ ] 执行完整 `idf.py flash`
- [ ] 检查挂载和 LCD 读取日志
### 新增 LCD 帧或资源组
- [ ] 添加 `.rle` 文件
- [ ] 加入 `platform_lcd_flash_assets`
- [ ] 在 `lcd_direct_assets.c` 中增加路径和 `run_count`
- [ ] 在 `lcd_direct_asset_frame()` 中增加或确认名称映射
- [ ] 在 domain 层配置播放顺序或表达式映射
- [ ] 执行完整构建和烧录
- [ ] 在硬件上验证实际播放
### 替换或新增音频
- [ ] 添加或替换 `.opus` 文件
- [ ] 确认调用路径正确
- [ ] 执行 `idf.py build`
- [ ] 执行完整 `idf.py flash`
- [ ] 检查提示音打开和播放日志
## 19. 一句话总结
资源交付链路可以概括为:
```text
源码资源
-> CMake 选择并整理目录
-> LittleFS 组件生成 gifpack.bin
-> ESP-IDF 根据分区表烧入 gifpack 分区
-> 设备把 gifpack 挂载为 /littlefs
-> LCD 和音频通过标准文件 API 读取资源
```
新增资源时要同时确认“是否进入 `gifpack.bin`”和“运行时代码是否会找到并使用它”;替换资源时则要特别注意更新 LCD `run_count`,并执行完整 `idf.py flash` 更新独立的资源分区。