> 本文是当时的分析 / 交付存档,路径按当时的工作目录写。 > 本项目里的对应关系: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` 正是这么写分区的: ```sh dd if=/ota/vela_app.bin of=/dev/app bs=32768 verify ``` 所以本包对每条记录执行三步,全程只用官方存在的命令: ```sh dd if=payload/NNN.bin of=/dev/app bs=4 seek= count= dd if=/dev/app of=/tmp/chk.bin bs=4 skip= count= 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 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 那份镜像,或官方固件包里的同名文件)。