docs(lcd): add Chinese GFX development guide and agent rules
This commit is contained in:
@@ -0,0 +1,250 @@
|
||||
# GFX 表情显示应用与开发指南
|
||||
|
||||
本文面向本项目的开发者,说明当前 `esp_emote_gfx` 的具体应用及后续开发方法。正文使用中文,代码、路径和日志字段保留原名以便检索。适用基线为 2026 年 10 月 9 日的工作区实现;资源和性能数字随代码变化而变化。
|
||||
|
||||
## 一、当前方案与边界
|
||||
|
||||
本项目没有触摸和复杂界面交互,显示需求以精致表情、时间和电量为主。当前方案是轻量图层渲染,不是完整界面框架迁移。
|
||||
|
||||
- 底层为 ESP32-S3、ST7789P3、单线 SPI,当前像素时钟为 40 MHz,逻辑画布为 320×240。
|
||||
- 当前使用 `esp_emote_gfx` 3.0.5,由 `platform` 私有依赖。没有接入 `esp_emote_expression` 状态管理组件,也没有把当前表情绘制建立在 LVGL 上。依赖目录中存在其他库不等于当前显示链路使用它们。
|
||||
- 待机基础表情采用 GFX;其他表情仍走原 PNG 或步骤序列,不是所有资产都已经迁移。
|
||||
- 屏幕目前未使用 TE 同步。软件预合成和连续传输能改善撕裂观感,但不能保证面板扫描与写入完全同步。
|
||||
- 用户已确认缓存与固定时间轴版本的观感满意;最后的固定时间轴版本尚未提供新的量化日志,不能写成“稳定达到 30 FPS”。
|
||||
|
||||
已安装组件提供图片、网格图片、运动、动画、标签、二维码和按钮等接口。但本项目仅实际使用图片、网格、显示刷新及相关对象控制;通用中文文本、按钮、触摸和复杂布局不属于当前已实现能力。
|
||||
|
||||
## 二、分层与调用入口
|
||||
|
||||
```text
|
||||
控制层决定何时展示表情
|
||||
→ 领域层请求语义表情并查目录
|
||||
→ 平台层接收通用播放请求
|
||||
→ LCD 渲染任务串行播放
|
||||
→ GFX 合成或 PNG 合成
|
||||
→ SPI 写入屏幕
|
||||
```
|
||||
|
||||
上层通过领域层请求,不直接调用 GFX 或面板接口。例如返回待机:
|
||||
|
||||
```c
|
||||
const domain_expression_request_t request = {
|
||||
.id = DOMAIN_EXPRESSION_IDLE,
|
||||
.trigger = DOMAIN_EXPRESSION_TRIGGER_UNSPECIFIED,
|
||||
};
|
||||
esp_err_t err = domain_runtime_request_expression(&request);
|
||||
/* 调用方按自身策略处理 err;不要绕过领域映射直接写 LCD。 */
|
||||
```
|
||||
|
||||
新增语义表情应在 `components/domain/include/domain.h` 声明,并在领域目录中映射;平台层只接收播放原语,不应出现 `DOMAIN_EXPRESSION_*` 语义依赖。GFX 类型不得泄漏到上层公共接口。
|
||||
|
||||
当前请求采用单个待处理槽,不是完整优先级仲裁器。请求结构虽有优先级字段,也不能据此假定已经实现“低优先级自动排队”。
|
||||
|
||||
## 三、关键文件与职责
|
||||
|
||||
| 文件 | 职责 |
|
||||
| --- | --- |
|
||||
| `components/platform/src/lcd_direct.c` | 面板初始化、请求播放、GFX 图层、眼型生成和缓存、时间轴、状态栏、传输及性能日志 |
|
||||
| `components/platform/include/platform.h` | 通用显示请求和电池覆盖层公共契约 |
|
||||
| `components/platform/src/runtime/platform_lcd_runtime.c` | 平台显示入口转接 |
|
||||
| `components/domain/src/runtime/domain_runtime_adapters.c` | 语义表情目录、请求映射、电量状态映射 |
|
||||
| `components/platform/internal/lcd_emote_assets.h` | 私有压缩资产描述与解码声明 |
|
||||
| `components/platform/src/lcd_emote_assets.c` | 游程压缩解码与输入校验 |
|
||||
| `components/platform/src/generated/emote_gfx/idle_base.c` | 静态鼻子和胡须压缩数据 |
|
||||
| `components/platform/src/generated/emote_gfx/idle_eyes_open.c` | 原始睁眼压缩数据 |
|
||||
| `components/platform/CMakeLists.txt` | 私有组件依赖及实际参与编译的资产列表 |
|
||||
| `main/Kconfig.projbuild` | GFX 待机开关与测量日志开关 |
|
||||
| `tools/split_idle_expression.py` | 从旧三帧中拆出静态底图及眼睛图层 |
|
||||
| `tools/compress_idle_expression.py` | 将底图和睁眼图层转换为压缩 C 资产,并做编码往返检查 |
|
||||
| `tools/test_lcd_status_hud.py` | 状态栏及自动回待机回归测试 |
|
||||
| `tools/test_lcd_emote_transport.py` | 离屏捕获、缓存索引、传输、闭眼弧线和统计开关回归测试 |
|
||||
| `tools/test_lcd_emote_timeline.py` | 固定时间轴、时钟粒度、迟到及取消回归测试 |
|
||||
|
||||
`/tmp` 下修改过程中生成的补丁脚本不是工程运行依赖,不应提交或作为正式资源流程使用。
|
||||
|
||||
## 四、当前待机表情如何运行
|
||||
|
||||
### 1. 原始资产只解码一次
|
||||
|
||||
静态底图和睁眼图层以游程压缩数据进入应用镜像,初始化时解压到 PSRAM。每个游程由四字节组成:数量、RGB565 高字节、低字节、透明度。解压后是 RGB565 色彩平面加独立透明度平面,不是交错的三字节像素。
|
||||
|
||||
压缩合计为 19,336 字节,解压合计为 97,926 字节。后续眨眼不重新解析 PNG,也不重复解压这些数据。压缩不会在每一帧增加解码成本。
|
||||
|
||||
### 2. 网格用于首次生成,不是正常播放时反复绘制
|
||||
|
||||
睁眼图层为 238×93,锚点为 `(41,65)`。网格为 16 列、2 行,共 51 个控制点。闭合程度使用 `closure` 表示,0 为睁眼,255 为闭眼。
|
||||
|
||||
原网格向中线对称收缩,并增加弧度。GFX 会根据网格边界重新计算局部原点,因此代码补偿最小纵坐标偏移,防止眼睛闭合时整体向上漂移。修改网格后不能随意删除这个补偿。
|
||||
|
||||
闭合程度达到 232 后,不再把眼圈压成极薄纹理,而是显示程序绘制的抗锯齿弧线。弧线颜色取自原睁眼资源,避免色差。
|
||||
|
||||
### 3. 离屏生成九个缓存眼型
|
||||
|
||||
`lcd_direct_emote_prepare_cache()` 在同一任务中持有 GFX 锁,分别生成:
|
||||
|
||||
```text
|
||||
0、16、24、64、80、128、160、192、232
|
||||
```
|
||||
|
||||
其中 232 及以上共用闭眼图像。GFX 的真实合成输出通过 `lcd_direct_emote_capture()` 捕获到 PSRAM,预热期间不发送到 LCD,也不计播放帧率。
|
||||
|
||||
缓存窗口是 `(39,63)`,尺寸为 242×97,包含眼睛四周两像素余量。缓存是已经合成的、不透明 RGB565 图像:**其中包含窗口内的静态脸部像素,不是纯透明眼睛层**。修改鼻子、胡须、背景、锚点或配色时,必须同步重新生成缓存,不能拿旧缓存任意叠加到新背景。
|
||||
|
||||
九张缓存总计:`9×242×97×2 = 422,532` 字节。正常播放使用普通图片对象 `s_emote_cached_eyes` 切换来源。重复闭眼阶段不使对象失效、不再次传输。缓存生成失败时释放已分配的缓存,保留实时网格路径。
|
||||
|
||||
### 4. 固定时间轴播放
|
||||
|
||||
当前序列为:
|
||||
|
||||
```text
|
||||
24 → 80 → 160 → 232 → 255 → 255 → 232 → 192 → 128 → 64 → 16 → 0
|
||||
```
|
||||
|
||||
共十二个步骤、九次不同的眼型更新。正常待机先保持睁眼约两秒,再眨眼。
|
||||
|
||||
每轮记录共同起点,第 i 步截止时间为 `起点+(i+1)×34ms`。迟到时减少下一步等待,不把误差继续加到后面的目标时间。不跳眼型,不使用忙等,不修改全局系统时钟粒度。
|
||||
|
||||
十二步目标总长为 408ms,对应约 29.41 步/秒;九次更新除以完整眨眼时长对应约 22.06 次/秒。闭眼保持也占时间,二者都不是面板扫描率。实际仍受系统 10ms 时钟粒度及后台调度影响。
|
||||
|
||||
### 5. 预合成与连续传输
|
||||
|
||||
GFX 使用一个 153,600 字节 PSRAM 合成缓冲。虽容量为整屏,当前仍是分区缓冲模式,不是 RGB 显示的硬件换帧模式。
|
||||
|
||||
普通更新区域先合成完成,再进入 `lcd_direct_emote_flush()`。它将像素复制到一个 10,240 字节内部 DMA 暂存区,每批最多 5,120 像素。每批等中断通知传输完成后才复用暂存区;整个区域发送完再通知 GFX。
|
||||
|
||||
当前缓存图像全窗口更新时,242×97 像素通常需五批传输。这不是一次原子换帧。SPI 完成中断只释放信号量,不计算眼型、不解码、不打印日志。
|
||||
|
||||
DMA 等待超时后禁止继续复用可能仍被硬件读取的缓冲,GFX 传输路径需重启恢复。不能为了“继续显示”而清除故障标志并立即覆盖缓冲。
|
||||
|
||||
## 五、时间、电量及返回待机
|
||||
|
||||
左上显示系统本地时间,尚未同步时显示 `--:--`。当前配置为中国标准时间;时区应通过现有时间服务配置,不在显示模块硬编码。
|
||||
|
||||
右上显示领域层提供的电量百分比、有效性及充电状态。未知时显示 `--%`,充电显示绿色电池及闪电缺口,低电量显示红色。显示层不得从 ADC 电压自行重算电量。
|
||||
|
||||
状态栏复用内置 5×7 点阵数字和程序绘制图标,无需额外字体包。GFX 状态栏为 320×40 RGB565 图层,只在内容变化时刷新。PNG 表情在每帧合成时更新状态;二维码和屏幕边界测试不叠加它。
|
||||
|
||||
当前不能据此声称已实现任意中文文字。后续中文提示需要选择字体、字符子集、布局及字形缓存策略;数字时间和电量不需要引入完整中文字库或大型界面框架。
|
||||
|
||||
开机空闲、显式待机请求、有限动画结束、资源缺失及非取消播放错误都应进入统一待机后端。`lcd_direct_queue_idle_if_available()` 在状态锁内检查新请求和停止状态,不得改回无条件覆盖待处理槽。
|
||||
|
||||
历史 `jichubiaoqing` 请求在 `duration_ms=0` 且 GFX 就绪时也转到新路径,包括服务器错误的有限播放。只有 GFX 禁用或初始化失败才使用旧三帧后备。其他表情无需在这次迁移中全部替换。
|
||||
|
||||
## 六、资源目录、制作和替换
|
||||
|
||||
```text
|
||||
components/platform/assets/
|
||||
lcd_source/ 旧 PNG 源图,仍保留
|
||||
lcd_layers/jichubiaoqing/ 可编辑拆分图层、布局及预览
|
||||
components/platform/src/generated/emote_gfx/
|
||||
idle_base.c 实际编译的底图压缩数据
|
||||
idle_eyes_open.c 实际编译的睁眼压缩数据
|
||||
```
|
||||
|
||||
半闭眼、闭眼源图及对应历史生成文件不代表当前参与编译;以构建列表为准,不把目录里的全部文件自动加入固件,也不擅自删除旧资源。
|
||||
|
||||
在仓库根目录,确认 Python 环境有 Pillow 后,可执行:
|
||||
|
||||
```bash
|
||||
python3 tools/split_idle_expression.py
|
||||
python3 tools/compress_idle_expression.py
|
||||
```
|
||||
|
||||
第一条会从旧三帧重新生成拆分图层和预览,**会覆盖该目录中手工改过的图层**。仅替换已经拆好的图层时,通常只运行第二条。生成前先查看差异,保留美术编辑成果。
|
||||
|
||||
第二条会重新生成底图和睁眼的 C 压缩资产,并校验编码往返。当前脚本明确要求睁眼尺寸为 238×93,眼睛锚点固定为 `(41,65)`;它不是能适配所有角色的通用生成器。生成后的 C 数组不应手改。
|
||||
|
||||
替换资源时需要核对:
|
||||
|
||||
1. 原画布、裁剪尺寸、透明层和锚点是否一致。
|
||||
2. 两只眼睛中心、闭合中线、弧度和闭眼宽度是否仍适用;这些目前在代码中有专用参数。
|
||||
3. 新动作是否超出缓存窗口,是否会盖住时间、电量或静态脸部。
|
||||
4. 动作所有阶段是否在缓存表中;新增阶段不能静默复用错误眼型。
|
||||
5. 编码格式、颜色高低字节、透明度平面及显示字节交换是否一致,避免重复交换导致颜色错误。
|
||||
6. PNG 资产打包与 GFX 生成资产是两条不同管线;相关步骤参考现有素材指南。不能把图层 PNG 放进目录就认为 GFX 已经接入。
|
||||
|
||||
## 七、资源开销与取舍
|
||||
|
||||
| 当前常驻对象 | 明确像素缓冲占用 |
|
||||
| --- | ---: |
|
||||
| 解压后的底图与睁眼图层 | 97,926 字节 PSRAM |
|
||||
| 程序闭眼透明纹理 | 9,996 字节 PSRAM |
|
||||
| 时间电量图层 | 25,600 字节 PSRAM |
|
||||
| GFX 合成缓冲 | 153,600 字节 PSRAM |
|
||||
| 九个眼型缓存 | 422,532 字节 PSRAM |
|
||||
| 上述合计 | 709,654 字节 PSRAM |
|
||||
| GFX DMA 暂存区 | 10,240 字节内部 DMA 内存 |
|
||||
|
||||
此表不含对象管理、任务栈、SPI 描述符、PNG 缓存和系统其他模块。不能把表中合计当作整个显示组件的完整运行内存开销。
|
||||
|
||||
压缩源图像像素数据为 19,336 字节,不含描述信息和组件代码。运行时眼型缓存不写入 Flash。最新本地构建应用镜像为 3,040,480 字节,OTA 槽为 4,718,592 字节;这是当次构建快照,不是后续版本承诺。
|
||||
|
||||
实机日志曾显示联网、唤醒同时运行时,内部内存最低只剩约十 KiB,而 PSRAM 尚有数 MiB。后续优先考虑 PSRAM 和局部复用,不能因为外部内存充足就不断扩大 DMA 或内部任务栈。
|
||||
|
||||
## 八、性能日志如何读
|
||||
|
||||
通过 `menuconfig` 搜索 `TQ_LCD_EMOTE_GFX_TEST` 和 `TQ_LCD_EMOTE_FPS_LOG`。前者控制 GFX 待机路径;后者控制性能统计,默认关闭,当前本地测量配置开启。关闭统计不应改变动画节奏。
|
||||
|
||||
每轮完整眨眼输出一次 `emote_fps` 和 `emote_cost`,不在逐帧传输中打印:
|
||||
|
||||
- `blink_us`:真实眨眼总耗时,不含此前睁眼停留和该条日志输出。
|
||||
- `step_fps`:步骤数除以眨眼秒数;保持步骤也计入。
|
||||
- `eye_updates`:眼型步骤中确实完成至少一个区域传输的次数;多个 DMA 块不会重复计数。
|
||||
- `update_fps`:实际更新次数除以同一眨眼时长,不是仅运动段的瞬时频率。
|
||||
- `draw_avg_us`、`draw_max_us`:步骤绘制时间;平均包含没有传输的保持步骤。
|
||||
- `late_steps`:绘制达到或超过 34ms 的步骤数,不等于屏幕丢帧数,也不统计所有调度迟到。
|
||||
- `lock`:等待 GFX 锁;`compose`:眼型修改及合成、刷新处理,扣除复制和传输;`copy`:复制到 DMA;`spi`:提交并等待传输完成。
|
||||
|
||||
各项均为墙钟时间,可能包含被其他任务抢占。各项最大值可能来自不同步骤,不能相加。不能仅凭合成时间大就断言算法慢,也不能仅凭 SPI 等待大就断言线路带宽不足。
|
||||
|
||||
缓存前低负载合成平均约 19ms,缓存后约 1.4~1.8ms;缓存后前期更新吞吐率约 18.7~19.2 次/秒,后台任务运行后多数约 17 次/秒。这些是**固定时间轴修改之前**的设备样本,不应拿来宣称当前版本最终实测帧率。
|
||||
|
||||
## 九、后续开发建议
|
||||
|
||||
### 接入更多表情
|
||||
|
||||
先逐个迁移,保留未迁移 PNG 路径。每个表情准备静态层、动态图层、锚点、时间线、缓存预算及结束行为。固定短动作适合预生成缓存;大量不同动作或连续交互需要考虑按需生成、共享缓存和生命周期,不应给每个表情无上限常驻一份整屏缓存。
|
||||
|
||||
建议把几何参数和时间线整理为平台私有描述结构,避免每个表情复制一套线程、锁和 SPI 逻辑。表情语义仍由领域目录管理,不要往平台层添加服务器、联网或业务状态判断。
|
||||
|
||||
### 后续状态控制
|
||||
|
||||
当前不是完整状态机。建议控制层决定待机、监听、响应、联网、更新等状态何时进入和退出;领域层管理语义表情及触发原因;平台层执行播放、取消、完成及安全恢复。
|
||||
|
||||
改造前先定义取消与完成的区别、临时表情结束后恢复什么、并发请求优先级及超时语义。不能把停止或被抢占也记为成功完成,不能用“统一返回待机”覆盖正在执行的新状态。若需要恢复上一业务状态,而不是一律待机,应由控制层明确决定。
|
||||
|
||||
### 缩小缓存传输区域
|
||||
|
||||
当前每个缓存眼型传输固定窗口。下一阶段可按眼型裁剪,但必须刷新旧、新区域并集并恢复背景。由于缓存包含静态像素,不能只缩小新图片后留下旧眼圈。先验证残影、边界和状态栏遮挡,再比较吞吐率。
|
||||
|
||||
### 生命周期与解耦
|
||||
|
||||
缓存和 GFX 对象当前随 LCD 实例常驻,停止渲染循环不等于完整释放 GFX 引擎、图层和缓存。后续若支持重复初始化、热换资源或停机释放,需要明确停止、等待传输、注销对象、停止引擎和释放内存的顺序,补充错误注入测试。不要宣称当前已经具备完整热替换和销毁机制。
|
||||
|
||||
源码中的锁与显示完成等待来自现有组件实现。后续新增生命周期接口仍需有明确超时,不能把内部阻塞接口包装成没有边界的公开停止接口。
|
||||
|
||||
## 十、验证与交付
|
||||
|
||||
在仓库根目录执行:
|
||||
|
||||
```bash
|
||||
python3 tools/test_lcd_status_hud.py
|
||||
python3 tools/test_lcd_emote_transport.py
|
||||
python3 tools/test_lcd_emote_timeline.py
|
||||
```
|
||||
|
||||
这些脚本在电脑上从实际源码提取 C 函数,使用 Clang 与内存、未定义行为检查器。需要 Python 和 Clang;不是固件代码,不占设备 Flash 或 RAM。测试使用模拟接口,不能证明屏幕无撕裂、实时帧率或设备像素完全一致。
|
||||
|
||||
按根目录 `AGENTS.md` 激活 ESP-IDF 5.5.4,再执行 `idf.py build`。修改日志条件编译时还要检查开启与关闭两种配置,不能只验证当前开启的配置。
|
||||
|
||||
实机至少验证:
|
||||
|
||||
1. 缓存成功日志、首次生成时间及回退路径。
|
||||
2. 固定时间轴版本的帧率与分项耗时,低负载和唤醒、网络同时运行各采样。
|
||||
3. 向中间闭眼,无上移、残影、明显颜色差异或缓存窗口截断。
|
||||
4. 各表情切换后正确恢复待机,新请求不被旧动画结束覆盖。
|
||||
5. 跨分钟、首次时间同步、电量变化、充电插拔和二维码页面。
|
||||
6. 语音采集、唤醒、网络无明显退化;内部、DMA 和 PSRAM 的最低余量及分配失败。
|
||||
7. 新增停止或资源释放功能时的取消、超时和反复启动测试。
|
||||
|
||||
交付时分别报告源码检查、主机测试、编译和实机证据。不能把“编译通过”写成“设备已验证”,也不能只用目标帧率或一次录像承诺持续性能。
|
||||
Reference in New Issue
Block a user