feat(lcd): animate fortune results with shared GFX layers

This commit is contained in:
chenjintao
2026-10-10 11:36:54 +08:00
parent bf06618848
commit ee32b4d63f
26 changed files with 3063 additions and 11 deletions
+32 -5
View File
@@ -1,6 +1,6 @@
# GFX 表情显示应用与开发指南
本文面向本项目的开发者,说明当前 `esp_emote_gfx` 的具体应用及后续开发方法。正文使用中文,代码、路径和日志字段保留原名以便检索。适用基线为 2026 年 10 月 9 日的工作区实现;资源和性能数字随代码变化而变化。
本文面向本项目的开发者,说明当前 `esp_emote_gfx` 的具体应用及后续开发方法。正文使用中文,代码、路径和日志字段保留原名以便检索。适用基线包含 2026 年 10 月 10 日的抽签分层动画;资源和性能数字随代码变化而变化。
## 一、当前方案与边界
@@ -8,9 +8,9 @@
- 底层为 ESP32-S3、ST7789P3、单线 SPI,当前像素时钟为 40 MHz,逻辑画布为 320×240。
- 当前使用 `esp_emote_gfx` 3.0.5,由 `platform` 私有依赖。没有接入 `esp_emote_expression` 状态管理组件,也没有把当前表情绘制建立在 LVGL 上。依赖目录中存在其他库不等于当前显示链路使用它们。
- 待机基础表情与 listening 采用 GFX;其他表情仍走原 PNG 或步骤序列,不是所有资产都已经迁移。listening 缓存失败时单独回退旧 PNG,不影响待机。
- 待机基础表情、listening 与五种抽签结果采用 GFX 图层;其他表情仍走原 PNG 或步骤序列,不是所有资产都已经迁移。listening 缓存失败时单独回退旧 PNG,不影响待机。抽签只在开始画面前准备失败时回退,播放途中取消/传输失败不从头重播 PNG。
- 屏幕目前未使用 TE 同步。软件预合成和连续传输能改善撕裂观感,但不能保证面板扫描与写入完全同步。
- 用户已确认待机缓存/固定时间轴以及新 listening 版本的观感满意。两者最终版本尚无新的配套量化日志,不能写成“稳定达到 30 FPS”或“全部实机验收完成”。
- 用户已确认待机缓存/固定时间轴以及新 listening 版本的观感满意。2026-10-10 启动样本中的待机眨眼约 410 ms、步骤约 29.26/s、实际眼型更新约 21.95/s;不是 listening/抽签帧率或全负载验证,不能写成“全部实机验收完成”。
已安装组件提供图片、网格图片、运动、动画、标签、二维码和按钮等接口。但本项目仅实际使用图片、网格、显示刷新及相关对象控制;通用中文文本、按钮、触摸和复杂布局不属于当前已实现能力。
@@ -63,6 +63,11 @@ esp_err_t err = domain_runtime_request_expression(&request);
| `components/platform/internal/lcd_emote_listening.h` | 平台私有插值接口,不向领域或控制层泄漏 GFX 类型 |
| `tools/prepare_listening_expression.py` | 拆分旧 listening、编码往返校验、生成四个 C 资产及同算法动画预览 |
| `tools/test_lcd_listening_gfx.py` | 实际资产、全闭合参数、缓存分配失败、GFX 对象失败、静态像素恢复与切换回归 |
| `components/platform/src/lcd_emote_fortune.c` | 抽签 LZ4 有界解码、轻透视纹理光栅、签文展开、放大与星光 |
| `components/platform/internal/lcd_emote_fortune.h` | 抽签私有图层/纹理/固定窗口契约 |
| `components/platform/src/generated/emote_gfx/fortune_assets.c` | 六个共享图层、五种签文 LZ4 压缩载荷 |
| `tools/prepare_fortune_expression.py` / `tools/preview_fortune_expression.py` | 拆分/压缩与实际 C 算法预览;均不进设备固件 |
| `tools/test_lcd_fortune_gfx.py` | 五签、损坏输入、几何边界、时间轴、取消和释放回归 |
`/tmp` 下修改过程中生成的补丁脚本不是工程运行依赖,不应提交或作为正式资源流程使用。
@@ -178,6 +183,7 @@ RLE(游程长度编码)把连续相同像素写成“数量+像素值”
| --- | --- | --- | --- |
| 普通 PNG 与 GFX 失败后备 | `lcd_source/*.png` | `lcd_optimized/*.png` 经素材包链接入应用 | 原 PNG 解码/缓存链路 |
| 新待机/listening GFX | 原 PNG 与拆分图层 PNG | 六个实际参与编译的 RLE C 资产 | 初始化解压,生成 PSRAM 眼型缓存,之后切换缓存 |
| 五种抽签 GFX | 抽签源 PNG 与 `lcd_layers/chouqian` 拆分层 | 十一图层的 LZ4 block C 数组 | 动作开始解压六个共享层及所选签文,实时局部纹理光栅,结束/取消解绑后释放 |
| 离线预览 | 图层与实际插值算法 | 不入固件 | 在电脑上检查造型,不能模拟 LCD 扫描 |
RLE 的编码往返对转换后的 RGB565+A8 数据无损,不等于原 PNG 的所有 RGB888
@@ -195,6 +201,7 @@ components/platform/assets/
lcd_layers/
jichubiaoqing/ 待机可编辑拆分图层、布局及预览
listening/ 聆听可编辑拆分图层、布局及预览
chouqian/ 抽签共享图层、五种原签文及布局
components/platform/src/generated/emote_gfx/
idle_base.c 实际编译的底图压缩数据
idle_eyes_open.c 实际编译的睁眼压缩数据
@@ -202,6 +209,7 @@ components/platform/src/generated/emote_gfx/
listening_eyes_open.c 实际编译的聆听睁眼关键图
listening_eyes_half.c 实际编译的聆听半闭关键图
listening_eyes_closed.c 实际编译的聆听闭眼关键图
fortune_assets.c 实际编译的抽签 LZ4;不是 RLE
```
半闭眼、闭眼源图及对应历史生成文件不代表当前参与编译;以构建列表为准,不把目录里的全部文件自动加入固件,也不擅自删除旧资源。
@@ -290,7 +298,7 @@ PSRAM 外部内存充足当成内部 DMA 安全。当前 listening 的已接受
## 八、性能日志如何读
通过 `menuconfig` 搜索 `TQ_LCD_EMOTE_GFX_TEST` 和 `TQ_LCD_EMOTE_FPS_LOG`。前者控制 GFX 待机及 listening 路径;后者控制性能统计,默认关闭,当前本地测量配置开启。关闭统计不应改变动画节奏。
通过 `menuconfig` 搜索 `TQ_LCD_EMOTE_GFX_TEST` 和 `TQ_LCD_EMOTE_FPS_LOG`。前者控制 GFX 待机、listening 及抽签路径;后者控制性能统计,默认关闭,当前本地测量配置开启。关闭统计不应改变动画节奏。抽签使用 `fortune_fps`,只计算运动段、不含最后停留,详见抽签开发说明。
每轮完整眨眼输出一次 `emote_fps` 和 `emote_cost`,不在逐帧传输中打印:
@@ -366,7 +374,25 @@ python3 tools/test_lcd_listening_gfx.py
固定窗口/锚点变化需同步脚本与测试。需要手工保留眉毛等素材编辑时,不要直接重新
拆分覆盖;当前生成脚本不是所有角色通用的热替换工具。
### 接入更多表情
### 已接入的五种抽签结果(2026-10-10)
统一抽出、轻透视翻面、问号、签文展开、放大/闪光与回待机。领域仍选择旧六步
序列和所选结果,平台只对完整匹配的资产序列优化;其他步骤序列不受影响。
纹理光栅由平台私有 C 完成,GFX 使用普通图片承接局部合成及 HUD,不是完整
三维引擎或 LVGL 动画。六图层共享,一次只载入一份签文,不建立五套缓存。
旧十八张 PNG 完整保留。详见 [抽签开发说明](抽签分层动画开发说明.zh-CN.md)。
新载荷 43,547 B,应用实际净增 51,856 B(本次构建 3,157,728 B);运动期
像素 PSRAM 278,249 B,加场景描述 148 B,正常结束/取消释放;临时图片对象
普通堆请求 96 B(不含分配器开销);静态可写 RAM 增量 4 B,无新 DMA/任务。
动画前分配失败后备 PNG;已开始后不因错误重播 PNG;解绑异常保留绑定资源
直到重启,不能假装完整释放。不是新格式一定最省 Flash,比较详见资产台账。
本次主机测试、ESP-IDF 5.5.4、日志/GFX 关闭编译及应用烧录校验通过。
启动日志确认待机/listening 缓存成功,短时显示日志无传输/锁/崩溃错误;
尚未捕获实机抽签播放日志,不宣称已测得抽签 FPS 或五签观感已验收。
### 接入更多表情(逐个评估)
先逐个迁移,保留未迁移 PNG 路径。每个表情准备静态层、动态图层、锚点、时间线、缓存预算及结束行为。固定短动作适合预生成缓存;大量不同动作或连续交互需要考虑按需生成、共享缓存和生命周期,不应给每个表情无上限常驻一份整屏缓存。
@@ -397,6 +423,7 @@ python3 tools/test_lcd_status_hud.py
python3 tools/test_lcd_emote_transport.py
python3 tools/test_lcd_emote_timeline.py
python3 tools/test_lcd_listening_gfx.py
python3 tools/test_lcd_fortune_gfx.py
```
这些脚本在电脑上从实际源码提取 C 函数,使用 Clang 与内存、未定义行为检查器。需要 Python 和 Clang;不是固件代码,不占设备 Flash 或 RAM。测试使用模拟接口,不能证明屏幕无撕裂、实时帧率或设备像素完全一致。
@@ -0,0 +1,151 @@
# 抽签分层动画开发说明
## 当前实现
2026-10-10 将五种结果接入同一动作:卜运签(保留原素材中的“补运签”字形)、
和平签、上上签、小吉签、中上签。领域结果目录及云端结果索引未改变,不在平台
重新随机,不改抽签请求、语音或联网流程。
领域继续下发原六步:前三步 `chouqian_qianzhi` 的 0/1/2,后三步所选结果
的 0/1/2,间隔 500/500/500/500/500/1500 ms。平台只有在完整匹配六步、
无附加图标、原间隔及 320×240、GFX 就绪时使用新动画。修改步骤/间隔会回到
普通 PNG,不静默覆盖通用步骤序列。
| 时间 | 动作 |
| --- | --- |
| 0~600 ms | 背面爪签从下方爪堆中抽出;爪堆遮挡签底,保留钓线/鱼 |
| 600~1200 ms | 轻透视翻面;背面缩窄,近 90° 用侧面厚度关键层,正面展开 |
| 1200~1600 ms | 正面问号保持;问号在翻面后半段已经随正面透视展开 |
| 1600~1900 ms | 所选原签文自上往下展开,不使用完整图片交叉淡化 |
| 1900~2500 ms | 爪签连续放大,爪堆淡出,程序金色星光出现 |
| 运动末帧后 1500 ms | 保持结果,变化的时间/电池可刷新,最后正常完成并守卫回待机 |
名义总长约 4 秒;若运动因负载迟到,仍保留完整 1500 ms 末帧阅读时间。
共同起点固定时间轴,目标运动步间隔 34 ms,75 张绘制(含起始与末端),不
画完再等 34 ms、不跳动作、不忙等。实际调度可迟到;目标不是实测 FPS。
取消以 20 ms 等待片轮询加当前绘制/传输时间为边界,不视为成功完成。
保留统一待机 guard,旧动画完成不能覆盖新 pending 请求。
## 图层和渲染
六个共享层:下方爪堆、背面、侧面、空白正面、问号、钓线/鱼;另有五个原签文。
保留源图,背景抖动杂点归零,清除背面裁剪内的钓线,将正面原签文区恢复签底,
保留肉垫与外轮廓;程序星光代替分散的原星光。属于小幅性能适配,并非原 PNG
逐像素不变。签文轮廓/颜色来自旧素材,不使用另加中文字体。
`lcd_emote_fortune.c` 在 PSRAM 的 `(56,20,208,220)` 局部不透明 RGB565 窗口
完成光栅:16 条纹理带近似透视、前后近远缩放、微小姿态变化;仅近侧面时使用
厚度层,避免半翻面时浮出一条不属于轮廓的金线。正面 UV 补偿反向,文字不镜像。
双线性采样在预乘 Alpha 空间进行,边缘不会把透明黑混出暗边。
每步完整恢复同一局部窗口并画新轮廓,防止旧轮廓残影。图片位置/尺寸固定,
无需以错误的“只更新新轮廓”留下旧像素。钓线在窗口外部分借用现有背景对象,
翻面开始只隐藏一次;不逐帧让不变的钓线失效。窗口顶部覆盖放大爪尖但避开
左上时间和右上电池;源图/几何变化需同步边界及预览。
GFX 负责普通图片图层、共享 HUD、区域合成及既有安全传输,不是使用 GFX
三维引擎,也没有 LVGL GUI。复用 153,600 B GFX 合成缓冲、10,240 B DMA
暂存区、ISR 完成等待和故障锁存;新增 91,520 B 是局部光栅缓冲,不是第二个
整屏换帧缓冲。屏幕无 TE,不能承诺完全无撕裂。
## 制作与测试
```text
components/platform/assets/lcd_source/chouqian_*.png 原十八张 PNG,保留
components/platform/assets/lcd_optimized/chouqian_*.png 旧 PNG 后备,仍入包
components/platform/assets/lcd_layers/chouqian/ 六共享图层、五签文、layout
components/platform/src/generated/emote_gfx/fortune_assets.c 实际编译压缩资产
components/platform/internal/lcd_emote_fortune.h 平台私有描述
components/platform/src/lcd_emote_fortune.c 解码与几何
components/platform/src/lcd_direct.c 匹配、准备、播放、解绑、测量
```
生成文件不要手改。以下需要 Pillow 和 Clang,生成会覆盖本次拆分图层及 C,
先保存手工美术修改;旧十八张源/优化 PNG 不改,不把拆分 PNG 重复入资源包。
```bash
python3 tools/prepare_fortune_expression.py
python3 tools/prepare_fortune_expression.py --check
python3 tools/test_lcd_fortune_gfx.py
python3 tools/preview_fortune_expression.py --output /tmp/lucky-fortune-preview --result 2
```
新压缩编码为 LZ4 block:字面量加 16-bit 回指,用有界、自包含解码器直接恢复
RGB565 高字节在前+独立 A8,不引入新解码任务/库,不在每帧解压。解码拒绝
越界回指、截断和输出长度错误。与旧四字节像素 RLE 不能互换。往返相对转换后
的 RGB565+A8 无损;转换/拆分并非 RGB888 原画无损。Alpha 全平面校验保留。
主机测试实际 C 解码和几何,五种结果共 1180 个采样时刻、空间不足/损坏输入、
九次分配失败、75 个取消点、4 秒时间轴、解绑/传输失败;日志开/关均覆盖。
预览也调用实际 C,另比较 C 解压与图层 PNG 转换后的像素及 Alpha。
仍需执行原四个显示回归和 IDF 5.5.4,并对 LCD 源文件验证日志/GFX 关闭条件编译。
替换资源不是只改 PNG:当前拆分坐标、尺寸、问号、原签文区域、锚点、放大
比例和窗口耦合。要调整生成器与几何,并用实际 C 预览检查翻面镜像、爪尖/签底
裁剪、字形缺笔、Alpha 暗边、HUD 遮挡及返回待机。
## 资源与生命周期
新载荷 43,547 B,十一张优化图层 PNG 37,046 B,原十八 PNG 372,012 B、
旧优化十八 PNG 136,226 B。不是相同图层数量的无损压缩比例对比;新方案
只保留少量关键素材而实时计算过渡。新 LZ4 比其十一 PNG 大 6,501 B,选择
它是为有界直接解压与减少 PNG 解码暂存,不是保证最小 Flash。
| 新增项 | 数值 |
| --- | ---: |
| 十一图层原始 RGB565+A8 | 246,633 B(含五签文,不全解压到运行内存) |
| 编译资产对象只读段(含描述/目录名) | 43,831 B;data/BSS 0 |
| 应用镜像 | 3,157,728 B,较上一提交 3,105,872 B 净增 51,856 B |
| 单 OTA 槽余量 | 1,560,864 B |
| 六共享图层解压像素 | 171,753 B |
| 本次所选一份签文 | 14,976 B |
| 局部光栅缓冲 | 91,520 B |
| 播放期像素 PSRAM 峰值 | 278,249 B |
| 场景状态/描述 | 148 B PSRAM;峰值明确请求合计 278,397 B |
| 临时 GFX 图片对象/状态/链节点 | 80+8+8=96 B 普通堆,不含管理开销 |
| 静态可写数据 | LCD 文件增加 4 B 指针 |
| 新内部/DMA 暂存区、任务 | 0 |
| 正常完成/取消后新增 PSRAM/对象常驻 | 0,解绑释放 |
数据来自目标编译器及本次构建,不包含分配器管理、局部栈和绘制临时开销。
旧 PNG 后备保留,包仍是 820,125 B、75 PNG/32 Opus,所以完整固件不会按
新旧素材差额减少。成本在本次提出的额外 Flash 80 KiB、像素 PSRAM 300 KiB
预算内;不要将此预算自动授权给后续更大缓存或新内部/DMA 缓冲。
播放准备失败释放部分成果;创建/绑定失败且安全解绑后才回退 PNG;开始绘制
后取消/失败不从头重播。每次动作只解压六共享层与一签文、申请一局部缓冲和
一个图片对象,正常退出持同任务 GFX 锁,背景重新绑定待机,再删除临时对象,
释放像素/场景。若锁/解绑异常,保留已绑定资源到重启,禁止再次创建该场景,
不能宣称此异常下已完成释放;既有强制删任务异常路径也不作为正常释放保证。
## 设备测试与日志
工厂 UART0 增加仅显示的测试命令,不调用云端,不写 NVS:
```text
lcd_fortune_test 0
lcd_fortune_test 1
lcd_fortune_test 2
lcd_fortune_test 3
lcd_fortune_test 4
```
每条结束后等至少 4 秒再发下一条;新命令会中断当前画面,不能当排队命令。
0~4 映射与上面顺序一致;仅接受单字符索引,缺失/越界/多参数拒绝。
`OK` 仅代表请求入队,完成/显示仍以日志及屏幕为准。当前 USB Serial/JTAG
是次级日志输出,CLI 读取 UART0,不能假设向 USB 日志口发送命令就会被执行。
正常抽签仍可通过既有 SW2/云端流程触发,不需要该测试命令。
开启 `CONFIG_TQ_LCD_EMOTE_FPS_LOG` 时每次运动段输出 `fortune_fps`:
`frames` 仅计成功绘制/传输;`motion_us` 为实际运动墙钟,排除最后保持;
`update_fps` 使用相同时间分母;`draw_avg_us/max_us` 含锁、光栅、GFX、DMA
复制及传输等待,平均分母是绘制尝试数(包括失败尝试);`late_frames` 是绘制
达到 34 ms 的次数,不是面板丢帧。
这些是任务墙钟,不能当纯 CPU,也不等于面板扫描率。开始日志另有解压/准备
时间、像素 PSRAM 及场景描述字节。
本次五签主机/条件编译/IDF 5.5.4 构建通过;当前设备 ota_0 有效,经读取 OTA
选择区确认后只用 app-flash 写 ota_0 应用,镜像校验成功,NVS/模型/选择区未改。
启动采样 25 秒:最终应用 SHA 前缀 06e65e4a0、待机/listening 缓存成功,采样内
无显示锁/传输/崩溃错误。未捕获抽签运动日志,未完成五签 LCD 观感及并行负载
验收,不写成实测稳定 30 FPS。启动期间的内存最低值不能代表抽签运行期最低值。
+51 -1
View File
@@ -419,7 +419,57 @@ PNG 包仍为 820,125 B、75 PNG/32 Opus;只读资产已包含于整个应用
内部/DMA 专用缓冲及静态可写数据均为 0;复用既有 HUD 和行缓冲。
局部栈、CPU 耗时和设备最低内存尚未新增实机测量,不应写成整体资源零成本。
## 维护与后续清理
## 新增抽签分层资产(2026-10-10)
以下新增项不合并进上面的历史目录汇总。旧抽签十八张源 PNG 共 372,012 B、
优化 PNG 共 136,226 B,全部保留且后备仍入包。PNG 包仍 820,125 B,不重复
加入本次拆分层。生成器使用优化 PNG 保存拆分层,但实际编译 LZ4,不是 RLE。
图层目录:`components/platform/assets/lcd_layers/chouqian/`。
| 图层 | 尺寸 | 优化 PNG B | 本机分配 B | RGB565+A8 B | LZ4 载荷 B |
| --- | --- | ---: | ---: | ---: | ---: |
| `pile.png` | 208×76 | 9,594 | 12,288 | 47,424 | 9,417 |
| `back.png` | 63×113 | 2,999 | 4,096 | 21,357 | 4,179 |
| `edge.png` | 41×110 | 2,957 | 4,096 | 13,530 | 4,389 |
| `front.png` | 100×190 | 7,579 | 8,192 | 57,000 | 10,774 |
| `question.png` | 27×50 | 730 | 4,096 | 4,050 | 817 |
| `rod.png` | 169×56 | 1,584 | 4,096 | 28,392 | 2,094 |
| `text_buyun.png` | 48×104 | 2,502 | 4,096 | 14,976 | 2,638 |
| `text_heping.png` | 48×104 | 2,784 | 4,096 | 14,976 | 2,851 |
| `text_shangshang.png` | 48×104 | 2,132 | 4,096 | 14,976 | 2,069 |
| `text_xiaoji.png` | 48×104 | 2,091 | 4,096 | 14,976 | 2,210 |
| `text_zhongshang.png` | 48×104 | 2,094 | 4,096 | 14,976 | 2,109 |
| 合计 | 十一图层 | 37,046 | 57,344 | 246,633 | 43,547 |
`layout.json` 1,929 B、分配 4,096 B,不入固件;目录共十二文件 38,975 B、
分配 61,440 B。预览工具默认输出 `/tmp/lucky-fortune-preview`,不是编译资产。
源十八 PNG 与本次拆分层不是相同数量的无损比较;背景/几何做了小幅性能适配。
LZ4 对转换后的 RGB565+A8 无损且完整保留 Alpha,但比拆分 PNG 多 6,501 B,
不能称为压得最小。生成器及实际 C 解码均做往返校验,原旧 PNG 不覆盖。
生成 C:`components/platform/src/generated/emote_gfx/fortune_assets.c`,文本
270,251 B、分配 270,336 B;目标对象只读段总计 43,831 B(载荷、176 B 资产
描述、20 B 目录指针、88 B 目录名),可写 data/BSS 为 0。生成 C 不手改。
本次完整应用 3,157,728 B,较 HUD 提交 3,105,872 B 增加 51,856 B,包含
资产、几何/解码、播放与 UART 显示测试命令的代码及对齐;单 OTA 槽余量
1,560,864 B。所有只读资源已经包含在应用里,不能再相加。
六共享层解压 171,753 B、一次仅选一份签文 14,976 B、208×220 RGB565
局部光栅缓冲 91,520 B,像素峰值合计 278,249 B PSRAM;目标编译器场景
描述 148 B 也在 PSRAM,明确申请峰值 278,397 B,不含堆管理。
正常结束/取消后释放,新增常驻像素为 0。临时 GFX 图片对象/状态/链节点
普通堆请求合计 96 B,不含分配器;LCD 静态可写数据增加 4 B 指针,
无新增内部/DMA 缓冲、合成整屏缓冲或任务;局部栈/CPU/实际最低余量另测。
解绑故障时保留绑定资源到重启,不冒充完整释放。
五签主机回归、IDF 5.5.4、日志关闭/GFX 关闭编译与应用烧录校验通过。
设备启动确认缓存成功、25 秒采样无显示锁/传输/崩溃错误;未捕获抽签运动
日志,FPS/五签观感/并行负载最低内存仍待实机验收。详见
[抽签开发说明](抽签分层动画开发说明.zh-CN.md)。
## 维护与后续清理(通用)
1. PNG 新增/替换按 [图片优化流程](lcd-image-asset-optimization.zh-CN.md) 生成并验证,
不能编辑优化产物代替修改源图。GFX 新增/替换使用对应压缩脚本和编码往返测试。