14 KiB
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,压缩后长度各不相同。三步:
- 缩放 + 量化:q63 帧 → 464×464,按 (颜色数, 缩放比例) 阶梯挑一档使编码 ≤ 槽位 csize; 先把接近黑的背景(亮度 <40)钉成纯黑 —— 既干净又大幅缩短 RLE(91/116 帧因此能上 256 色,压平前只有 53 帧)
- 编码:LVGL RLE 语法(
tools/lvgl_bin.py: rle_compress) - 补齐:
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。
重建
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/ 是可再生产物,同样不进仓库。
真机流程
- 装
dist/S4PAnim.face→ 表盘「环境预检查」(只读)→「开始替换」→ 等失败 0→ 「重启手表」 - 打开压力 App → 开始检测 → 应该是 q63 的旋转渐变球
- 想还原:装
dist/S4PAnimRestore.face(同 id,会顶掉替换盘)→ 表盘「开始还原」→ 重启 - 表盘里的「回滚修改 / 恢复修改」按钮会提示去装另一份盘(本盘不含另一侧的完整数据)
命令行路线:flash.sh(替换)/ restore.sh(还原)+ 对应 payload*/ 目录,用法见脚本头部注释。
真机反馈后的 v2(已修)
第一次真机跑到「写入 56/117 失败 1」。根因是搬运方式太慢:119 个槽位里有 97 条的字节数不是 4 的倍数 (RLE 流长度随机,如 4485 / 196005),老写法只能退化成 bs=1 —— 一条 19.6 万字节的记录要 38 万次单字节读写, 慢到偶发超时/读回不一致。v2 改了三处:
- 大块 + 尾块:偏移都是 512 对齐,于是 bs=512 搬大块 → bs=4 搬次块 → 最后 1~3 字节 bs=1 收尾, 每条最多 3 个 dd(117 条从约 1600 万次单字节读写降到 351 次 dd 调用)
- 单条重试 3 次,写完先 sync 再读回比对
- 版本校验改为「记账但仍写」:设备上既不是原厂也不是已改(例如上次只写了一半)时不再跳过 —— 跳过去反而修不好那种半截状态;现在照样写,只在结论里记一笔
改动只在 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 改了策略:
- 先在同一尺寸下降颜色,降到 32 色这个底线
- 还不够就缩小球体(1.0 → 0.9 → … → 0.42),而不是继续丢颜色
- 连「32 色 + 0.42 倍」都放不下的槽位(6 个最末尾的小槽位)保留原厂 —— 那几帧原厂本来就近乎全黑, 读起来正好是淡入/淡出
结果:113 帧替换,颜色全部 ≥ 32 色(其中 91 帧仍是 256 色),224 色的失真帧一个不剩;
代价是紧预算的帧球体略小(0.60.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 |
|---|
| 帧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.44.5 KB,物理上只能装一个小点。
② 「总是失败 1」的两处加固(v7):
- 去掉 canary 记录:它往偏移 0 写 32 字节(内容与原厂相同,纯属多余),而偏移 0 的写/回读恰恰最容易出岔子, 而且它永远是第一条——这与「每次都恰好失败 1」的现象高度吻合
- 加「计划 B」:常规「分段写」3 次不过时,改成整条一次 dd(长度能被 4 整除用 bs=4,否则 bs=1)再校验一次; 有些驱动对「同一区域被拆成多次小写」比「一次大写」更敏感
- 失败时依旧打印
> 失败 #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 改成两遍法:
- 量上限:对每一帧二分求「还能保住 ≥32 色的最大比例」cap(8 次二分,比离散档细得多)
- 平滑:从峰值向两侧取运行最小,得到单峰、单调、不含抖动的曲线
- 编码:严格按曲线的比例来,预算不够时只降颜色、不改尺寸;只有连 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 通过 |