# GFX 表情显示应用与开发指南 本文面向本项目的开发者,说明当前 `esp_emote_gfx` 的具体应用及后续开发方法。正文使用中文,代码、路径和日志字段保留原名以便检索。适用基线包含 2026 年 10 月 10 日的抽签分层动画;资源和性能数字随代码变化而变化。 ## 一、当前方案与边界 本项目没有触摸和复杂界面交互,显示需求以精致表情、时间和电量为主。当前方案是轻量图层渲染,不是完整界面框架迁移。 - 底层为 ESP32-S3、ST7789P3、单线 SPI,当前像素时钟为 40 MHz,逻辑画布为 320×240。 - 当前使用 `esp_emote_gfx` 3.0.5,由 `platform` 私有依赖。没有接入 `esp_emote_expression` 状态管理组件,也没有把当前表情绘制建立在 LVGL 上。依赖目录中存在其他库不等于当前显示链路使用它们。 - 待机基础表情、listening 与五种抽签结果采用 GFX 图层;其他表情仍走原 PNG 或步骤序列,不是所有资产都已经迁移。listening 缓存失败时单独回退旧 PNG,不影响待机。抽签只在开始画面前准备失败时回退,播放途中取消/传输失败不从头重播 PNG。 - 屏幕目前未使用 TE 同步。软件预合成和连续传输能改善撕裂观感,但不能保证面板扫描与写入完全同步。 - 用户已确认待机缓存/固定时间轴以及新 listening 版本的观感满意。2026-10-10 启动样本中的待机眨眼约 410 ms、步骤约 29.26/s、实际眼型更新约 21.95/s;不是 listening/抽签帧率或全负载验证,不能写成“全部实机验收完成”。 已安装组件提供图片、网格图片、运动、动画、标签、二维码和按钮等接口。但本项目仅实际使用图片、网格、显示刷新及相关对象控制;通用中文文本、按钮、触摸和复杂布局不属于当前已实现能力。 ## 二、分层与调用入口 ```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 类型不得泄漏到上层公共接口。 当前请求采用单个待处理槽,不是完整优先级仲裁器。请求结构虽有优先级字段,也不能据此假定已经实现“低优先级自动排队”。 ## 三、关键文件与职责 现有 LCD GPIO 与背光有效极性集中在 `menuconfig` 的 `TQ Cat Configuration -> Board Hardware Configuration (GPIO)` 页面, 与音频、输入、电机和电源引脚分区展示。显示功能、颜色、亮度及 GFX/性能日志 仍在原显示菜单。2026-10-23 已同步新板 LCD 映射:BLK/CS/DC/RES/MOSI/CLK 为 GPIO4/5/6/7/15/16;旧 GPIO4 RMT、GPIO5 脉冲不再随平台初始化启动。 配置名和依赖保持不变;未新增 GPIO8 充满检测或 GPIO10 摇臂控制选项。 BLK 按新图作为使能使用,默认 100%;调光与显示/电机/音频并行仍需实机验收。 | 文件 | 职责 | | --- | --- | | `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 待机/listening 开关与测量日志开关 | | `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` | 固定时间轴、时钟粒度、迟到及取消回归测试 | | `components/platform/src/lcd_emote_listening.c` | listening 关键眼型的逐列几何插值,仅初始化生成缓存时运行 | | `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` 下修改过程中生成的补丁脚本不是工程运行依赖,不应提交或作为正式资源流程使用。 ## 四、当前待机表情如何运行 ### 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 电压自行重算电量。旧底部低电量提示保留。 时间改用现有 LVGL 依赖中的 Montserrat 14 字形,仅提取 `0..9`、冒号及横线, 不缩放,保留原始 4-bit Alpha。白字及黑色偏移阴影直接合成到已有缓冲, 并非创建 GFX label,也不启动 LVGL GUI。字形数组常量位于 Flash,无动态字形缓存。 电池使用用户提供的 `battery_icons.zip` 中 40×20 的十张透明 PNG(五档及充电版本), 位于 `(272,12)`(320 像素宽时);不采用 80×40 版本,不拉伸、不改图标画风。 电量分档:0~10→1、11~25→25、26~50→50、51~75→75、76~100→100。 图标名表示视觉档位,不是重新计算 SOC;充电状态选择对应带闪电版本。 PNG 原图和优化图均保留,但实际编译为 RGB565+A8 调色板+游程数组, 每条游程为“数量、调色板索引”,与表情的四字节 RLE 不同。 在 Flash 中流式读取并按 Alpha 混合,不解码成常驻纹理。 GFX 状态栏仍为 320×40 RGB565 图层,只在内容变化时刷新。PNG 表情在每帧合成时 更新状态;二维码和屏幕边界测试不叠加它。两种后端使用同一字体和图标。 资源生成:`tools/prepare_status_hud_assets.py`;原图及优化图: `components/platform/assets/status_hud/battery_source/` 与 `battery_optimized/`; 实际编译:`components/platform/src/generated/lcd_status_assets.c`,私有描述见 `components/platform/internal/lcd_status_assets.h`。运行生成脚本会重建优化 PNG/C, 原 PNG 不覆盖;首次导入可指定 `--import-zip <压缩包路径>`,日常验证用 `--check`。 需要 Pillow;字形来自现有依赖,不需要下载新字体。 本次 HUD 构建镜像 3,105,872 B,相对前一版 3,094,176 B 增加 11,696 B。 生成资产的 ESP32-S3 对象只读数据为 12,083 B:字形位图 425 B、字形描述 96 B、 十图标压缩载荷 11,442 B、图标描述 120 B。原十 PNG 9,973 B,优化后 9,806 B; 压缩载荷仍比优化 PNG 大 1,636 B,选择它是为避免 PNG 解码及额外像素缓存, 不能称作最小 Flash。新增常驻/初始化像素 PSRAM、普通堆、静态可写 RAM、DMA 均为 0;复用原状态栏/行缓冲,仍有局部栈和绘制 CPU 成本,需实机测量。 四个主机显示测试、ESP-IDF 5.5.4 构建及日志关闭/GFX 关闭的 LCD 编译均通过; 字体大小、图标显示、充电插拔及最低内存仍待实机验收。 当前不能据此声称已实现任意中文文字。后续中文提示需要选择字体、字符子集、布局及字形缓存策略;数字时间和电量不需要引入完整中文字库或大型界面框架。 开机空闲、显式待机请求、有限动画结束、资源缺失及非取消播放错误都应进入统一待机后端。`lcd_direct_queue_idle_if_available()` 在状态锁内检查新请求和停止状态,不得改回无条件覆盖待处理槽。 历史 `jichubiaoqing` 请求在 `duration_ms=0` 且 GFX 就绪时也转到新路径,包括服务器错误的有限播放。只有 GFX 禁用或初始化失败才使用旧三帧后备。其他表情无需在这次迁移中全部替换。 ## 六、资源目录、制作和替换 ### PNG、RLE 与运行时缓存不是同一份文件 PNG 是源素材格式,也是旧播放路径的压缩格式。新 GFX 路径从 PNG 生成 RLE 数组;不存在独立 `.rle` 文件,压缩字节保存在 `generated/emote_gfx/*.c` 中。 RLE(游程长度编码)把连续相同像素写成“数量+像素值”。本项目一条记录四字节: 数量(1~255)、RGB565 高字节、RGB565 低字节、Alpha。例如 `0A 00 00 FF` 表示十个不透明黑色像素,替代十份 RGB565+A8 像素的三十字节。 颜色/透明度频繁变化时 RLE 可能压得较差,甚至膨胀;PNG 的压缩可能更紧凑。 | 路径 | 编辑输入 | 固件中的内容 | 运行时处理 | | --- | --- | --- | --- | | 普通 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 像素完全不变。RGB565 转换、背景归零、裁剪及性能适配是另一层处理,须单独 验证视觉并保留可编辑源图。九张待机与九张 listening 运行时缓存只在 PSRAM 中, 没有对应的固件图片或磁盘缓存文件;离线 PNG/GIF 预览不能算成这些缓存的 Flash。 ### 当前目录与准确入口 ```text components/platform/assets/ lcd_source/ 原 PNG 源图,仍保留,不直接入包 lcd_optimized/ 原 PNG 路径实际入包图片及 manifest audio_prompts/ 实际入包提示音 lcd_layers/ jichubiaoqing/ 待机可编辑拆分图层、布局及预览 listening/ 聆听可编辑拆分图层、布局及预览 chouqian/ 抽签共享图层、五种原签文及布局 components/platform/src/generated/emote_gfx/ idle_base.c 实际编译的底图压缩数据 idle_eyes_open.c 实际编译的睁眼压缩数据 listening_base.c 实际编译的眉毛、鼻子、胡须数据 listening_eyes_open.c 实际编译的聆听睁眼关键图 listening_eyes_half.c 实际编译的聆听半闭关键图 listening_eyes_closed.c 实际编译的聆听闭眼关键图 fortune_assets.c 实际编译的抽签 LZ4;不是 RLE ``` 半闭眼、闭眼源图及对应历史生成文件不代表当前参与编译;以构建列表为准,不把目录里的全部文件自动加入固件,也不擅自删除旧资源。 这里指待机历史 `idle_eyes_half.c`、`idle_eyes_closed.c`,不是 listening 的 半闭/闭眼 C 文件:后两者当前确实参与编译。逐文件大小、入固件分类、音频及 源图/预览汇总见 [显示与音频资产台账](显示与音频资产台账.zh-CN.md)。 - 待机拆分/压缩:`tools/split_idle_expression.py`、`tools/compress_idle_expression.py`。 - listening 拆分、压缩与预览:`tools/prepare_listening_expression.py`。 - RLE 解码:`components/platform/src/lcd_emote_assets.c`。 - 聆听关键图几何插值:`components/platform/src/lcd_emote_listening.c`。 - 缓存与播放:`components/platform/src/lcd_direct.c`。 - PNG 后备优化:`tools/optimize_lcd_assets.py`,流程见 [图片优化说明](lcd-image-asset-optimization.zh-CN.md)。 在仓库根目录,确认 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 内存 | 此表是待机与共用缓冲的基线,不包含新增 listening。它也不含对象管理、任务栈、SPI 描述符、PNG 缓存和系统其他模块。不能把表中合计当作整个显示组件的完整运行内存开销。 待机压缩源图像像素数据为 19,336 字节,不含描述信息和组件代码。运行时眼型缓存不写入 Flash。接入 listening 前应用镜像为 3,040,480 字节,接入后为 3,094,176 字节,新增 53,696 字节;OTA 槽为 4,718,592 字节,剩余 1,624,416 字节。这是当次本地构建快照,不是后续版本承诺。 实机日志曾显示联网、唤醒同时运行时,内部内存最低只剩约十 KiB,而 PSRAM 尚有数 MiB。后续优先考虑 PSRAM 和局部复用,不能因为外部内存充足就不断扩大 DMA 或内部任务栈。 ### 压缩优化的判定与预算限制 新增或替换素材必须做适合编码的压缩优化、数值比较及视觉验证,不强制 PNG 或 RLE,不默认“接入 GFX=Flash 更小”。RLE 只是简单解压编码;当前播放快主要 依靠初始化生成局部缓存,不能把性能收益全部归因于 RLE,也不能断言 PNG 无法 在初始化解码后缓存。未来可评估 PNG/其他编码初始化解码生成同样缓存的成本。 listening 三张旧原 PNG 共 45,445 字节,已有优化 PNG 共 14,681 字节。 新底图及三个关键图未压缩像素为 255,528 字节,RLE 载荷为 50,176 字节, 相对未压缩像素减少约 80.4%,但比旧优化 PNG 大 35,495 字节。这几个比较对象 并不完全相同:旧图是三个整屏画面,新图是固定底图加三个可变眼型。 旧优化 PNG 仍作后备,因此本次没有从固件扣掉它们,完整镜像实际增加 53,696 字节。 当前方案不是最小 Flash 方案,是用户已接受的空间换流畅度方案。 所有迁移应报告优化前后素材、固件增量、常驻/峰值 PSRAM、静态数据、普通堆及 内部/DMA 增量,说明效果和测量证据。不按源码 C 文件大小推算 Flash,也不把 PSRAM 外部内存充足当成内部 DMA 安全。当前 listening 的已接受基线是固件 增量 53,696 字节、常驻像素 PSRAM 增量 423,234 字节,不是所有表情的硬上限。 超出该表情已确认预算、增加内部/DMA 缓冲、或可能影响 OTA/语音/网络时,先 给出预算、风险和替代方案并取得用户确认;新增表情尚无预算时先提出预算。 ### 旧资源保留、整理与迁移完成条件 - 可编辑原 PNG 保留;离线预览和拆分 PNG 不入固件。旧待机/listening 的优化 PNG 仍入包,提供关闭 GFX、初始化/缓存失败等后备,不是可以直接删除的重复文件。 - 未参与编译的历史待机 C 文件、旧预览和 `defaultAudio/` 副本仅标记为待评估, 不因“历史”标签自动删除。实际入包音频来自 `platform/assets/audio_prompts/`。 - 迁移完成至少包括:语义入口与退出/取消一致、正常画面/状态栏验收、故障及 关闭开关时的后备行为明确、联网/语音并行与最低内存记录、生成可重现、文档 和台账同步。用户观感满意仅覆盖其中一项,不自动满足删除后备的条件。 - 删除或移出固件前先列清文件、引用、回退替代方案及预计收益,取得用户授权; 再同步构建清单/资源目录映射、相关测试和台账。保留 Git 可恢复记录并验证 正常与故障路径。清理只是后续计划,本次没有删除资源或改变打包范围。 ## 八、性能日志如何读 通过 `menuconfig` 搜索 `TQ_LCD_EMOTE_GFX_TEST` 和 `TQ_LCD_EMOTE_FPS_LOG`。前者控制 GFX 待机、listening 及抽签路径;后者控制性能统计,默认关闭,当前本地测量配置开启。关闭统计不应改变动画节奏。抽签使用 `fortune_fps`,只计算运动段、不含最后停留,详见抽签开发说明。 每轮完整眨眼输出一次 `emote_fps` 和 `emote_cost`,不在逐帧传输中打印: - `blink_us`:真实眨眼总耗时,不含此前睁眼停留和该条日志输出。 - `step_fps`:步骤数除以眨眼秒数;保持步骤也计入。 - `eye_updates`:眼型步骤中确实完成至少一个区域传输的次数;多个 DMA 块不会重复计数。 - `update_fps`:实际更新次数除以同一眨眼时长,不是仅运动段的瞬时频率。 - `draw_avg_us`、`draw_max_us`:步骤绘制时间;平均包含没有传输的保持步骤。 - `late_steps`:绘制达到或超过对应时间线步长的步骤数(待机 34ms、listening 30ms),不等于屏幕丢帧数,也不统计所有调度迟到。 - `lock`:等待 GFX 锁;`compose`:眼型修改及合成、刷新处理,扣除复制和传输;`copy`:复制到 DMA;`spi`:提交并等待传输完成。 各项均为墙钟时间,可能包含被其他任务抢占。各项最大值可能来自不同步骤,不能相加。不能仅凭合成时间大就断言算法慢,也不能仅凭 SPI 等待大就断言线路带宽不足。 缓存前低负载合成平均约 19ms,缓存后约 1.4~1.8ms;缓存后前期更新吞吐率约 18.7~19.2 次/秒,后台任务运行后多数约 17 次/秒。这些是**固定时间轴修改之前**的设备样本,不应拿来宣称当前版本最终实测帧率。 ## 九、后续开发建议 ### 已接入的 listening 旧实现每张停留 500ms,顺序为睁眼、半闭眼、闭眼;新实现保留三种斜眼造型, 并固定第一张的眉毛、鼻子、胡须。背景中的细小杂色归零,旧源图和打包 PNG 全部保留。 动态图层窗口为 `(41,87)`,尺寸 242×77,包含闭眼右端延伸及边缘余量。 Flash 中只保存静态底图和三个关键眼型,不保存九张缓存。初始化时按列提取两张 相邻关键图的上下轮廓,插值出目标轮廓,再变形单张纹理,并做纵向亚像素采样。 不是两张表情交叉淡化;避免高光双影和眼圈灰化。闭合 0、128、232 分别精确使用 睁眼、半闭眼、闭眼关键图,232 以上共用闭眼缓存。 生成阶段的三张 RGB565A8 关键图共 167,706 字节 PSRAM,生成完释放。底图和九个 局部 RGB565 缓存常驻;缓存中的黑色眼睛外区域恢复眉毛/胡须静态像素,不能任意 叠加到不同底图。播放时没有 PNG 解码、关键图解压或逐列变形。 | listening 新增项 | 数值 | | --- | ---: | | 静态底图压缩数据 | 8,732 字节 Flash | | 睁眼、半闭眼、闭眼压缩数据 | 24,036 / 13,236 / 4,172 字节 Flash | | 四份压缩数据合计 | 50,176 字节 Flash | | 整个应用镜像实际增量,含新增代码与对齐 | 53,696 字节 Flash | | 静态底图解压后 | 87,822 字节 PSRAM | | 九个局部眼型缓存 | 335,412 字节 PSRAM | | 新增常驻像素合计 | 423,234 字节 PSRAM | | 初始化新增像素峰值,含临时关键图 | 590,940 字节 PSRAM | | LCD 文件静态数据/BSS 增量 | 333 字节 | | 新图片对象、图片状态及链表节点申请量 | 80 + 8 + 8 = 96 字节普通堆,不含分配器开销 | | 新 DMA 缓冲、新合成缓冲、新任务 | 0,复用原有实现 | 上述静态/对象字节数通过当前 ESP32-S3 编译器核对,不包含链接段对齐、堆分配 管理、栈瞬时使用或绘制临时分配。普通堆的实际放置由 ESP-IDF 内存策略决定。 待机加 listening 的明确常驻像素缓冲总计为 1,132,888 字节 PSRAM,仍不等于整个 显示组件总内存。初始化峰值及联网时真实余量需要设备日志验证。 新时间线是 `24→80→160→232→232→192→128→64→16→0`,共十步, 每步 30ms,锚定共同起点;目标眨眼 300ms,闭眼保持约 60ms,睁眼停留约 2.4 秒。 同一闭眼缓存重复保持不触发新传输。目标步骤频率不是实测 FPS,也不是面板扫描率。 `emote_fps` 日志新增 `face=idle/listening` 区分后端,仍由原测量开关控制。 领域目录和控制层监听状态不变:按键/本地唤醒进入时展示 listening,直到新请求 或停止;它不严格等于录音已经开始。眨眼结束不自动回待机。退出后下一次待机请求 恢复待机静态层与眼型;切换时完整刷新一次,之后只刷新眼睛及变化的状态栏。 缓存失败释放未绑定像素并保留旧 PNG;极端解锁错误下已绑定对象的像素保留到重启, 不能释放后仍被渲染线程引用。GFX 总开关关闭时仍使用原 PNG。 生成资源与预览(需要 Pillow 和 Clang,覆盖本次生成的 listening 图层与资产): ```bash python3 tools/prepare_listening_expression.py python3 tools/test_lcd_listening_gfx.py ``` 可编辑 PNG、布局和预览在 `components/platform/assets/lcd_layers/listening/`; 实际编译的四个 `listening_*.c` 在 `components/platform/src/generated/emote_gfx/`。 动画预览 `preview.gif` 调用实际 C 插值生成,但没有模拟 LCD 扫描与语音负载。 固定窗口/锚点变化需同步脚本与测试。需要手工保留眉毛等素材编辑时,不要直接重新 拆分覆盖;当前生成脚本不是所有角色通用的热替换工具。 ### 已接入的五种抽签结果(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 路径。每个表情准备静态层、动态图层、锚点、时间线、缓存预算及结束行为。固定短动作适合预生成缓存;大量不同动作或连续交互需要考虑按需生成、共享缓存和生命周期,不应给每个表情无上限常驻一份整屏缓存。 建议把几何参数和时间线整理为平台私有描述结构,避免每个表情复制一套线程、锁和 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 python3 tools/test_lcd_listening_gfx.py python3 tools/test_lcd_fortune_gfx.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. 新增停止或资源释放功能时的取消、超时和反复启动测试。 交付时分别报告源码检查、主机测试、编译和实机证据。不能把“编译通过”写成“设备已验证”,也不能只用目标帧率或一次录像承诺持续性能。 ### 对话需求与验证记录 | 需求或反馈 | 当前结果与边界 | | --- | --- | | 不需要触摸/复杂 GUI,精致流畅为主 | 局部 GFX 图层,不迁移完整 LVGL GUI | | 拆分原表情,允许性能适配但整体保持 | 待机/listening 拆分与固定静态层;后续延续原画风,大幅变化需确认 | | 眼睛向中间闭合,避免重影和撕裂 | 单一轮廓、闭眼弧线、缓存和连续传输;没有 TE,不能承诺零撕裂 | | 统一所有返回待机入口、补时间电量 | 已实现统一后端及 HUD;不能覆盖新 pending;Montserrat 数字子集不代表任意中文文字 | | 时间使用 Montserrat 14、移除电量数值、使用提供的电池图标 | 直接合成字形子集和 40×20 图标;构建与主机检查通过,待实机验收 | | 增加可开关日志、计算具体帧数 | 已有 emote_fps/emote_cost;区分步骤/实际更新/面板扫描率 | | 全中文开发说明与 AI 注意事项 | 本指南、资产台账及 AGENTS.md 中文 GFX 约束;原有仓库架构规范保留 | | listening 也迁移为流畅表情并报告开销 | 已接入,开销见上表;保持业务监听生命周期与旧 PNG 后备 | | 用户确认待机与 listening 效果满意 | 2026-10-09 对话中确认;没有据此声称最终量化 FPS/最低内存及全部场景合格 | | 素材需要压缩优化与成本限制 | 已补通用强制流程、PNG/RLE 权衡与先确认预算规则 | 此前缓存/时序及中文指南提交为 `89e8e3a`、`c0770ac`;本次基线包含 listening 接入及补充文档。完整状态机、任意中文字体、其他表情批量迁移、 资源清理/热替换是后续方向,不标成已经实现,也不推断用户已要求立即实施。 ### 2026-10-23 新板 GPIO 映射验证 已更新现有 LCD/音频/按键/步进/电池引脚默认值和本地有效配置,保持现有 GFX 机制与资产不变;旧 GPIO4/5 输出模块不再启动。电池驱动允许 GPIO17 对应的 ADC2, 未新增 GPIO8/10 配置。五个显示回归脚本(含其 profiling 开/关分支)、电池 ADC 初始化主机测试和 ESP-IDF 5.5.4 构建通过。未烧录;仍需实机核对背光使能、 画面切换/取消、跨分钟、充电/唤醒,以及电机/音频/网络并行,不能据此声称新板 实测 FPS、触摸电平或 ADC 精度已验收。