Files
o62lte-customising-plan/s4e-pressure-anim/README.md
T
2026-10-02 21:34:56 +08:00

246 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# S4PAnim —— 把 S4 eSIM 的压力检测动画换成 S5 41mm 的
**一句话**:用 Lua 表盘把 S4 eSIM(o62lte)`/dev/app` 里压力 App 的 **119 帧检测动画**
等长覆盖成 S5 41mm(q63 / OS4)那套 **旋转渐变球**。
**两份盘,各 ~16 MB,共用一个 watchface id** —— 装其中一份就顶掉另一份,设备上始终只占一份空间:
| 文件 | 干什么 | 体积 / sha256 |
|---|---|---|
| `dist/S4PAnim.face` | **只负责替换**:把 q63 的 119 帧全部写进 119 个槽位 | 16,686,680 B / `ce73d9ede1c9a574…`(v8) |
| `dist/S4PAnimRestore.face` | **只负责还原**:把原厂字节写回同样的槽位 | 16,686,680 B / `d16b5cfda35089b9…`(v8) |
两份都是 id `362150102`、b5=0。装完一份想换另一份,直接装另一份 .face 即可(同 id 互相顶替)。
## 为什么要拆
合在一份里 = 负载 15.5 MB + 回滚 15.5 MB = **31.4 MB**,而原厂最大的 `resource.bin` 才 9.4 MB。
拆开后每份只有 **15.9 MB**,且因为 id 相同,设备上不会同时存在两份。
拆了以后**版本校验没丢**:每份盘里额外带一张 **117×64 B 的指纹表**(7.5 KB,就是另一侧字节的前 64 字节),
写入前照样能判断「设备上现在是原厂还是改后」,写入后照样整段回读比对。
## 源 / 目标
| | 分区 | 帧 | 格式 | 合计 |
|---|---|---|---|---|
| **目标** S4 eSIM(o62lte) | `inputs/target/vela_app.bin`(`/dev/app`) | `pressure/measure/Measuring1..119.bin` | 464×464 I8 **RLE** | 15.5 MB |
| **源** S5 41mm(q63 v4.101.020) | `inputs/source/vela_app.bin` | `pressure/measure/Measuring1..100.bin` | 480×480 I8 **RLE** | 17.8 MB |
两代动画完全不是一套东西:eSIM 是「青绿气泡群脉冲」(119 帧一次呼吸),q63 是「蓝绿渐变球 + 游走高光」(100 帧一循环)。
## 怎么做到等长覆盖
ROMFS 里每个文件的数据长度固定(后面所有文件的偏移都是绝对值),所以槽位必须**逐字节等长**替换;
而两边尺寸不同、还都是 RLE,压缩后长度各不相同。三步:
1. **缩放 + 量化**:q63 帧 → 464×464,按 (颜色数, 缩放比例) 阶梯挑一档使编码 ≤ 槽位 csize;
先把接近黑的背景(亮度 <40)钉成纯黑 —— 既干净又大幅缩短 RLE(**91/116 帧因此能上 256 色**,压平前只有 53 帧)
2. **编码**:LVGL RLE 语法(`tools/lvgl_bin.py: rle_compress`)
3. **补齐**:`pad_rle_safe` 用两类「产出 0 像素」的合法指令把流补到 csize
(`00 00` = 重复 0 次;`80` = 原样 0 个,用来修奇偶)—— 原厂自己也是这么补的(stock 流尾部就是一片 `00 00`)
## 本机实测
| 指标 | replace 盘 | restore 盘 |
|---|---|---|
| 记录数 | 117(116 数据 + 1 canary) | 120(119 数据 + 1 canary) |
| 等长 | 116 条全部 == 原槽位长度 | 119 条全部 == 原槽位长度 |
| 可解码 | 116/116 解出 464×464 | 119/119 |
| 字节 | 与改后镜像逐字节一致 | 与原厂镜像逐字节一致 |
| 指纹 | 117 条 64 B,= 原厂前 64 B | 120 条 = 改后前 64 B |
| ROMFS | inode 表 / superblock / 条目数 / 分区长度逐位不变 | 同左 |
| 独立校验 | `tools/verify_anim.py replace` **11/11 通过** | `tools/verify_anim.py restore` **11/11 通过** |
替换质量:256 色 91 帧、192 色 1、128 色 1、96 色 2、64 色 1、32 色 5、24 色 3、16 色 5、12 色 3、8/6/3/2 各 1;
最末尾 3 个槽位预算太小(4.5 KB 级)保留原厂。见 `docs/anim_before_after.png` 与 `docs/anim_new.mp4`。
## 重建
```powershell
python tools/make_anim_payload.py # q63 帧 -> 等长槽位 + 改后镜像(自带 inode / 回放自证)
python tools/make_dd_payload.py replace # 负载 + 指纹 + blob_replace/
python tools/gen_ui_lua.py replace # 两遍构建 -> dist/S4PAnim.face(0 漂移校验)
python tools/verify_anim.py replace # 独立校验
python tools/make_dd_payload.py restore # 原厂字节 + 指纹 + blob_restore/
python tools/gen_ui_lua.py restore # -> dist/S4PAnimRestore.face
python tools/verify_anim.py restore
```
`inputs/`、`vendor/`(LuaDevTemplate,含 Compiler.exe)要自己放,已被 gitignore;
`payload*/`、`blob_*/`、`.work/` 是可再生产物,同样不进仓库。
## 真机流程
1. 装 `dist/S4PAnim.face` → 表盘「环境预检查」(只读)→「开始替换」→ 等 `失败 0` → 「重启手表」
2. 打开压力 App → 开始检测 → 应该是 q63 的旋转渐变球
3. 想还原:装 `dist/S4PAnimRestore.face`(同 id,会顶掉替换盘)→ 表盘「开始还原」→ 重启
4. 表盘里的「回滚修改 / 恢复修改」按钮会提示去装另一份盘(本盘不含另一侧的完整数据)
命令行路线:`flash.sh`(替换)/ `restore.sh`(还原)+ 对应 `payload*/` 目录,用法见脚本头部注释。
## 真机反馈后的 v2(已修)
第一次真机跑到「写入 56/117 失败 1」。根因是**搬运方式太慢**:119 个槽位里有 **97 条的字节数不是 4 的倍数**
(RLE 流长度随机,如 4485 / 196005),老写法只能退化成 bs=1 —— 一条 19.6 万字节的记录要 **38 万次单字节读写**,
慢到偶发超时/读回不一致。v2 改了三处:
1. **大块 + 尾块**:偏移都是 512 对齐,于是 bs=512 搬大块 → bs=4 搬次块 → 最后 1~3 字节 bs=1 收尾,
每条最多 3 个 dd(117 条从**约 1600 万次**单字节读写降到 **351 次 dd 调用**)
2. **单条重试 3 次**,写完先 sync 再读回比对
3. **版本校验改为「记账但仍写」**:设备上既不是原厂也不是已改(例如上次只写了一半)时不再跳过 ——
跳过去反而修不好那种半截状态;现在照样写,只在结论里记一笔
改动只在 Lua(负载字节没变),所以旧盘写到一半的状态,用新盘重跑一遍即可对齐。
## v2 的 bug 与 v3 修复
v2 在真机上直接报错:
```
main.lua:58: attempt to call a nil value (global 'sh')
in upvalue 'ddWrite' <- upvalue 'writeOne' <- upvalue 'tick'
```
**根因是 Lua 的词法作用域**:v2 把 `segs/ddWrite/ddRead` 插在了 `ddBlk` 后面(文件顶部),
而 `sh` / `slurp` 是**后面才 `local` 声明**的 —— 于是这两个函数里引用的 `sh`/`slurp` 是**全局名字(nil)**,
一调用就炸。(v2 因此**一个字节都没写进设备**:报错发生在第一条记录的 dd 之前。)
v3 把整块挪到 `slurp` / `sh` / `readChunk` 之后、`writeOne` 之前,并加了一个「先调用后声明」静态检查,
以后这类错能在本机被抓到。
顺带把写入改成**每段一个临时文件 + 只用 seek**,不再依赖同一条 dd 里同时用输入 skip 和输出 seek。
| 盘 | v3 体积 / sha256 | 校验 |
|---|---|---|
| `dist/S4PAnim.face` | 16,670,972 B / `4a10d1f83a0fd5a2…` | 11/11 通过 |
| `dist/S4PAnimRestore.face` | 16,685,460 B / `6318bc22698ea734…` | 11/11 通过 |
生成出来的 Lua 里关键函数顺序(已按依赖排好):
`slurp` 528 → `sh` 536 → `logOut` 617 → `readChunk` 629 → `segs` 641 → `ddWrite` 656 → `ddRead` 670 → `writeOne` 685。
## v4 —— 真机「黑屏」的根因(**解码长度少了 1 字节**)
真机现象:开始测量后动画**完全不显示**(黑),只偶尔闪一下**原版前两帧**。
根因在 header 的 `usize` 字段:
| | 值 |
|---|---|
| 槽位 header 里的 `usize`(119 个槽位全是这个) | **216,321** = 1024 + 464×464 + **1** |
| 原厂 stock 流解码出来的字节数 | **216,321**(正好等于 usize) |
| 我们生成的流解码出来的字节数 | **216,320**(少 1 字节)✗ |
设备端的 RLE 解码器要求解码结果**正好**是 `usize` 字节,少一字节就判定失败 → **那一帧直接不画**(黑屏)。
这解释了全部现象:
- 帧 1~3 是「保留原厂」的三帧(stock 流,解码正常)→ 偶尔闪一下原版 ✅
- 帧 4~56 是 v1 那轮写进去的坏流(解码失败)→ 黑 ✅
- 帧 57~119 还没写(仍是原版)—— 但脉冲前段的画面本来就接近全黑,所以看不出 ✅
**修复**:在编码结果后面补一个「重复 1 次」的 token(`0x01 0x00`),它产出 1 个字节、落在像素区之后(不影响画面),
把解码长度凑到 `usize`;之后照旧用空指令补到 `csize`。
**校验升级**:`tools/verify_anim.py` 现在对交付的**每一条**记录都检查「解码长度 == usize」,不只检查能否解出图。
| 盘 | v4 体积 / sha256 | 校验 |
|---|---|---|
| `dist/S4PAnim.face` | 16,670,844 B / `620cbdff87c1292c…` | 11/11 通过(含 usize 检查) |
| `dist/S4PAnimRestore.face` | 16,685,328 B / `4fad8c5b0f5f48c4…` | 11/11 通过 |
## v5 —— 诊断版(为了抓「总是失败 1」)
本机把 117 条记录都翻过一遍:全部 512 对齐、没有两条落在同一个 512 块、最大结束偏移离分区末端还有 132 MB、
97 条带 1~3 字节尾巴 —— **结构上没有哪一条特殊**,所以得让设备自己说是哪一条。
v5 在写入失败时会打出一行:
> 失败 #N off=0xXXXXXXXX len=NNNNN 首差@i 得AA期BB
(也可能是「读回 x 字节」或「读回 nil」)。把这一行发我,就能定位是哪个文件、差在哪一个字节、是没写进去还是写错了。
## v6 —— 「色彩失真」的根因与修法
真机反馈:动画看起来**色彩失真**。根因不是调色板通道顺序(那批 I8 控制中心图标已在真机验证过颜色正确),
而是**颜色数被槽位预算逼到了 2~24 色** —— 平滑的蓝绿渐变被压成**色阶断层(posterization)**:
| v5 的颜色档 | 帧数 |
|---|---|
| 256 色 | 91 帧 |
| 32~128 色 | 9 帧 |
| **24 / 16 / 12 / 8 / 6 / 4 / 3 / 2 色** | **16 帧** ← 这些就是看起来失真的 |
| 保留原厂 | 3 帧 |
这 16 帧正好是脉冲的两端(`Measuring4~11`、`Measuring111~119`)—— 它们的槽位预算只有 5~46 KB,
而 v5 的策略是「**宁可满尺寸 2 色也不缩小**」,于是画面变成一整块平色 / 硬边横条。
**v6 改了策略**:
1. 先在同一尺寸下降颜色,降到 **32 色**这个底线
2. 还不够就**缩小球体**(1.0 → 0.9 → … → 0.42),而不是继续丢颜色
3. 连「32 色 + 0.42 倍」都放不下的槽位(6 个最末尾的小槽位)**保留原厂** —— 那几帧原厂本来就近乎全黑,
读起来正好是淡入/淡出
结果:**113 帧替换,颜色全部 ≥ 32 色**(其中 91 帧仍是 256 色),2~24 色的失真帧一个不剩;
代价是紧预算的帧球体略小(0.6~0.9 倍),观感上像呼吸的缩放。
想反过来(宁可平色也别缩小)只要把 `tools/make_anim_payload.py` 里的 `ALLOW_BELOW_MIN = True` 打开。
对照图:`docs/anim_color_v5_vs_v6.png`(上排 v5 失真版 / 下排 v6);预览视频 `docs/anim_new.mp4`。
| 盘 | v6 体积 / sha256 | 校验 |
|---|---|---|
| `dist/S4PAnim.face` | 16,650,888 B / `09c042a332f84b33…` | 11/11 通过(含 usize 断言 + v5 的诊断日志) |
| `dist/S4PAnimRestore.face` | 16,685,988 B / `fc2afd109e5d24f9…` | 11/11 通过 |
## v7 —— 不保留任何原机帧 + 两处写入加固
**① 用户要求「不要保留原机动画」** —— v6 之前对预算太小的槽位会保留原厂字节,真机上那几个槽位会闪出**原机的小球**。
v7 改成**一律替换**:阶梯一路下探到「2 色 × 0.04 倍」,再不行就写一帧全透明占位,绝不 keep 原厂。
结果:**119/119 全部替换**(不再有 keep)。
| 帧 1~6(最紧的槽位,5~8 KB)实际编码 |
|---|
| 帧1 = 2 色 ×0.04(一个点)、帧2 = 2 色 ×0.09、帧3 = 3 色 ×0.22、帧4 = 2 色 ×0.22、帧5 = 16 色 ×0.22、帧6 = 32 色 ×0.28 |
为什么这么小:464×464 的 I8 图**光是全黑背景**就要 ~3.7 KB(RLE 单条游程上限 127,一行至少 4 个 token),
再加 1 KB 调色板 —— 这两个槽位的总预算只有 4.4~4.5 KB,物理上只能装一个小点。
**② 「总是失败 1」的两处加固**(v7):
1. **去掉 canary 记录**:它往偏移 0 写 32 字节(内容与原厂相同,纯属多余),而偏移 0 的写/回读恰恰最容易出岔子,
而且它**永远是第一条**——这与「每次都恰好失败 1」的现象高度吻合
2. **加「计划 B」**:常规「分段写」3 次不过时,改成**整条一次 dd**(长度能被 4 整除用 bs=4,否则 bs=1)再校验一次;
有些驱动对「同一区域被拆成多次小写」比「一次大写」更敏感
3. 失败时依旧打印 `> 失败 #N off=0x… len=… 首差@i 得AA期BB`(诊断信息保留)
**顺手修的**:预览视频 `docs/anim_new.mp4` 之前是按**偏移**排序生成的,播出来顺序是乱的;v7 起按**帧号**生成。
| 盘 | v7 体积 / sha256 | 校验 |
|---|---|---|
| `dist/S4PAnim.face` | 16,686,552 B / `5b1814e6177791ee…` | 11/11 通过(119 条 pay,无 canary) |
| `dist/S4PAnimRestore.face` | 16,686,552 B / `d5d92b6a0cd1cfc0…` | 11/11 通过 |
## v8 —— 「忽大忽小」的根因与修法
真机反馈:结尾逐渐缩小的动画**忽大忽小**。根因是**球体比例逐帧从离散档位里挑**,而档位跟着槽位预算走
(预算继承自原厂脉冲、本身有小抖动)—— 相邻两帧可能落到差很远的档位,看起来就一跳一跳。
v8 改成两遍法:
1. **量上限**:对每一帧**二分**求「还能保住 ≥32 色的最大比例」cap(8 次二分,比离散档细得多)
2. **平滑**:从峰值向两侧取**运行最小**,得到单峰、单调、不含抖动的曲线
3. **编码**:严格按曲线的比例来,预算不够时**只降颜色、不改尺寸**;只有连 2 色都放不下时才允许再缩 10%
| 阶段 | 帧 | 比例 |
|---|---|---|
| 涨起来 | 1~12 | 0.02 → 0.04 → 0.09 → 0.18 → 0.31 → 0.45 → 0.62 → 0.70 → 0.81 → 0.99 → 1.00 |
| 保持 | 12~110 | 1.00(其中 91 帧 256 色) |
| 落下去 | 111~119 | 1.00 → 0.93 → 0.90×4 → 0.81 → 0.77 → 0.72 → 0.66 |
曲线图 `docs/anim_scale_profile.png`;预览视频(按帧号)`docs/anim_new.mp4`。
全 119 帧里只有最前两帧(比例 0.02,约 9 像素的点)颜色低于 32。
| 盘 | v8 体积 / sha256 | 校验 |
|---|---|---|
| `dist/S4PAnim.face` | 16,686,680 B / `ce73d9ede1c9a574…` | 11/11 通过 |
| `dist/S4PAnimRestore.face` | 16,686,680 B / `d16b5cfda35089b9…` | 11/11 通过 |