feat(assets): embed LCD and audio resources in OTA app

This commit is contained in:
chenjintao
2026-10-08 16:58:33 +08:00
parent 3160a33f50
commit 7bc9a3dc76
43 changed files with 443 additions and 243 deletions
+2 -2
View File
@@ -52,7 +52,7 @@ flowchart LR
LCD["ST7789 direct LCD backend"]
Audio["ES8311 audio pipeline"]
Network["Wi-Fi, BLE provisioning, HTTP, WebSocket"]
Storage["NVS, LittleFS"]
Storage["NVS, embedded assets, model partition"]
GPIO["GPIO, stepper, console UART"]
end
@@ -187,7 +187,7 @@ flowchart LR
PlatformReq["platform_lcd_request_presentation()"]
Pending["lcd_direct pending request"]
Render["cp_lcd_render loop"]
Draw["decode PNG asset frame\nfrom /littlefs/lcd"]
Draw["decode PNG asset frame\nfrom embedded app image"]
Panel["ST7789 panel"]
CP --> DomainReq --> Resolve --> Catalog --> PlatformReq --> Pending --> Render --> Draw --> Panel
@@ -273,7 +273,7 @@ key: last_shutdown
紧急确认次数:2次
确认期间采样间隔:1秒
实体按键唤醒 GPIO:-1(关闭)
关机提示音:/littlefs/audio_prompts/shutdown_power_empty.opus
关机提示音:audio_prompts/shutdown_power_empty.opus
充电唤醒 GPIO:GPIO10,低电平有效
```
@@ -326,7 +326,7 @@ key: last_shutdown
因为要求“播完才能关机”,提示文件缺失、损坏、Codec 初始化失败或播放通路持续故障时,软件会每秒重试而不会休眠。这符合当前产品决策,但可能导致电池最终依靠保护板断电。
量产前必须验证 LittleFS 中始终包含 `shutdown_power_empty.opus`,并验证关机场景下音频通路可初始化。
量产前必须验证应用镜像中包含 `audio_prompts/shutdown_power_empty.opus`,并验证关机场景下音频通路可初始化。
### 5.4 GPIO10 只能在实际充电时可靠唤醒
@@ -336,7 +336,7 @@ TP4057 状态脚低电平表示正在充电,高电平可能表示充满或未
### 5.5 Deep Sleep 唤醒是新的软件生命周期
唤醒后会重新执行 Bootloader 和 `app_main()`,运行内存、任务状态和普通全局变量不会保留。需要跨休眠保留的数据必须写入 NVS、LittleFS 或 RTC 内存。
唤醒后会重新执行 Bootloader 和 `app_main()`,运行内存、任务状态和普通全局变量不会保留。需要跨休眠保留的数据必须写入 NVS 或 RTC 内存;应用内嵌素材是只读的,不能保存运行时数据。
### 5.6 NVS 写入带来少量 Flash 磨损
+23
View File
@@ -0,0 +1,23 @@
# 应用内嵌素材与 Flash 布局
LCD PNG 和 Opus 提示音随应用固件一起发布。`ota_0`、`ota_1` 各为 4.5 MiB;每个槽位包含代码和同版本素材。语音识别模型仍单独使用 `model` 分区,NVS、OTA 数据和 coredump 维持各自用途。分区的实际偏移以 `16m.csv` 和每次构建生成的空间报告为准。
## 构建流程
1. `tools/optimize_lcd_assets.py check-current` 核对已提交的优化 PNG 与原图、manifest 一致。
2. `components/platform/CMakeLists.txt` 将明确列出的 PNG 和 `assets/audio_prompts/` 中的 Opus 复制到构建暂存目录。
3. `tools/pack_embedded_assets.py` 按名称排序,生成 `platform_assets.pack`。包内每项包含名称、长度及原始文件字节;程序通过名称查找,不需要 LittleFS 挂载。未列入 PNG 清单的原图不会进入固件。
4. 构建系统将整个包嵌入 `TQ_Cat_Code.bin`。OTA 对应用镜像执行原有长度、SHA-256 和镜像校验,素材随固件一起切换或回滚。
5. 每次 `idf.py build` 结束后,`tools/report_flash_usage.py` 输出完整分区地址、应用槽位占用、素材包大小、模型镜像占用和 Flash 未分配尾部;超出分区容量时构建失败。
`idf.py flash` 首次烧录 bootloader、分区表、OTA 数据、应用和模型。不再生成或烧录 `gifpack.bin`。旧分区布局的开发板需先整片擦除,再烧录新布局;只执行应用 OTA 不会改写分区表或模型分区。
## 运行时读取
`platform` 内部的 `platform_asset_find()` 返回只读地址和长度。调用方必须使用完整包内名称:LCD 使用 `lcd/<名称>.png`,提示音使用 `audio_prompts/<名称>.opus`。PNG 解码器直接读取应用镜像内的字节,不复制整张 PNG 到 PSRAM;解码后的 RGB565 缓存仍保留。Opus 按原有四字节大端长度头逐帧送入音频管线,并逐帧检查边界。
素材只随应用固件发布。仅修改某张图或提示音,也需要发布新的应用 OTA 镜像。`model` 分区仍通过 ESP-SR 独立加载,不属于应用 OTA。
## 验证边界
本地构建和素材包校验可证明文件被打包且镜像装入分区;LCD 显示、提示音播放、双槽 OTA、回滚及模型加载仍须在开发板上验证。
@@ -1,10 +1,10 @@
# Flash 与内存容量分析、风险及后续开发建议
> 历史容量快照:文中远程日志与 `logfs` 已从当前固件移除,分区地址和容量应以现行 `16m.csv` 与最新构建产物为准。
> 历史容量快照:文中远程日志、`logfs` 和独立 `gifpack` 分区已从当前固件移除。下文的“当前”“建议”和旧构建步骤仅针对当时版本,不能作为现行操作指南。现行分区以 `16m.csv` 与最新构建报告为准,资源操作见 [应用内嵌素材使用与添加指南](../应用内嵌素材使用与添加指南.md)。
本文记录当前固件的 Flash 分区使用情况、静态 RAM 使用情况、运行时内存风险、可优化内容和后续开发约束,用于功能评审、资源接入、OTA 设计和长期容量管理。
LittleFS 资源从源码到设备的详细流程见:
历史 LittleFS 资源流程归档见:
- [`docs/littlefs-resource-build-flash-mount.md`](littlefs-resource-build-flash-mount.md)
@@ -12,7 +12,7 @@
项目当前直接通过 ESP-IDF `esp_lcd` 驱动 ST7789P3,LCD 渲染链路主要支持:
- 从 LittleFS 读取 PNG;
- 从应用镜像内嵌素材包读取 PNG;
- 将 PNG 解码为 RGB565;
- 按 8 行一组复制到 LCD DMA 行缓冲;
- 合成充电、低电量等状态图标;
@@ -1,5 +1,7 @@
# LittleFS 资源构建、烧录与挂载流程
> 历史流程:当前固件已将 LCD 图片和提示音嵌入 OTA 应用镜像。下文的“当前”、命令和故障排查仅针对旧版 `gifpack` LittleFS,不适用于现行工程;请使用 [应用内嵌素材使用与添加指南](../应用内嵌素材使用与添加指南.md)。
本文说明项目中的 LCD 图片和提示音资源如何从源码目录进入设备 Flash,以及设备运行时如何通过 LittleFS 访问这些资源。同时给出新增、替换和排查资源问题的完整操作步骤。
## 1. 先建立整体认识
@@ -46,7 +46,7 @@ if (!s_server_success_prompt_played) {
if (should_play_prompt) {
(void)domain_runtime_behavior_play_prompt_file(
"/littlefs/audio_prompts/server_connect_success.opus");
"audio_prompts/server_connect_success.opus");
}
```
@@ -14,7 +14,7 @@
- Wi-Fi 连接、重连和配置热点;
- 设备身份、JWT 和会话令牌;
- 用户出生资料、每日运势和财富方向;
- NVS、LittleFS 和工厂 CLI。
- NVS、应用内嵌素材、模型分区和工厂 CLI。
## 2. 分层架构
@@ -238,12 +238,14 @@ TQ_Cat_Code-1.0.3.bin
### 6.4 该文件只是应用镜像,不是完整烧录包
`TQ_Cat_Code-1.0.2.bin` 只对应应用程序分区。它不包含以下独立镜像或资源:
`TQ_Cat_Code-1.0.2.bin` 只对应应用程序分区。它不包含以下独立镜像:
- Bootloader;
- 分区表;
- OTA data 初始化镜像;
- LittleFS/LCD/音频资源镜像。
- 独立模型分区镜像。
当前 LCD 图片和提示音已包含在应用镜像内,会随应用 OTA 一起更新。
该文件适合应用分区 OTA,不能被误认为包含全部分区的整机合并烧录镜像。工厂烧录仍需按照 ESP-IDF 生成的烧录参数写入各个分区。
+1 -1
View File
@@ -65,7 +65,7 @@ Transport disconnect/error events are not server error expressions.
## Asset Migration Direction
The image playback path is a platform-owned direct LCD asset pipeline. Runtime
PNG files are stored in the `gifpack` LittleFS partition, decoded through
PNG files are embedded in each OTA application image, decoded through
`lodepng`, converted to RGB565 in PSRAM, and written to the ST7789 panel through
the existing bounded row-transfer buffer.
+4 -4
View File
@@ -4,7 +4,7 @@
固件使用的 LCD 为 RGB565 显示格式,而设计工具导出的 PNG 通常是 RGB888 或 RGBA8888。原始 PNG 中包含 LCD 无法显示的颜色精度,部分图片还携带未实际使用的 Alpha 通道。
本项目在不改变 LCD 最终 RGB565 像素的前提下,将固件图片转换成 RGB565 颜色精度,并在颜色数量不超过 256 时保存为索引色 PNG,以降低 `gifpack` LittleFS 分区的占用。
本项目在不改变 LCD 最终 RGB565 像素的前提下,将固件图片转换成 RGB565 颜色精度,并在颜色数量不超过 256 时保存为索引色 PNG,以降低应用镜像中的素材占用。
正常固件编译直接使用已经提交到 Git 的优化图片,不要求开发环境安装 Pillow。只有修改或验证图片资源时才需要 Pillow。
@@ -16,9 +16,9 @@ components/platform/assets/
└── lcd_optimized/ 优化图片和 manifest.json,固件实际使用
```
两个目录中的 PNG 文件名必须一致。运行时路径仍为 `/lcd/<文件名>.png`,业务代码不需要区分原图和优化图。
两个目录中的 PNG 文件名必须一致。运行时逻辑名称为 `lcd/<文件名>.png`,业务代码不需要区分原图和优化图。
`lcd_source` 中的 `OTA.gif` 是历史资源,不属于当前 PNG 优化和 `gifpack` 打包清单。
`lcd_source` 中的 `OTA.gif` 是历史资源,不属于当前 PNG 优化和嵌入素材清单。
## 优化规则
@@ -81,7 +81,7 @@ components/platform/assets/lcd_optimized/manifest.json
- 优化图片没有缺失或被手动修改;
- 原图、优化图和清单中的文件集合一致。
资源过期时,构建会停止并提示重新运行 `generate`。资源有效时,构建系统把 `lcd_optimized` 中的 PNG 复制到构建目录并生成 `gifpack` 分区镜像。
资源过期时,构建会停止并提示重新运行 `generate`。资源有效时,构建系统把 `lcd_optimized` 中的 PNG 复制到构建目录,生成只读素材包并链接到应用镜像。
## 命令说明
@@ -1,5 +1,7 @@
# LCD PNG 存储优化实施报告
> 历史优化报告:本文记录当时的 `gifpack` LittleFS 布局及测试数据,不代表当前固件。现行素材流程见 [应用内嵌素材使用与添加指南](应用内嵌素材使用与添加指南.md)。
## 1. 基本信息
- 对应提交:`549f495 perf(assets): optimize LCD PNG storage`
@@ -74,7 +74,7 @@ CONFIG_TQ_LIGHT_STANDBY_IDLE_TIMEOUT_MS=300000
CONFIG_TQ_LIGHT_STANDBY_DIM_BACKLIGHT=y
CONFIG_TQ_LIGHT_STANDBY_BACKLIGHT_PERCENT=10
CONFIG_TQ_LIGHT_STANDBY_PROMPT_ENABLE=y
CONFIG_TQ_LIGHT_STANDBY_PROMPT_FILE="standby_5min.opus"
CONFIG_TQ_LIGHT_STANDBY_PROMPT_FILE="audio_prompts/standby_5min.opus"
CONFIG_TQ_LIGHT_STANDBY_DISABLE_SPEAKER_OUTPUT=y
CONFIG_TQ_LIGHT_STANDBY_RELEASE_MOTOR_HOLD=y
CONFIG_TQ_LIGHT_STANDBY_WIFI_POWER_SAVE=y
@@ -606,7 +606,7 @@ esp_err_t control_plane_light_standby_record_activity(
- ESP-IDF 5.5.4 完整构建通过。
- 当前构建生成的应用固件约 2.08 MiB,应用分区剩余约 41%。
- `standby_5min.opus` 已纳入 LittleFS 音频资源。
- `audio_prompts/standby_5min.opus` 已纳入应用镜像中的音频素材包。
- 源码差异格式检查通过。
- 已确认提示音等待的是播放管线终止事件,而不是固定时长硬等待。
@@ -321,13 +321,13 @@ Wi-Fi 断开表情在离线期间每 5 秒刷新一次,但被动断网提示
```c
if (!control_plane_is_sta_connected()) {
play_prompt("network_unstable.opus");
play_prompt("audio_prompts/network_unstable.opus");
request_immediate_wifi_reconnect();
return ESP_ERR_INVALID_STATE;
}
if (server_outage_confirmed) {
play_prompt("server_repair_wait_reopen.opus");
play_prompt("audio_prompts/server_repair_wait_reopen.opus");
schedule_fresh_auth_websocket_reconnect();
return ESP_ERR_INVALID_STATE;
}
+9 -7
View File
@@ -1,5 +1,7 @@
# 网络异常语音反馈与业务快速失败改造说明
> 本文中的提交、构建容量和测试结果是历史记录;资源路径已更新为现行应用内嵌素材名称。现行添加流程见 [应用内嵌素材使用与添加指南](应用内嵌素材使用与添加指南.md)。
## 1. 提交概况
```text
@@ -95,19 +97,19 @@ static portMUX_TYPE s_feedback_lock = portMUX_INITIALIZER_UNLOCKED;
### 3.2 复用现有音频资源
本次没有新增录音文件,直接复用 LittleFS 中已有的五个 Opus 资源:
本次没有新增录音文件,直接复用应用镜像中已有的五个 Opus 资源:
```c
#define CONTROL_PLANE_WIFI_INTERRUPTED_PROMPT_FILE \
"/littlefs/audio_prompts/network_interrupted_repairing.opus"
"audio_prompts/network_interrupted_repairing.opus"
#define CONTROL_PLANE_WIFI_ACTION_BLOCKED_PROMPT_FILE \
"/littlefs/audio_prompts/network_unstable.opus"
"audio_prompts/network_unstable.opus"
#define CONTROL_PLANE_SERVER_INTERRUPTED_PROMPT_FILE \
"/littlefs/audio_prompts/server_abnormal_interrupted.opus"
"audio_prompts/server_abnormal_interrupted.opus"
#define CONTROL_PLANE_SERVER_ACTION_BLOCKED_PROMPT_FILE \
"/littlefs/audio_prompts/server_repair_wait_reopen.opus"
"audio_prompts/server_repair_wait_reopen.opus"
#define CONTROL_PLANE_NETWORK_RESTORED_PROMPT_FILE \
"/littlefs/audio_prompts/signal_restored_business_resume.opus"
"audio_prompts/signal_restored_business_resume.opus"
```
对应关系如下:
@@ -486,7 +488,7 @@ Free space: 0x1682b0 bytes(40%)
- 新增网络反馈模块编译通过;
- 固件链接通过;
- 五个提示音均进入 LittleFS 镜像;
- 五个提示音均进入应用镜像中的素材包;
- 应用分区容量检查通过;
- Markdown 格式和空白错误检查通过。
@@ -272,7 +272,7 @@ return domain_runtime_request_expression(&request);
- 财位、校准、抽签等已有模型指令的枚举值未改变。
- 表达式请求结构增加字段,但现有请求均使用指定字段初始化,新增字段默认置零。
- 原五行调用的 `wuxing_element_valid` 默认为 `false`,继续随机显示。
- 未新增 LCD 图片,不增加 LittleFS 资源占用。
- 未新增 LCD 图片,不增加应用镜像中的素材占用。
- 未新增任务、队列或锁,不改变任务生命周期和并发模型。
### 5.2 当前限制
@@ -0,0 +1,68 @@
# 应用内嵌素材使用与添加指南
## 当前机制
LCD PNG 和提示音 Opus 在构建时生成 `platform_assets.pack`,作为二进制数据链接进应用固件。`ota_0`、`ota_1` 都装入完整的代码和素材,因此应用 OTA 会同步更新两者。运行时按**包内完整相对名称**查找,例如 `lcd/ota1.png`、`audio_prompts/boot.opus`;这些名称不是 VFS 文件路径,不能用 `fopen()` 读取,也不需要挂载 LittleFS。模型仍在独立的 `model` 分区,不随应用 OTA 更新。
源码、打包器和运行时入口:
| 用途 | 位置 |
| --- | --- |
| 原始 LCD PNG | `components/platform/assets/lcd_source/` |
| 优化后实际入包的 PNG | `components/platform/assets/lcd_optimized/` |
| 实际入包的 Opus | `components/platform/assets/audio_prompts/` |
| PNG 入包清单与构建暂存 | `components/platform/CMakeLists.txt` |
| 素材包格式 | `tools/pack_embedded_assets.py` |
| 只读查找实现 | `components/platform/src/platform_asset_bundle.c` |
| Flash 空间报告 | `tools/report_flash_usage.py` |
素材查找器属于 `platform` 内部接口。业务代码不要读取包内字节或拼接 Flash 地址,应调用下述 `domain`/`control_plane` 接口。
## 添加或替换 LCD 图片
1. 将原始 PNG 放到 `components/platform/assets/lcd_source/<名称>.png`。替换图片时只修改原图,不直接编辑 `lcd_optimized/`。
2. 按 [LCD 图片资源优化与维护](lcd-image-asset-optimization.zh-CN.md) 准备 Pillow 环境,在仓库根目录执行:
```bash
python tools/optimize_lcd_assets.py generate
python tools/optimize_lcd_assets.py verify
```
3. 在 `components/platform/CMakeLists.txt` 的 `platform_lcd_png_assets` 中加入 `<名称>`,不带 `.png`。未列入清单的 PNG 不会打包。
4. 在 `components/platform/src/lcd_direct_assets.c` 登记图片及其帧索引,并在 `lcd_direct_asset_frame()` 中登记对应的资源组名。**仅入包不会让 LCD 自动播放它。**
5. 如需新增业务表情,在 `components/domain/include/domain.h` 增加 `domain_expression_id_t`,并在 `components/domain/src/runtime/domain_runtime_adapters.c` 的目录映射中配置帧顺序、时长、重复次数、优先级和完成后行为。现有表情只换图时,通常不需要新增表情 ID。
业务触发点放在 `control_plane`,通过 `domain` 的语义接口请求,示例:
```c
domain_expression_request_t request = {
.id = DOMAIN_EXPRESSION_OTA_UPDATE,
.trigger = DOMAIN_EXPRESSION_TRIGGER_OTA_UPDATE,
};
esp_err_t err = domain_runtime_request_expression(&request);
```
`app_composition` 只负责启动接线;若启动阶段需要触发表情,应调用 `control_plane` 的公开入口。不要从上层直接调用 `platform_lcd_*` 或 `platform_asset_find()`。例如现有开机流程调用 `control_plane_request_boot_expression()`。
## 添加或替换提示音
1. 将文件放到 `components/platform/assets/audio_prompts/<名称>.opus`。该目录中的 `.opus` 会自动入包,无须修改 PNG 清单。当前打包格式只接受 `audio_prompts/` 下的单层文件,勿另建子目录。
2. 文件必须符合项目现有提示音格式:每个 Opus 包前有 **4 字节大端包长**,单包长度为 1–2048 字节。普通 Ogg Opus 文件不能仅改名为 `.opus` 使用。
3. 在负责该业务时机的 `control_plane` 模块中使用完整名称:
```c
esp_err_t err = domain_runtime_behavior_play_prompt_file(
"audio_prompts/new_prompt.opus");
```
4. 如果提示音路径由 Kconfig 配置,配置值也必须是完整包内名称,例如 `audio_prompts/boot.opus`;禁用可选提示音时按对应配置的空值规则处理。旧的 `/littlefs/...` 路径和裸文件名不再被解析。
播放接口同步完成分帧、下行播放会话和等待,调用位置应符合该业务的阻塞预算。需要新的语义触发或重试策略时,在对应的 `control_plane` 模块实现,不在 `app_composition` 中加入业务策略。
## 构建、更新与验收
使用项目指定的 ESP-IDF v5.5.4 环境执行 `idf.py build`。构建会先检查原图、优化图和 manifest 的一致性,再打包所选 PNG 与全部提示音。输出包括 `build/esp-idf/platform/platform_assets.pack` 和 `build/TQ_Cat_Code.bin`,并打印应用槽位、素材包、模型及 Flash 尾部空间。**素材包大小已包含在应用镜像大小中,不要重复相加。**
检查新增素材时,先确认构建通过、空间报告未超出 OTA 槽位,再在设备上验证实际画面或声音。更新素材需要重新发布应用 OTA;对于当前分区布局,首次烧录需同时烧录新的分区表、应用与模型。仅改素材也要生成新的应用镜像。
若运行时报告素材未找到,按顺序检查:调用名称是否含 `lcd/` 或 `audio_prompts/` 前缀;PNG 是否在 CMake 清单中;PNG 是否已登记到平台帧表和领域表情映射;提示音是否为项目分帧格式;设备运行的是否为本次构建的应用版本。构建目录的暂存文件不是设备上的文件系统。