246 lines
14 KiB
Markdown
246 lines
14 KiB
Markdown
# 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 通过 | |