Files
2026-10-02 21:34:56 +08:00

6.9 KiB
Raw Permalink Blame History

本文是当时的分析 / 交付存档,路径按当时的工作目录写。 本项目里的对应关系:tools 对应 _tools,inputs/target/vela_app.bin 对应 _x/vela_app.bin, inputs/source 对应 _s5,.work 对应 _out(产物),dist 对应 _out/faces。

S5 图标包 → S4 eSIM 替换包

把 Xiaomi Watch S5(q63, v4.101.020) 的系统应用图标,写进 Xiaomi Watch S4 eSIM(o62lte) 的 /dev/app 分区。

文件

文件 说明
main.lua Lua 表盘入口(LVGL 界面 + dd 写入/校验状态机)
payload.lua 负载清单:38 条记录(1 条超级块 canary + 37 个图标)
payload/ 逐条的图标字节,文件名与 payload.lua 对应
payload_rollback.lua + payload_rollback/ 同样 38 条,内容为 S4 原厂字节(回滚用)
flash.sh / rollback.sh 不需要表盘:有 nsh shell 时直接跑这两条脚本
payload.json / manifest.json 机器可读的明细(偏移、长度、格式、来源)
README.md 本文件

机制:走官方 OTA 同一条路

Lua 表盘无法"设置图标"。系统图标住在 /dev/app 这个 265 MB 的 ROMFS 分区镜像里 (/resource/app/<应用>/launcher.bin),必须写入分区 + 重启才会生效。

关键发现来自 docs-shell:设备侧有 dd / cmp / md5 / reboot, 而 Lua 里 os.execute 可用(io.popen 不可用)。固件自带的 ota.sh 正是这么写分区的:

dd if=/ota/vela_app.bin of=/dev/app bs=32768 verify

所以本包对每条记录执行三步,全程只用官方存在的命令:

dd  if=payload/NNN.bin  of=/dev/app      bs=4 seek=<offset/4> count=<length/4>
dd  if=/dev/app         of=/tmp/chk.bin  bs=4 skip=<offset/4> count=<length/4>
cmp /tmp/chk.bin payload/NNN.bin          # 退出码 0 = 一致

为什么 bs=4:所有目标偏移都是 512 对齐,且每条的长度都是 4 的倍数 (82956 / 21772 / 32 都能被 4 整除),所以 bs=4 正好写满目标区间、 不会多写一个字节到相邻文件里,也不会产生不完整的尾块。

最后 reboot 让只读挂载重新读取分区 —— 这就是"点一下、稍等片刻、手表重启、替换成功" 那条链路。

关于"打包方法"

我把 docs-lua 全部 68 个文件读完了:里面没有表盘打包/安装的说明。 它给出的是运行时契约:

事实 出处
内建表盘源码就在 /watchface/lua/<名字>/(circle, clearheart, complexzone, corona, daynight) lvgl/modules/fs.md
表盘资源 /watchface/(resource.bin、aod) 同上
已安装表盘在 /data/app/watchface/(builtin / market) 同上
资源用绝对路径引用,如 /watchface/lua/corona/image/sec.bin lvgl/widgets/analogtime.md
入口:init(style) 返回根对象;全局 pageOnPause / pageOnResume activity/overview.md
/data fatfs 可读写;/tmp tmpfs 可读写(重启清空);其余系统目录 romfs 只读 docs-shell/syntax-and-environment.md
pm install <包名|rpk> / am start <包名> 是快应用的包管理器 docs-shell/runtimes-and-media.md

也就是说:Lua 表盘本身就是一个目录(.lua + 资源),没有专用打包格式; 真正没被文档覆盖的是"怎么把这个目录推上手表、怎么在 watchface.db 里注册"。 这一步取决于你所用的工具(AstroBox / 表盘自定义工具等)。

好消息是这一步对本项目不是必需的 —— 见下面两条替代路线。

三条路线(按推荐顺序)

路线 A:flash.sh(最简单,只要能拿到一次 shell)

把 flash.sh、payload/ 复制到手表 /data/s5icons/,然后:

sh /data/s5icons/flash.sh
reboot

不需要表盘、不需要打包、不依赖表盘宿主权限。回滚用 rollback.sh。

路线 B:Lua 表盘(main.lua)

按你需要的形态提供。界面三个按钮 + 回滚:

  1. ① 只读检查(不写任何字节):dd 读出分区头,确认 magic 是 -rom1fs-、 卷名是 app;再逐条确认负载文件都在、长度正确。只有检查通过,② 才解锁。
  2. ② 写入并校验:逐条 dd 写入 → dd 读回 → cmp 比对,带进度条。
  3. ③ 重启:sync + reboot。
  4. 回滚:用 payload_rollback 走同一套流程。

分片执行(定时器每 40 ms 一条),界面不会卡死;每条失败都会计数并在结束时报告。

路线 C:直接刷整分区(最稳)

_out/s4_from_s5/vela_app_s5icons.bin     已改好图标的 vela_app.bin(265 MB)

用你已有的刷机通道写进 /dev/app。完全绕开设备侧脚本。

替换内容

S4 有 40 个 launcher 图标槽位:

  • 37 个真正被改写(字节有差异)
  • 2 个字节完全相同,已从负载中剔除:voice_alexa、wxpay
  • 1 个 S4 独有、S5 没有:walkie_talkie,保持原厂
  • S5 独有的 12 个(launcher_* 第三方图标、temperature、calorie_management 等)按约定跳过

编码策略:S5 文件长度 == S4 槽位长度 → 原样拷贝;否则解码 S5 图标后按 S4 槽位的色彩格式 重新编码,确保长度逐字节相等。结果:8090 条目录项、inode 表、superblock 逐位不变。

验证过的等价性:把这些负载按偏移重放进原厂镜像,得到的文件与我逐字节验证过的 vela_app_s5icons.bin 完全相同(sha256 一致)。

观感差异:10 个视觉上无变化,29 个有可见差异,多数是重新配色/描边 (例如 barometer 的底盘由蓝变白、activities 的渐变换了)。

⚠ 未验证项

我无法在真机上测试,以下三点请自行确认:

  1. os.execute 是否在表盘宿主里真的可用 —— docs-lua/intro/overview.md 标为 ✅, 但那是模拟器环境的结论;docs-lua/intro/conventions.md 也写明"接口通过逆向分析获得, 不同设备可用性不一致"。
  2. SCRIPT_PATH 是否注入、值是什么 —— 这是 LoXe 文档的约定,不在本仓库文档里。 main.lua 里已有兜底常量,若加载失败请改成实际路径。
  3. 写入 dd 的权限 —— 表盘宿主进程是否有权写 /dev/app。docs-shell 只说明命令存在, 不代表当前进程有权限。

第一次请只按「① 只读检查」,看清输出再决定是否写入。或者直接走路线 A/C。

风险

  • 往分区节点写数据是危险操作,写错位置可能导致无法开机。
  • 系统 OTA 升级会整分区覆盖 vela_app.bin,图标会回到原厂(等于自动还原)。
  • 修改系统资源分区可能影响保修;请自行确认设备所有权与当地法律。
  • 动手前请确认你能刷回原厂 vela_app.bin(路线 C 那份镜像,或官方固件包里的同名文件)。