Files
lucky-cat/docs/knowledge vault/littlefs-resource-build-flash-mount.md
T

813 lines
23 KiB
Markdown
Raw 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.
# LittleFS 资源构建、烧录与挂载流程
> 历史流程:当前固件已将 LCD 图片和提示音嵌入 OTA 应用镜像。下文的“当前”、命令和故障排查仅针对旧版 `gifpack` LittleFS,不适用于现行工程;请使用 [应用内嵌素材使用与添加指南](../应用内嵌素材使用与添加指南.md)。
本文说明项目中的 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` 更新独立的资源分区。