Files
lucky-cat/docs/GFX表情显示应用与开发指南.md

34 KiB
Raw Permalink Blame History

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/抽签帧率或全负载验证,不能写成“全部实机验收完成”。

已安装组件提供图片、网格图片、运动、动画、标签、二维码和按钮等接口。但本项目仅实际使用图片、网格、显示刷新及相关对象控制;通用中文文本、按钮、触摸和复杂布局不属于当前已实现能力。

二、分层与调用入口

控制层决定何时展示表情
    → 领域层请求语义表情并查目录
        → 平台层接收通用播放请求
            → LCD 渲染任务串行播放
                → GFX 合成或 PNG 合成
                    → SPI 写入屏幕

上层通过领域层请求,不直接调用 GFX 或面板接口。例如返回待机:

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 待机/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 锁,分别生成:

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. 固定时间轴播放

当前序列为:

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。

当前目录与准确入口

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 文件:后两者当前确实参与编译。逐文件大小、入固件分类、音频及 源图/预览汇总见 显示与音频资产台账。

  • 待机拆分/压缩: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,流程见 图片优化说明。

在仓库根目录,确认 Python 环境有 Pillow 后,可执行:

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 图层与资产):

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 完整保留。详见 抽签开发说明。

新载荷 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 引擎、图层和缓存。后续若支持重复初始化、热换资源或停机释放,需要明确停止、等待传输、注销对象、停止引擎和释放内存的顺序,补充错误注入测试。不要宣称当前已经具备完整热替换和销毁机制。

源码中的锁与显示完成等待来自现有组件实现。后续新增生命周期接口仍需有明确超时,不能把内部阻塞接口包装成没有边界的公开停止接口。

十、验证与交付

在仓库根目录执行:

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 接入及补充文档。完整状态机、任意中文字体、其他表情批量迁移、 资源清理/热替换是后续方向,不标成已经实现,也不推断用户已要求立即实施。