diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..c89edf3 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,11 @@ +# 文本按 LF 存,二进制一律别碰(.bin/.face 被转换就会损坏负载) +* text=auto eol=lf +*.py text eol=lf +*.lua text eol=lf +*.md text eol=lf +*.sh text eol=lf +*.json text eol=lf +*.bin -text +*.face -text +*.png -text +*.ttf -text diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9e96e7f --- /dev/null +++ b/.gitignore @@ -0,0 +1,26 @@ +# ==== 体积大 / 有版权 / 可再生成的东西,不进仓库 ==== + +# 固件分区镜像 + 字体(版权属小米;从官方 OTA 包 / vela_font.bin 自行获取) +**/inputs/**/*.bin +**/inputs/fonts/ +**/inputs/**/*.ttf +**/inputs/**/*.otf + +# 第三方编译器(含 Compiler.exe;自行从 GitHub 取) +**/vendor/LuaDevTemplate-main/ + +# 构建中间产物 +**/.work/ +**/__pycache__/ +*.pyc + +# 逐条负载(可由 make_dd_payload.py / s5e_payload.py 重新生成) +**/payload/*.bin +**/payload_rollback/*.bin +**/payload.json + +# 编辑器 / 系统垃圾 +.vscode/ +.idea/ +Thumbs.db +desktop.ini diff --git a/README.md b/README.md index 18374e6..b9743ee 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,138 @@ -# o62lte-customising-plan +# 小米手表 系统图标修改器(Lua 表盘) -Xiaomi Watch S4 eSIM 系统个性化项目 \ No newline at end of file +两个互相独立的项目,同一套机制:**用一个 Lua 表盘把系统应用图标「等长原地覆盖」进 ROMFS 资源分区**,重启生效;界面里带只读预检查、写入进度和一键回滚。 + +| 项目 | 目标机型 | 屏幕 | 写入分区 | 主交付物 | +|---|---|---|---|---| +| [`s4e-icons/`](s4e-icons/) | Xiaomi Watch S4 eSIM(o62lte, 3.202.79) | 466×466 | `/dev/app` | `dist/S5Icons.face` | +| [`s5e-icons/`](s5e-icons/) | Xiaomi Watch S5 eSIM 46mm(p62lte, 3.112.035) | 480×480 | `/dev/app` + `/dev/health` | `dist/S5eIcons.face` | + +两个项目的**图标来源都是 Xiaomi Watch S5 41mm(q63, v4.101.020)的 OS4 图标包**。 + +每个项目内部结构: + +``` +README.md 项目说明(交付物 / 装法 / 边界) +main.lua 表盘源码(含 __PAY__ / __RB__ / __UIIMG__ 占位符,构建时回填) +tools/ 全部 Python 脚本(ROMFS 解析、LVGL 编解码、负载生成、两遍构建、独立校验) +assets/ 设计素材;assets/override/ 里的自制 PNG 优先级高于固件来源 +inputs/ 固件分区镜像(**要自己放,不进仓库**) +vendor/ 第三方 LuaDevTemplate-main(含 Compiler.exe,**不进仓库**) +.work/ 构建工作区(产物,可删) +dist/ 交付的 .face +docs/ 说明、分析报告、预览图 +``` + +--- + +## 跑起来(全部在 `icon-editors/` 目录内完成) + +前置:**Python 3 + Pillow**(`pip install pillow`)。 + +### 1) 放 `inputs/`(固件分区镜像) + +官方 OTA 包(ZIP)里的 `vela_*.bin` 条目,解压后按下面的名字放(`ota.sh`、`CERT.*`、`MANIFEST.MF` 不需要): + +| 路径 | 是什么 | 字节 | sha256 前 16 | +|---|---|---:|---| +| `s4e-icons/inputs/target/vela_app.bin` | S4 eSIM 的 app 分区 | 265,684,992 | `caa66693f49477dd` | +| `s4e-icons/inputs/source/vela_app.bin` | S5 41mm q63 的 app 分区 | 206,000,128 | `161a8f7a5f0bd8af` | +| `s4e-icons/inputs/source/vela_health.bin` | q63 的 health 分区 | 233,939,968 | `12a3bd2f8b62e969` | +| `s5e-icons/inputs/target/vela_app.bin` | S5 eSIM 的 app 分区 | 103,043,072 | `9b41c5f5eb17c112` | +| `s5e-icons/inputs/target/vela_health.bin` | S5 eSIM 的 health 分区 | 206,924,800 | `489752c8a3fb24ff` | +| `s5e-icons/inputs/source/*` | 与 s4e 的 `source/` 是同一份 q63 镜像 | | | + +### 2) 放 `vendor/`(第三方编译器) + +把 [FangAiden/LuaDevTemplate](https://github.com/FangAiden/LuaDevTemplate) 解压成 +`<项目>/vendor/LuaDevTemplate-main/`(里面必须有 `watchface/tools/Compiler.exe`)。 +两个项目各一份。没有它只能生成负载,不能编译 `.face`。 + +(本项目本来也支持 `ICON_EDIT_VENDOR=<目录>` 指向别处的模板,路径定义集中在 +`tools/paths.py`,可用 `ICON_EDIT_INPUTS / ICON_EDIT_VENDOR / ICON_EDIT_WORK / ICON_EDIT_DIST` 覆盖。) + +### 3) 构建 + +```powershell +cd icon-editors/s4e-icons +python tools/make_s5_to_s4_inset.py # q63 素材 -> S4 槽位(含尺寸归一化、格式转换、I8 调色板复用) +python tools/make_dd_payload.py # 逐条负载 + 回滚负载 + flash.sh / rollback.sh + payload.json +python tools/gen_ui_lua.py # 两遍构建 -> dist/S5Icons.face(回填偏移,断言 0 漂移) +python tools/s4e_verify.py # 独立校验(不复用构建代码,建议每次跑) + +cd ../s5e-icons +python tools/s5e_payload.py # q63 素材 -> S5e 槽位 +python tools/s5e_build.py # -> dist/S5eIcons.face +python tools/s5e_verify.py # 独立校验 +``` + +### 4) 内容开关 + +默认打进表盘的是「应用图标 + 控制中心顶栏状态 + 系统 UI + 通知 / 电话 / 录音机 / 计时器 / 待办」。 +下面两组**默认关闭**: + +| 环境变量 | 作用 | +|---|---| +| `ICON_MIJIA=1` | 控制中心里的米家 / 融合中心设备图标(实测目标机本来就装了同一个米家表 App,129 张设备图标里 128 张与 q63 逐字节相同,一般不需要) | +| `ICON_WALKIE=1` | 对讲机素材 `walkie_talkie/**`(160 条;S5 41mm 没有这个 App,素材来自用户提供的 PNG) | +| `ICON_SKIP_WALKIE=1` / `ICON_ONLY_WALKIE=1` | 要分组出表盘时用(一个大表盘拆两张) | + +--- + +## 为什么必须等长 + +ROMFS 把 inode、文件名、文件数据首尾相接顺序排布,所有 `next` / `spec` 都是**绝对偏移**。替换文件只要长度差一个字节,它后面所有文件的偏移就整体平移 —— 轻则图标错乱,重则资源分区损坏。 + +所以两个项目都保证 **每条负载的长度 == 原槽位长度**,只动数据、不碰 inode 表 / superblock / 目录项。 + +## 表盘到底做了什么 + +Lua 表盘不能「设置图标」。系统图标住在 ROMFS 分区镜像里,必须写分区 + 重启。表盘只做运载 + 执行: + +``` +每条记录三步(只用官方存在的 dd,判据全看内容,不看 os.execute 的返回值 —— 它不可靠): + 1 对版 读出设备上该偏移处 64 字节,与「原厂字节」或「替换后字节」比对 + 两者都不是 -> 判「版本不符」,该分区剩余记录全部跳过(绝不往别人的文件里写) + 2 写入 dd if=/tmp/xxx of=<分区> bs=4 seek=<偏移/4> count=<长度/4> + 3 回读 整段读回来逐字节比对 + +回滚 = 同一套流程,只是写入的字节换成原厂字节 +最后 sync + reboot(ROMFS 是只读挂载 + 有页缓存,不重启看不到新图标) +``` + +偏移都在**构建时两遍测量回填**:先编一遍,在 .face 里搜负载定位,量出真实偏移写回 Lua,再编第二遍并逐条校验 0 漂移。 + +## 只安装、不重建 + +直接拿 `s4e-icons/dist/S5Icons.face` 或 `s5e-icons/dist/S5eIcons.face`,和普通 Lua 表盘一样安装(`resource.bin` 推到 `/data/app/watchface/market//`)。装到手表后在表盘上操作: + +``` +首页 →「快速帮助」→「环境预检查」 只读,一个字节都不写 +首页 →「开始替换」→ 确认 → 自动写入 → 「重启手表」 + 快速帮助 →「回滚修改」→ 确认 → 写回原厂图标 +``` + +**环境预检查是只读的**,它只能证明「两个分区可读 + 抽样偏移对版」,证明不了「可写」;但写入路径每条都自带对版 + 回读校验,最坏结果是「图标没变」,不会把分区写坏。 + +## 已知边界 + +1. **偏移锁固件版本**:ROMFS 绝对偏移排布,换固件版本必须拿新镜像重跑,旧偏移会写错位置。 +2. **等长是硬要求**:长度不等会让后面所有偏移平移;脚本对每条断言长度相等。 +3. **I8 槽位**必须复用源图调色板,否则边缘锯齿 + 彩点。 +4. **真机能力**:`os.execute` / `dd` 可用;`io.popen` 捕获不到输出;`os.execute` 返回值不可靠(成功也可能是 65280),判据一律看内容读回。 +5. 首次请只点「环境预检查」,看清输出再替换。 + +## 仓库里没有的东西(体积 / 版权) + +| 不含 | 原因 | 怎么补 | +|---|---|---| +| `inputs/**/*.bin`(固件分区镜像) | 体积 + 版权 | 从官方 OTA 包解包,见上表 | +| `inputs/fonts/*.ttf`(MiSans) | 版权属小米 | 从固件 `vela_font.bin` 取;没有只影响预览图 | +| `vendor/LuaDevTemplate-main/` | 第三方 | 从 GitHub 取,见上 | + +## 第三方与法律 + +- `.face` 由 [FangAiden/LuaDevTemplate](https://github.com/FangAiden/LuaDevTemplate) 的 `Compiler.exe` 编译;本仓库不含它,请自行放到 `vendor/LuaDevTemplate-main/` +- MiSans 字体版权属小米;本仓库不含字体文件,需要时从固件 `vela_font.bin` 里取 +- 固件 / OTA 版权属小米,请从官方更新包里自行解包 +- **改系统分区有风险**:写错偏移可能损坏资源分区。两个项目都自带版本校验和回滚,但请自行确认设备所有权并承担后果 diff --git a/s4e-charge/.gitignore b/s4e-charge/.gitignore new file mode 100644 index 0000000..13a0346 --- /dev/null +++ b/s4e-charge/.gitignore @@ -0,0 +1,10 @@ +# 大文件 / 中间产物:不进仓库 +.work/ +inputs/ +vendor/ +__pycache__/ +*.pyc +payload/ +payload_restore/ +blob_replace/ +blob_restore/ diff --git a/s4e-charge/README.md b/s4e-charge/README.md new file mode 100644 index 0000000..6135125 --- /dev/null +++ b/s4e-charge/README.md @@ -0,0 +1,71 @@ +# S4Chg —— 把 S5 41mm(q63) 的充电动画搬到 S4 eSIM + +**当前阶段:全量版已交付(57 帧全换)** —— 轻量测试(5 帧)已在真机确认生效:**`/dev/system` 可写 + 动画确实取自这 57 帧**。 + +## 改了什么 + +| | | +|---|---| +| 目标 | `inputs/target/vela_system.bin`(S4 eSIM `/dev/system`)→ `charging/charge0..56.bin`(57 帧 464×464 I8 RLE) | +| 源 | `inputs/source/vela_system.bin`(q63 S5 41mm)→ `charging/charge0..60.bin`(61 帧) | +| 映射 | 第 n 帧 ← q63 第 `round(n/56*60)` 帧(时间轴按比例映射) | +| 本次 | **57/57 全换**,**每帧都是 256 色**(槽位 79~119 KB 很宽裕;背景压平后 RLE 完全放得下) | +| 校验 | 57/57 回解逐像素一致、usize 长度一致、inode 表逐位不变、superblock 不变、回放一致 | + +## 交付物 + +| 文件 | 说明 | +|---|---| +| `dist/S4Chg.face` | **替换盘**:6,422,844 B(6.1 MB),id `362150103`,b5=0,sha256 `687f600df38597e8…` | +| `dist/S4ChgRestore.face` | **还原盘**:同 id,sha256 `a243fb4734913280…`(装上即还原,也用来顶掉替换盘) | +| `docs/full_before_after.png` | 全量对照图(原厂 vs q63,采样 10 帧) | +| `docs/full_preview.mp4` | 换后整段 57 帧预览 | +| `docs/sample_before_after.png` / `sample_preview.mp4` | 轻量测试阶段(5 帧)的留档 | + +## 真机怎么做 + +1. 装 `dist/S4Chg.face`(同 id 会自动顶掉测试盘) +2. 表盘「环境预检查」→「开始替换」→ 等 `失败 0`(57 条,约 6 MB;失败会打印 `> 失败 #N off=0x… len=… 首差@i 得AA期BB`) +3. 「重启手表」→ 插上充电器看整段动画 +4. 还原:装 `dist/S4ChgRestore.face` →「开始还原」→ 重启 + +## 重建 + +```powershell +python tools/make_charge_payload.py all # 全量 57 帧(不加 all = 只改 5 帧的轻量测试) +python tools/make_dd_payload.py replace # 负载 + 指纹 + blob_replace/ +python tools/gen_ui_lua.py replace # 两遍构建 -> dist/S4Chg.face(0 漂移校验) +python tools/verify_anim.py replace # 独立校验(只看交付物 + 原厂镜像) +python tools/make_dd_payload.py restore +python tools/gen_ui_lua.py restore # -> dist/S4ChgRestore.face +python tools/verify_anim.py restore +``` + +`inputs/`(固件分区镜像)与 `vendor/`(LuaDevTemplate,含 Compiler.exe)不进仓库;`payload*/`、`blob_*/`、`.work/` 是可再生产物。 + +## 备注 + +- 本盘写的是 **`/dev/system`**(系统资源分区):轻量测试证明它可 dd 写入,这是本轮最重要的发现 —— 以后 `system/drain/*`(排水)、`ota/anim*`(升级动画)、`setupwizard/*`(配对引导)都能用同一套办法改。 +- ROMFS 的数据偏移是**结构性**的(`data_off = inode + 16 + align16(名字长度+1)`),所以只能逐帧等长覆盖,不能把数据指到别处、也不能跨分区。 + +# 编码 v2(2026-10-02):画质问题的根因与修法 + +真机反馈「有点糊」。诊断后发现是**编码器**的问题,不是素材上限: + +| | 旧方案 | 新方案 | +|---|---|---| +| 做法 | 整帧重新量化(MEDIANCUT 256 色)+ 把暗于 46 的像素压成纯黑 | **沿用 q63 原帧的调色板与索引**,只把接近黑的暗区归到一个索引,阈值按每帧预算取最小值(实测 4~40) | +| 圆环 | 亮部面积小,MEDIANCUT 按像素数分配颜色 → 圆环分不到颜色;阈值 46 还把环上的暗段啃成黑色缺口 | **逐像素与原帧完全一致** | +| 整帧 PSNR(57 帧) | 25.1 dB(最差 24.0) | **33.4 dB(最差 25.2)** | +| 接近黑区域均差 | 8.56 | 6.46 | + +对照图见 docs/quality_v1_vs_v2.png(整帧 / 局部 2× / 与 q63 原帧的差异 ×4)。 + +**天花板在哪**:圆环已经到顶(逐像素一致,除非改尺寸或格式);雾面暗区受**槽位字节预算**限制 —— q63 原帧是 135~201 KB/帧,S4e 槽位只有 79~119 KB,而暗区是噪点雾面、最吃 RLE。要连暗区也几乎无损,需要把 57 个槽位当总池重新分配,但总池 5.7 MB < 源 10.6 MB,仍然要取舍。 + +| 盘 | v2 体积 / sha256 | 校验 | +|---|---|---| +| dist/S4Chg.face | 6,422,844 B / 66640d3a869bbc29… | 11/11 通过 + 自检 57/57 | +| dist/S4ChgRestore.face | 6,422,844 B / 9652747f3c79402f… | 11/11 通过 | + +**若之前装过 v1 全量版**(sha 687f600d…):设备上是旧编码的帧,新盘替换时会提示「版本不符」(指纹表记的是**原厂**字节),但**它会照写**(版本不符只提示不阻止),写完重启即为 v2 画质。 diff --git a/s4e-charge/assets/MdiBandage.png b/s4e-charge/assets/MdiBandage.png new file mode 100644 index 0000000..26abc5b Binary files /dev/null and b/s4e-charge/assets/MdiBandage.png differ diff --git a/s4e-charge/assets/MdiClipboardTextSearchOutline.png b/s4e-charge/assets/MdiClipboardTextSearchOutline.png new file mode 100644 index 0000000..85ea2da Binary files /dev/null and b/s4e-charge/assets/MdiClipboardTextSearchOutline.png differ diff --git a/s4e-charge/assets/MdiInformationOutline.png b/s4e-charge/assets/MdiInformationOutline.png new file mode 100644 index 0000000..3af2a29 Binary files /dev/null and b/s4e-charge/assets/MdiInformationOutline.png differ diff --git a/s4e-charge/assets/MdiLightbulbOutline.png b/s4e-charge/assets/MdiLightbulbOutline.png new file mode 100644 index 0000000..7d7819c Binary files /dev/null and b/s4e-charge/assets/MdiLightbulbOutline.png differ diff --git a/s4e-charge/assets/MdiReplyAllOutline.png b/s4e-charge/assets/MdiReplyAllOutline.png new file mode 100644 index 0000000..f419de0 Binary files /dev/null and b/s4e-charge/assets/MdiReplyAllOutline.png differ diff --git a/s4e-charge/assets/backarrow.png b/s4e-charge/assets/backarrow.png new file mode 100644 index 0000000..9e55859 Binary files /dev/null and b/s4e-charge/assets/backarrow.png differ diff --git a/s4e-charge/assets/cover.png b/s4e-charge/assets/cover.png new file mode 100644 index 0000000..23c1cf8 Binary files /dev/null and b/s4e-charge/assets/cover.png differ diff --git a/s4e-charge/assets/override/activities.png b/s4e-charge/assets/override/activities.png new file mode 100644 index 0000000..ae5c552 Binary files /dev/null and b/s4e-charge/assets/override/activities.png differ diff --git a/s4e-charge/assets/override/interconnect.png b/s4e-charge/assets/override/interconnect.png new file mode 100644 index 0000000..5739b3b Binary files /dev/null and b/s4e-charge/assets/override/interconnect.png differ diff --git a/s4e-charge/assets/override/sports_course.png b/s4e-charge/assets/override/sports_course.png new file mode 100644 index 0000000..4b95e98 Binary files /dev/null and b/s4e-charge/assets/override/sports_course.png differ diff --git a/s4e-charge/assets/override/voice_aivs.png b/s4e-charge/assets/override/voice_aivs.png new file mode 100644 index 0000000..228914f Binary files /dev/null and b/s4e-charge/assets/override/voice_aivs.png differ diff --git a/s4e-charge/assets/qq.png b/s4e-charge/assets/qq.png new file mode 100644 index 0000000..3982762 Binary files /dev/null and b/s4e-charge/assets/qq.png differ diff --git a/s4e-charge/assets/walkie_talkie/background.png b/s4e-charge/assets/walkie_talkie/background.png new file mode 100644 index 0000000..879c98f Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/background.png differ diff --git a/s4e-charge/assets/walkie_talkie/cancel_btn.png b/s4e-charge/assets/walkie_talkie/cancel_btn.png new file mode 100644 index 0000000..cc56188 Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/cancel_btn.png differ diff --git a/s4e-charge/assets/walkie_talkie/check.png b/s4e-charge/assets/walkie_talkie/check.png new file mode 100644 index 0000000..bc89d28 Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/check.png differ diff --git a/s4e-charge/assets/walkie_talkie/checked.png b/s4e-charge/assets/walkie_talkie/checked.png new file mode 100644 index 0000000..64bf2b9 Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/checked.png differ diff --git a/s4e-charge/assets/walkie_talkie/close_btn.png b/s4e-charge/assets/walkie_talkie/close_btn.png new file mode 100644 index 0000000..a872247 Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/close_btn.png differ diff --git a/s4e-charge/assets/walkie_talkie/confirm_btn.png b/s4e-charge/assets/walkie_talkie/confirm_btn.png new file mode 100644 index 0000000..932d711 Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/confirm_btn.png differ diff --git a/s4e-charge/assets/walkie_talkie/delete_icon.png b/s4e-charge/assets/walkie_talkie/delete_icon.png new file mode 100644 index 0000000..b3d948d Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/delete_icon.png differ diff --git a/s4e-charge/assets/walkie_talkie/guidance/finish_guidance.png b/s4e-charge/assets/walkie_talkie/guidance/finish_guidance.png new file mode 100644 index 0000000..932d711 Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/guidance/finish_guidance.png differ diff --git a/s4e-charge/assets/walkie_talkie/history_anim/anim0.png b/s4e-charge/assets/walkie_talkie/history_anim/anim0.png new file mode 100644 index 0000000..cdbf172 Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/history_anim/anim0.png differ diff --git a/s4e-charge/assets/walkie_talkie/history_anim/anim1.png b/s4e-charge/assets/walkie_talkie/history_anim/anim1.png new file mode 100644 index 0000000..9eb6f8b Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/history_anim/anim1.png differ diff --git a/s4e-charge/assets/walkie_talkie/history_anim/anim10.png b/s4e-charge/assets/walkie_talkie/history_anim/anim10.png new file mode 100644 index 0000000..927d24a Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/history_anim/anim10.png differ diff --git a/s4e-charge/assets/walkie_talkie/history_anim/anim11.png b/s4e-charge/assets/walkie_talkie/history_anim/anim11.png new file mode 100644 index 0000000..38d3985 Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/history_anim/anim11.png differ diff --git a/s4e-charge/assets/walkie_talkie/history_anim/anim12.png b/s4e-charge/assets/walkie_talkie/history_anim/anim12.png new file mode 100644 index 0000000..32c452c Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/history_anim/anim12.png differ diff --git a/s4e-charge/assets/walkie_talkie/history_anim/anim13.png b/s4e-charge/assets/walkie_talkie/history_anim/anim13.png new file mode 100644 index 0000000..b4338f7 Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/history_anim/anim13.png differ diff --git a/s4e-charge/assets/walkie_talkie/history_anim/anim14.png b/s4e-charge/assets/walkie_talkie/history_anim/anim14.png new file mode 100644 index 0000000..6dd42f1 Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/history_anim/anim14.png differ diff --git a/s4e-charge/assets/walkie_talkie/history_anim/anim15.png b/s4e-charge/assets/walkie_talkie/history_anim/anim15.png new file mode 100644 index 0000000..e4795f0 Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/history_anim/anim15.png differ diff --git a/s4e-charge/assets/walkie_talkie/history_anim/anim16.png b/s4e-charge/assets/walkie_talkie/history_anim/anim16.png new file mode 100644 index 0000000..5cdb8ad Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/history_anim/anim16.png differ diff --git a/s4e-charge/assets/walkie_talkie/history_anim/anim17.png b/s4e-charge/assets/walkie_talkie/history_anim/anim17.png new file mode 100644 index 0000000..9eb6f8b Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/history_anim/anim17.png differ diff --git a/s4e-charge/assets/walkie_talkie/history_anim/anim18.png b/s4e-charge/assets/walkie_talkie/history_anim/anim18.png new file mode 100644 index 0000000..cdbf172 Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/history_anim/anim18.png differ diff --git a/s4e-charge/assets/walkie_talkie/history_anim/anim2.png b/s4e-charge/assets/walkie_talkie/history_anim/anim2.png new file mode 100644 index 0000000..5cdb8ad Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/history_anim/anim2.png differ diff --git a/s4e-charge/assets/walkie_talkie/history_anim/anim3.png b/s4e-charge/assets/walkie_talkie/history_anim/anim3.png new file mode 100644 index 0000000..e4795f0 Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/history_anim/anim3.png differ diff --git a/s4e-charge/assets/walkie_talkie/history_anim/anim4.png b/s4e-charge/assets/walkie_talkie/history_anim/anim4.png new file mode 100644 index 0000000..6dd42f1 Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/history_anim/anim4.png differ diff --git a/s4e-charge/assets/walkie_talkie/history_anim/anim5.png b/s4e-charge/assets/walkie_talkie/history_anim/anim5.png new file mode 100644 index 0000000..b4338f7 Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/history_anim/anim5.png differ diff --git a/s4e-charge/assets/walkie_talkie/history_anim/anim6.png b/s4e-charge/assets/walkie_talkie/history_anim/anim6.png new file mode 100644 index 0000000..32c452c Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/history_anim/anim6.png differ diff --git a/s4e-charge/assets/walkie_talkie/history_anim/anim7.png b/s4e-charge/assets/walkie_talkie/history_anim/anim7.png new file mode 100644 index 0000000..38d3985 Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/history_anim/anim7.png differ diff --git a/s4e-charge/assets/walkie_talkie/history_anim/anim8.png b/s4e-charge/assets/walkie_talkie/history_anim/anim8.png new file mode 100644 index 0000000..927d24a Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/history_anim/anim8.png differ diff --git a/s4e-charge/assets/walkie_talkie/history_anim/anim9.png b/s4e-charge/assets/walkie_talkie/history_anim/anim9.png new file mode 100644 index 0000000..6d5503a Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/history_anim/anim9.png differ diff --git a/s4e-charge/assets/walkie_talkie/history_anim_right/anim0.png b/s4e-charge/assets/walkie_talkie/history_anim_right/anim0.png new file mode 100644 index 0000000..cdbf172 Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/history_anim_right/anim0.png differ diff --git a/s4e-charge/assets/walkie_talkie/history_anim_right/anim1.png b/s4e-charge/assets/walkie_talkie/history_anim_right/anim1.png new file mode 100644 index 0000000..5d99f04 Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/history_anim_right/anim1.png differ diff --git a/s4e-charge/assets/walkie_talkie/history_anim_right/anim10.png b/s4e-charge/assets/walkie_talkie/history_anim_right/anim10.png new file mode 100644 index 0000000..493b920 Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/history_anim_right/anim10.png differ diff --git a/s4e-charge/assets/walkie_talkie/history_anim_right/anim11.png b/s4e-charge/assets/walkie_talkie/history_anim_right/anim11.png new file mode 100644 index 0000000..e596207 Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/history_anim_right/anim11.png differ diff --git a/s4e-charge/assets/walkie_talkie/history_anim_right/anim12.png b/s4e-charge/assets/walkie_talkie/history_anim_right/anim12.png new file mode 100644 index 0000000..44ccabf Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/history_anim_right/anim12.png differ diff --git a/s4e-charge/assets/walkie_talkie/history_anim_right/anim13.png b/s4e-charge/assets/walkie_talkie/history_anim_right/anim13.png new file mode 100644 index 0000000..cc6d96c Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/history_anim_right/anim13.png differ diff --git a/s4e-charge/assets/walkie_talkie/history_anim_right/anim14.png b/s4e-charge/assets/walkie_talkie/history_anim_right/anim14.png new file mode 100644 index 0000000..709fe42 Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/history_anim_right/anim14.png differ diff --git a/s4e-charge/assets/walkie_talkie/history_anim_right/anim15.png b/s4e-charge/assets/walkie_talkie/history_anim_right/anim15.png new file mode 100644 index 0000000..08c909e Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/history_anim_right/anim15.png differ diff --git a/s4e-charge/assets/walkie_talkie/history_anim_right/anim16.png b/s4e-charge/assets/walkie_talkie/history_anim_right/anim16.png new file mode 100644 index 0000000..5b5a34a Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/history_anim_right/anim16.png differ diff --git a/s4e-charge/assets/walkie_talkie/history_anim_right/anim17.png b/s4e-charge/assets/walkie_talkie/history_anim_right/anim17.png new file mode 100644 index 0000000..5d99f04 Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/history_anim_right/anim17.png differ diff --git a/s4e-charge/assets/walkie_talkie/history_anim_right/anim18.png b/s4e-charge/assets/walkie_talkie/history_anim_right/anim18.png new file mode 100644 index 0000000..cdbf172 Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/history_anim_right/anim18.png differ diff --git a/s4e-charge/assets/walkie_talkie/history_anim_right/anim2.png b/s4e-charge/assets/walkie_talkie/history_anim_right/anim2.png new file mode 100644 index 0000000..5b5a34a Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/history_anim_right/anim2.png differ diff --git a/s4e-charge/assets/walkie_talkie/history_anim_right/anim3.png b/s4e-charge/assets/walkie_talkie/history_anim_right/anim3.png new file mode 100644 index 0000000..08c909e Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/history_anim_right/anim3.png differ diff --git a/s4e-charge/assets/walkie_talkie/history_anim_right/anim4.png b/s4e-charge/assets/walkie_talkie/history_anim_right/anim4.png new file mode 100644 index 0000000..709fe42 Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/history_anim_right/anim4.png differ diff --git a/s4e-charge/assets/walkie_talkie/history_anim_right/anim5.png b/s4e-charge/assets/walkie_talkie/history_anim_right/anim5.png new file mode 100644 index 0000000..cc6d96c Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/history_anim_right/anim5.png differ diff --git a/s4e-charge/assets/walkie_talkie/history_anim_right/anim6.png b/s4e-charge/assets/walkie_talkie/history_anim_right/anim6.png new file mode 100644 index 0000000..44ccabf Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/history_anim_right/anim6.png differ diff --git a/s4e-charge/assets/walkie_talkie/history_anim_right/anim7.png b/s4e-charge/assets/walkie_talkie/history_anim_right/anim7.png new file mode 100644 index 0000000..e596207 Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/history_anim_right/anim7.png differ diff --git a/s4e-charge/assets/walkie_talkie/history_anim_right/anim8.png b/s4e-charge/assets/walkie_talkie/history_anim_right/anim8.png new file mode 100644 index 0000000..493b920 Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/history_anim_right/anim8.png differ diff --git a/s4e-charge/assets/walkie_talkie/history_anim_right/anim9.png b/s4e-charge/assets/walkie_talkie/history_anim_right/anim9.png new file mode 100644 index 0000000..2286ae4 Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/history_anim_right/anim9.png differ diff --git a/s4e-charge/assets/walkie_talkie/icon_plus.png b/s4e-charge/assets/walkie_talkie/icon_plus.png new file mode 100644 index 0000000..a1117ab Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/icon_plus.png differ diff --git a/s4e-charge/assets/walkie_talkie/image/Speaker0.png b/s4e-charge/assets/walkie_talkie/image/Speaker0.png new file mode 100644 index 0000000..fc91da7 Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/image/Speaker0.png differ diff --git a/s4e-charge/assets/walkie_talkie/image/Speaker1.png b/s4e-charge/assets/walkie_talkie/image/Speaker1.png new file mode 100644 index 0000000..66fc559 Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/image/Speaker1.png differ diff --git a/s4e-charge/assets/walkie_talkie/image/Speaker10.png b/s4e-charge/assets/walkie_talkie/image/Speaker10.png new file mode 100644 index 0000000..2bb9b59 Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/image/Speaker10.png differ diff --git a/s4e-charge/assets/walkie_talkie/image/Speaker100.png b/s4e-charge/assets/walkie_talkie/image/Speaker100.png new file mode 100644 index 0000000..521ed3e Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/image/Speaker100.png differ diff --git a/s4e-charge/assets/walkie_talkie/image/Speaker101.png b/s4e-charge/assets/walkie_talkie/image/Speaker101.png new file mode 100644 index 0000000..9b955dd Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/image/Speaker101.png differ diff --git a/s4e-charge/assets/walkie_talkie/image/Speaker102.png b/s4e-charge/assets/walkie_talkie/image/Speaker102.png new file mode 100644 index 0000000..7825c03 Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/image/Speaker102.png differ diff --git a/s4e-charge/assets/walkie_talkie/image/Speaker11.png b/s4e-charge/assets/walkie_talkie/image/Speaker11.png new file mode 100644 index 0000000..c4415dd Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/image/Speaker11.png differ diff --git a/s4e-charge/assets/walkie_talkie/image/Speaker12.png b/s4e-charge/assets/walkie_talkie/image/Speaker12.png new file mode 100644 index 0000000..844818b Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/image/Speaker12.png differ diff --git a/s4e-charge/assets/walkie_talkie/image/Speaker13.png b/s4e-charge/assets/walkie_talkie/image/Speaker13.png new file mode 100644 index 0000000..89029fd Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/image/Speaker13.png differ diff --git a/s4e-charge/assets/walkie_talkie/image/Speaker14.png b/s4e-charge/assets/walkie_talkie/image/Speaker14.png new file mode 100644 index 0000000..221f19a Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/image/Speaker14.png differ diff --git a/s4e-charge/assets/walkie_talkie/image/Speaker15.png b/s4e-charge/assets/walkie_talkie/image/Speaker15.png new file mode 100644 index 0000000..4423df7 Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/image/Speaker15.png differ diff --git a/s4e-charge/assets/walkie_talkie/image/Speaker16.png b/s4e-charge/assets/walkie_talkie/image/Speaker16.png new file mode 100644 index 0000000..bd8aa8a Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/image/Speaker16.png differ diff --git a/s4e-charge/assets/walkie_talkie/image/Speaker17.png b/s4e-charge/assets/walkie_talkie/image/Speaker17.png new file mode 100644 index 0000000..9ea2e8b Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/image/Speaker17.png differ diff --git a/s4e-charge/assets/walkie_talkie/image/Speaker18.png b/s4e-charge/assets/walkie_talkie/image/Speaker18.png new file mode 100644 index 0000000..fb5a10b Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/image/Speaker18.png differ diff --git a/s4e-charge/assets/walkie_talkie/image/Speaker19.png b/s4e-charge/assets/walkie_talkie/image/Speaker19.png new file mode 100644 index 0000000..3ad3bf7 Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/image/Speaker19.png differ diff --git a/s4e-charge/assets/walkie_talkie/image/Speaker2.png b/s4e-charge/assets/walkie_talkie/image/Speaker2.png new file mode 100644 index 0000000..8601636 Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/image/Speaker2.png differ diff --git a/s4e-charge/assets/walkie_talkie/image/Speaker20.png b/s4e-charge/assets/walkie_talkie/image/Speaker20.png new file mode 100644 index 0000000..b6ea25b Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/image/Speaker20.png differ diff --git a/s4e-charge/assets/walkie_talkie/image/Speaker21.png b/s4e-charge/assets/walkie_talkie/image/Speaker21.png new file mode 100644 index 0000000..14200fd Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/image/Speaker21.png differ diff --git a/s4e-charge/assets/walkie_talkie/image/Speaker22.png b/s4e-charge/assets/walkie_talkie/image/Speaker22.png new file mode 100644 index 0000000..3d9c48f Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/image/Speaker22.png differ diff --git a/s4e-charge/assets/walkie_talkie/image/Speaker23.png b/s4e-charge/assets/walkie_talkie/image/Speaker23.png new file mode 100644 index 0000000..e91fd3d Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/image/Speaker23.png differ diff --git a/s4e-charge/assets/walkie_talkie/image/Speaker24.png b/s4e-charge/assets/walkie_talkie/image/Speaker24.png new file mode 100644 index 0000000..393fc07 Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/image/Speaker24.png differ diff --git a/s4e-charge/assets/walkie_talkie/image/Speaker25.png b/s4e-charge/assets/walkie_talkie/image/Speaker25.png new file mode 100644 index 0000000..fa9e68e Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/image/Speaker25.png differ diff --git a/s4e-charge/assets/walkie_talkie/image/Speaker26.png b/s4e-charge/assets/walkie_talkie/image/Speaker26.png new file mode 100644 index 0000000..6d43f7d Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/image/Speaker26.png differ diff --git a/s4e-charge/assets/walkie_talkie/image/Speaker27.png b/s4e-charge/assets/walkie_talkie/image/Speaker27.png new file mode 100644 index 0000000..0a65418 Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/image/Speaker27.png differ diff --git a/s4e-charge/assets/walkie_talkie/image/Speaker28.png b/s4e-charge/assets/walkie_talkie/image/Speaker28.png new file mode 100644 index 0000000..b0160e4 Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/image/Speaker28.png differ diff --git a/s4e-charge/assets/walkie_talkie/image/Speaker29.png b/s4e-charge/assets/walkie_talkie/image/Speaker29.png new file mode 100644 index 0000000..1a8c275 Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/image/Speaker29.png differ diff --git a/s4e-charge/assets/walkie_talkie/image/Speaker3.png b/s4e-charge/assets/walkie_talkie/image/Speaker3.png new file mode 100644 index 0000000..142da0d Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/image/Speaker3.png differ diff --git a/s4e-charge/assets/walkie_talkie/image/Speaker30.png b/s4e-charge/assets/walkie_talkie/image/Speaker30.png new file mode 100644 index 0000000..c07d68b Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/image/Speaker30.png differ diff --git a/s4e-charge/assets/walkie_talkie/image/Speaker31.png b/s4e-charge/assets/walkie_talkie/image/Speaker31.png new file mode 100644 index 0000000..b5bf877 Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/image/Speaker31.png differ diff --git a/s4e-charge/assets/walkie_talkie/image/Speaker32.png b/s4e-charge/assets/walkie_talkie/image/Speaker32.png new file mode 100644 index 0000000..c7635aa Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/image/Speaker32.png differ diff --git a/s4e-charge/assets/walkie_talkie/image/Speaker33.png b/s4e-charge/assets/walkie_talkie/image/Speaker33.png new file mode 100644 index 0000000..55c5c23 Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/image/Speaker33.png differ diff --git a/s4e-charge/assets/walkie_talkie/image/Speaker34.png b/s4e-charge/assets/walkie_talkie/image/Speaker34.png new file mode 100644 index 0000000..8291262 Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/image/Speaker34.png differ diff --git a/s4e-charge/assets/walkie_talkie/image/Speaker35.png b/s4e-charge/assets/walkie_talkie/image/Speaker35.png new file mode 100644 index 0000000..7299baa Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/image/Speaker35.png differ diff --git a/s4e-charge/assets/walkie_talkie/image/Speaker36.png b/s4e-charge/assets/walkie_talkie/image/Speaker36.png new file mode 100644 index 0000000..ab8b08e Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/image/Speaker36.png differ diff --git a/s4e-charge/assets/walkie_talkie/image/Speaker37.png b/s4e-charge/assets/walkie_talkie/image/Speaker37.png new file mode 100644 index 0000000..5f76111 Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/image/Speaker37.png differ diff --git a/s4e-charge/assets/walkie_talkie/image/Speaker38.png b/s4e-charge/assets/walkie_talkie/image/Speaker38.png new file mode 100644 index 0000000..41cb88e Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/image/Speaker38.png differ diff --git a/s4e-charge/assets/walkie_talkie/image/Speaker39.png b/s4e-charge/assets/walkie_talkie/image/Speaker39.png new file mode 100644 index 0000000..72a5863 Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/image/Speaker39.png differ diff --git a/s4e-charge/assets/walkie_talkie/image/Speaker4.png b/s4e-charge/assets/walkie_talkie/image/Speaker4.png new file mode 100644 index 0000000..c7ad479 Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/image/Speaker4.png differ diff --git a/s4e-charge/assets/walkie_talkie/image/Speaker40.png b/s4e-charge/assets/walkie_talkie/image/Speaker40.png new file mode 100644 index 0000000..81c7391 Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/image/Speaker40.png differ diff --git a/s4e-charge/assets/walkie_talkie/image/Speaker41.png b/s4e-charge/assets/walkie_talkie/image/Speaker41.png new file mode 100644 index 0000000..9881ad6 Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/image/Speaker41.png differ diff --git a/s4e-charge/assets/walkie_talkie/image/Speaker42.png b/s4e-charge/assets/walkie_talkie/image/Speaker42.png new file mode 100644 index 0000000..7e9ec32 Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/image/Speaker42.png differ diff --git a/s4e-charge/assets/walkie_talkie/image/Speaker43.png b/s4e-charge/assets/walkie_talkie/image/Speaker43.png new file mode 100644 index 0000000..216afb2 Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/image/Speaker43.png differ diff --git a/s4e-charge/assets/walkie_talkie/image/Speaker44.png b/s4e-charge/assets/walkie_talkie/image/Speaker44.png new file mode 100644 index 0000000..873728e Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/image/Speaker44.png differ diff --git a/s4e-charge/assets/walkie_talkie/image/Speaker45.png b/s4e-charge/assets/walkie_talkie/image/Speaker45.png new file mode 100644 index 0000000..70535de Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/image/Speaker45.png differ diff --git a/s4e-charge/assets/walkie_talkie/image/Speaker46.png b/s4e-charge/assets/walkie_talkie/image/Speaker46.png new file mode 100644 index 0000000..43563a3 Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/image/Speaker46.png differ diff --git a/s4e-charge/assets/walkie_talkie/image/Speaker47.png b/s4e-charge/assets/walkie_talkie/image/Speaker47.png new file mode 100644 index 0000000..f7bd1b7 Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/image/Speaker47.png differ diff --git a/s4e-charge/assets/walkie_talkie/image/Speaker48.png b/s4e-charge/assets/walkie_talkie/image/Speaker48.png new file mode 100644 index 0000000..6a942f9 Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/image/Speaker48.png differ diff --git a/s4e-charge/assets/walkie_talkie/image/Speaker49.png b/s4e-charge/assets/walkie_talkie/image/Speaker49.png new file mode 100644 index 0000000..f65235c Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/image/Speaker49.png differ diff --git a/s4e-charge/assets/walkie_talkie/image/Speaker5.png b/s4e-charge/assets/walkie_talkie/image/Speaker5.png new file mode 100644 index 0000000..8681f10 Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/image/Speaker5.png differ diff --git a/s4e-charge/assets/walkie_talkie/image/Speaker50.png b/s4e-charge/assets/walkie_talkie/image/Speaker50.png new file mode 100644 index 0000000..be90a63 Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/image/Speaker50.png differ diff --git a/s4e-charge/assets/walkie_talkie/image/Speaker51.png b/s4e-charge/assets/walkie_talkie/image/Speaker51.png new file mode 100644 index 0000000..d6de73f Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/image/Speaker51.png differ diff --git a/s4e-charge/assets/walkie_talkie/image/Speaker52.png b/s4e-charge/assets/walkie_talkie/image/Speaker52.png new file mode 100644 index 0000000..8b58b2d Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/image/Speaker52.png differ diff --git a/s4e-charge/assets/walkie_talkie/image/Speaker53.png b/s4e-charge/assets/walkie_talkie/image/Speaker53.png new file mode 100644 index 0000000..7b3b206 Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/image/Speaker53.png differ diff --git a/s4e-charge/assets/walkie_talkie/image/Speaker54.png b/s4e-charge/assets/walkie_talkie/image/Speaker54.png new file mode 100644 index 0000000..2367008 Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/image/Speaker54.png differ diff --git a/s4e-charge/assets/walkie_talkie/image/Speaker55.png b/s4e-charge/assets/walkie_talkie/image/Speaker55.png new file mode 100644 index 0000000..dbdf016 Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/image/Speaker55.png differ diff --git a/s4e-charge/assets/walkie_talkie/image/Speaker56.png b/s4e-charge/assets/walkie_talkie/image/Speaker56.png new file mode 100644 index 0000000..1d3eacb Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/image/Speaker56.png differ diff --git a/s4e-charge/assets/walkie_talkie/image/Speaker57.png b/s4e-charge/assets/walkie_talkie/image/Speaker57.png new file mode 100644 index 0000000..97c19e3 Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/image/Speaker57.png differ diff --git a/s4e-charge/assets/walkie_talkie/image/Speaker58.png b/s4e-charge/assets/walkie_talkie/image/Speaker58.png new file mode 100644 index 0000000..a7195d0 Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/image/Speaker58.png differ diff --git a/s4e-charge/assets/walkie_talkie/image/Speaker59.png b/s4e-charge/assets/walkie_talkie/image/Speaker59.png new file mode 100644 index 0000000..3e36cba Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/image/Speaker59.png differ diff --git a/s4e-charge/assets/walkie_talkie/image/Speaker6.png b/s4e-charge/assets/walkie_talkie/image/Speaker6.png new file mode 100644 index 0000000..2503467 Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/image/Speaker6.png differ diff --git a/s4e-charge/assets/walkie_talkie/image/Speaker60.png b/s4e-charge/assets/walkie_talkie/image/Speaker60.png new file mode 100644 index 0000000..cb0e1b9 Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/image/Speaker60.png differ diff --git a/s4e-charge/assets/walkie_talkie/image/Speaker61.png b/s4e-charge/assets/walkie_talkie/image/Speaker61.png new file mode 100644 index 0000000..93b8905 Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/image/Speaker61.png differ diff --git a/s4e-charge/assets/walkie_talkie/image/Speaker62.png b/s4e-charge/assets/walkie_talkie/image/Speaker62.png new file mode 100644 index 0000000..d171dab Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/image/Speaker62.png differ diff --git a/s4e-charge/assets/walkie_talkie/image/Speaker63.png b/s4e-charge/assets/walkie_talkie/image/Speaker63.png new file mode 100644 index 0000000..3bdfd0f Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/image/Speaker63.png differ diff --git a/s4e-charge/assets/walkie_talkie/image/Speaker64.png b/s4e-charge/assets/walkie_talkie/image/Speaker64.png new file mode 100644 index 0000000..392093f Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/image/Speaker64.png differ diff --git a/s4e-charge/assets/walkie_talkie/image/Speaker65.png b/s4e-charge/assets/walkie_talkie/image/Speaker65.png new file mode 100644 index 0000000..e26ca7c Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/image/Speaker65.png differ diff --git a/s4e-charge/assets/walkie_talkie/image/Speaker66.png b/s4e-charge/assets/walkie_talkie/image/Speaker66.png new file mode 100644 index 0000000..57aff14 Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/image/Speaker66.png differ diff --git a/s4e-charge/assets/walkie_talkie/image/Speaker67.png b/s4e-charge/assets/walkie_talkie/image/Speaker67.png new file mode 100644 index 0000000..2b7281a Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/image/Speaker67.png differ diff --git a/s4e-charge/assets/walkie_talkie/image/Speaker68.png b/s4e-charge/assets/walkie_talkie/image/Speaker68.png new file mode 100644 index 0000000..cdb5592 Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/image/Speaker68.png differ diff --git a/s4e-charge/assets/walkie_talkie/image/Speaker69.png b/s4e-charge/assets/walkie_talkie/image/Speaker69.png new file mode 100644 index 0000000..dc7eba5 Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/image/Speaker69.png differ diff --git a/s4e-charge/assets/walkie_talkie/image/Speaker7.png b/s4e-charge/assets/walkie_talkie/image/Speaker7.png new file mode 100644 index 0000000..054a489 Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/image/Speaker7.png differ diff --git a/s4e-charge/assets/walkie_talkie/image/Speaker70.png b/s4e-charge/assets/walkie_talkie/image/Speaker70.png new file mode 100644 index 0000000..6bbe93d Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/image/Speaker70.png differ diff --git a/s4e-charge/assets/walkie_talkie/image/Speaker71.png b/s4e-charge/assets/walkie_talkie/image/Speaker71.png new file mode 100644 index 0000000..bc451fb Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/image/Speaker71.png differ diff --git a/s4e-charge/assets/walkie_talkie/image/Speaker72.png b/s4e-charge/assets/walkie_talkie/image/Speaker72.png new file mode 100644 index 0000000..69b5ee7 Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/image/Speaker72.png differ diff --git a/s4e-charge/assets/walkie_talkie/image/Speaker73.png b/s4e-charge/assets/walkie_talkie/image/Speaker73.png new file mode 100644 index 0000000..909de00 Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/image/Speaker73.png differ diff --git a/s4e-charge/assets/walkie_talkie/image/Speaker74.png b/s4e-charge/assets/walkie_talkie/image/Speaker74.png new file mode 100644 index 0000000..a2d65b2 Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/image/Speaker74.png differ diff --git a/s4e-charge/assets/walkie_talkie/image/Speaker75.png b/s4e-charge/assets/walkie_talkie/image/Speaker75.png new file mode 100644 index 0000000..77d3cf6 Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/image/Speaker75.png differ diff --git a/s4e-charge/assets/walkie_talkie/image/Speaker76.png b/s4e-charge/assets/walkie_talkie/image/Speaker76.png new file mode 100644 index 0000000..4bacc59 Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/image/Speaker76.png differ diff --git a/s4e-charge/assets/walkie_talkie/image/Speaker77.png b/s4e-charge/assets/walkie_talkie/image/Speaker77.png new file mode 100644 index 0000000..3527380 Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/image/Speaker77.png differ diff --git a/s4e-charge/assets/walkie_talkie/image/Speaker78.png b/s4e-charge/assets/walkie_talkie/image/Speaker78.png new file mode 100644 index 0000000..f9cfe31 Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/image/Speaker78.png differ diff --git a/s4e-charge/assets/walkie_talkie/image/Speaker79.png b/s4e-charge/assets/walkie_talkie/image/Speaker79.png new file mode 100644 index 0000000..4583948 Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/image/Speaker79.png differ diff --git a/s4e-charge/assets/walkie_talkie/image/Speaker8.png b/s4e-charge/assets/walkie_talkie/image/Speaker8.png new file mode 100644 index 0000000..6b885ec Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/image/Speaker8.png differ diff --git a/s4e-charge/assets/walkie_talkie/image/Speaker80.png b/s4e-charge/assets/walkie_talkie/image/Speaker80.png new file mode 100644 index 0000000..6dbe5d3 Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/image/Speaker80.png differ diff --git a/s4e-charge/assets/walkie_talkie/image/Speaker81.png b/s4e-charge/assets/walkie_talkie/image/Speaker81.png new file mode 100644 index 0000000..cdff971 Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/image/Speaker81.png differ diff --git a/s4e-charge/assets/walkie_talkie/image/Speaker82.png b/s4e-charge/assets/walkie_talkie/image/Speaker82.png new file mode 100644 index 0000000..304ccea Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/image/Speaker82.png differ diff --git a/s4e-charge/assets/walkie_talkie/image/Speaker83.png b/s4e-charge/assets/walkie_talkie/image/Speaker83.png new file mode 100644 index 0000000..fe62e25 Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/image/Speaker83.png differ diff --git a/s4e-charge/assets/walkie_talkie/image/Speaker84.png b/s4e-charge/assets/walkie_talkie/image/Speaker84.png new file mode 100644 index 0000000..263f40c Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/image/Speaker84.png differ diff --git a/s4e-charge/assets/walkie_talkie/image/Speaker85.png b/s4e-charge/assets/walkie_talkie/image/Speaker85.png new file mode 100644 index 0000000..060ae88 Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/image/Speaker85.png differ diff --git a/s4e-charge/assets/walkie_talkie/image/Speaker86.png b/s4e-charge/assets/walkie_talkie/image/Speaker86.png new file mode 100644 index 0000000..c8fec2a Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/image/Speaker86.png differ diff --git a/s4e-charge/assets/walkie_talkie/image/Speaker87.png b/s4e-charge/assets/walkie_talkie/image/Speaker87.png new file mode 100644 index 0000000..053ee6d Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/image/Speaker87.png differ diff --git a/s4e-charge/assets/walkie_talkie/image/Speaker88.png b/s4e-charge/assets/walkie_talkie/image/Speaker88.png new file mode 100644 index 0000000..15505f5 Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/image/Speaker88.png differ diff --git a/s4e-charge/assets/walkie_talkie/image/Speaker89.png b/s4e-charge/assets/walkie_talkie/image/Speaker89.png new file mode 100644 index 0000000..162e637 Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/image/Speaker89.png differ diff --git a/s4e-charge/assets/walkie_talkie/image/Speaker9.png b/s4e-charge/assets/walkie_talkie/image/Speaker9.png new file mode 100644 index 0000000..03b9b1e Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/image/Speaker9.png differ diff --git a/s4e-charge/assets/walkie_talkie/image/Speaker90.png b/s4e-charge/assets/walkie_talkie/image/Speaker90.png new file mode 100644 index 0000000..ceec461 Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/image/Speaker90.png differ diff --git a/s4e-charge/assets/walkie_talkie/image/Speaker91.png b/s4e-charge/assets/walkie_talkie/image/Speaker91.png new file mode 100644 index 0000000..b3b35ae Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/image/Speaker91.png differ diff --git a/s4e-charge/assets/walkie_talkie/image/Speaker92.png b/s4e-charge/assets/walkie_talkie/image/Speaker92.png new file mode 100644 index 0000000..3464b7d Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/image/Speaker92.png differ diff --git a/s4e-charge/assets/walkie_talkie/image/Speaker93.png b/s4e-charge/assets/walkie_talkie/image/Speaker93.png new file mode 100644 index 0000000..de800f6 Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/image/Speaker93.png differ diff --git a/s4e-charge/assets/walkie_talkie/image/Speaker94.png b/s4e-charge/assets/walkie_talkie/image/Speaker94.png new file mode 100644 index 0000000..51e6c2a Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/image/Speaker94.png differ diff --git a/s4e-charge/assets/walkie_talkie/image/Speaker95.png b/s4e-charge/assets/walkie_talkie/image/Speaker95.png new file mode 100644 index 0000000..a22dffe Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/image/Speaker95.png differ diff --git a/s4e-charge/assets/walkie_talkie/image/Speaker96.png b/s4e-charge/assets/walkie_talkie/image/Speaker96.png new file mode 100644 index 0000000..f7151aa Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/image/Speaker96.png differ diff --git a/s4e-charge/assets/walkie_talkie/image/Speaker97.png b/s4e-charge/assets/walkie_talkie/image/Speaker97.png new file mode 100644 index 0000000..eae1262 Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/image/Speaker97.png differ diff --git a/s4e-charge/assets/walkie_talkie/image/Speaker98.png b/s4e-charge/assets/walkie_talkie/image/Speaker98.png new file mode 100644 index 0000000..328931c Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/image/Speaker98.png differ diff --git a/s4e-charge/assets/walkie_talkie/image/Speaker99.png b/s4e-charge/assets/walkie_talkie/image/Speaker99.png new file mode 100644 index 0000000..11a494e Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/image/Speaker99.png differ diff --git a/s4e-charge/assets/walkie_talkie/image/go_next.png b/s4e-charge/assets/walkie_talkie/image/go_next.png new file mode 100644 index 0000000..e9a1d81 Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/image/go_next.png differ diff --git a/s4e-charge/assets/walkie_talkie/image/stop.png b/s4e-charge/assets/walkie_talkie/image/stop.png new file mode 100644 index 0000000..b611709 Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/image/stop.png differ diff --git a/s4e-charge/assets/walkie_talkie/launcher.png b/s4e-charge/assets/walkie_talkie/launcher.png new file mode 100644 index 0000000..9a611a9 Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/launcher.png differ diff --git a/s4e-charge/assets/walkie_talkie/launcher1.png b/s4e-charge/assets/walkie_talkie/launcher1.png new file mode 100644 index 0000000..60aa469 Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/launcher1.png differ diff --git a/s4e-charge/assets/walkie_talkie/listen.png b/s4e-charge/assets/walkie_talkie/listen.png new file mode 100644 index 0000000..e06f591 Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/listen.png differ diff --git a/s4e-charge/assets/walkie_talkie/login_success.png b/s4e-charge/assets/walkie_talkie/login_success.png new file mode 100644 index 0000000..6e8c826 Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/login_success.png differ diff --git a/s4e-charge/assets/walkie_talkie/next_btn.png b/s4e-charge/assets/walkie_talkie/next_btn.png new file mode 100644 index 0000000..415abc0 Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/next_btn.png differ diff --git a/s4e-charge/assets/walkie_talkie/poc/default_icon.png b/s4e-charge/assets/walkie_talkie/poc/default_icon.png new file mode 100644 index 0000000..d5c33f0 Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/poc/default_icon.png differ diff --git a/s4e-charge/assets/walkie_talkie/poc/group_icon1.png b/s4e-charge/assets/walkie_talkie/poc/group_icon1.png new file mode 100644 index 0000000..9f22404 Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/poc/group_icon1.png differ diff --git a/s4e-charge/assets/walkie_talkie/poc/group_icon2.png b/s4e-charge/assets/walkie_talkie/poc/group_icon2.png new file mode 100644 index 0000000..0adef53 Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/poc/group_icon2.png differ diff --git a/s4e-charge/assets/walkie_talkie/poc/group_icon3.png b/s4e-charge/assets/walkie_talkie/poc/group_icon3.png new file mode 100644 index 0000000..4f1560b Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/poc/group_icon3.png differ diff --git a/s4e-charge/assets/walkie_talkie/poc/group_icon4.png b/s4e-charge/assets/walkie_talkie/poc/group_icon4.png new file mode 100644 index 0000000..5bb064a Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/poc/group_icon4.png differ diff --git a/s4e-charge/assets/walkie_talkie/poc/group_icon5.png b/s4e-charge/assets/walkie_talkie/poc/group_icon5.png new file mode 100644 index 0000000..abd170d Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/poc/group_icon5.png differ diff --git a/s4e-charge/assets/walkie_talkie/poc/member_icon.png b/s4e-charge/assets/walkie_talkie/poc/member_icon.png new file mode 100644 index 0000000..d5c33f0 Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/poc/member_icon.png differ diff --git a/s4e-charge/assets/walkie_talkie/poc/no_sound.png b/s4e-charge/assets/walkie_talkie/poc/no_sound.png new file mode 100644 index 0000000..2244475 Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/poc/no_sound.png differ diff --git a/s4e-charge/assets/walkie_talkie/poc/say_button_mic_icon.png b/s4e-charge/assets/walkie_talkie/poc/say_button_mic_icon.png new file mode 100644 index 0000000..64e5ea1 Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/poc/say_button_mic_icon.png differ diff --git a/s4e-charge/assets/walkie_talkie/poc/sound.png b/s4e-charge/assets/walkie_talkie/poc/sound.png new file mode 100644 index 0000000..9b7b453 Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/poc/sound.png differ diff --git a/s4e-charge/assets/walkie_talkie/poc/talk_listen.png b/s4e-charge/assets/walkie_talkie/poc/talk_listen.png new file mode 100644 index 0000000..c9a9688 Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/poc/talk_listen.png differ diff --git a/s4e-charge/assets/walkie_talkie/poc/toast_listen.png b/s4e-charge/assets/walkie_talkie/poc/toast_listen.png new file mode 100644 index 0000000..c9a9688 Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/poc/toast_listen.png differ diff --git a/s4e-charge/assets/walkie_talkie/poc/toast_say.png b/s4e-charge/assets/walkie_talkie/poc/toast_say.png new file mode 100644 index 0000000..f5b5dec Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/poc/toast_say.png differ diff --git a/s4e-charge/assets/walkie_talkie/poc/tools.png b/s4e-charge/assets/walkie_talkie/poc/tools.png new file mode 100644 index 0000000..e7cf9be Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/poc/tools.png differ diff --git a/s4e-charge/assets/walkie_talkie/poc/voice_icon.png b/s4e-charge/assets/walkie_talkie/poc/voice_icon.png new file mode 100644 index 0000000..85e366d Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/poc/voice_icon.png differ diff --git a/s4e-charge/assets/walkie_talkie/poc/voice_icon_rt.png b/s4e-charge/assets/walkie_talkie/poc/voice_icon_rt.png new file mode 100644 index 0000000..501320a Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/poc/voice_icon_rt.png differ diff --git a/s4e-charge/assets/walkie_talkie/speak.png b/s4e-charge/assets/walkie_talkie/speak.png new file mode 100644 index 0000000..1b715e3 Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/speak.png differ diff --git a/s4e-charge/assets/walkie_talkie/walkie_talkie.png b/s4e-charge/assets/walkie_talkie/walkie_talkie.png new file mode 100644 index 0000000..9a611a9 Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/walkie_talkie.png differ diff --git a/s4e-charge/assets/walkie_talkie/walkie_toast_icon.png b/s4e-charge/assets/walkie_talkie/walkie_toast_icon.png new file mode 100644 index 0000000..81f4e3f Binary files /dev/null and b/s4e-charge/assets/walkie_talkie/walkie_toast_icon.png differ diff --git a/s4e-charge/dist/S4Chg.face b/s4e-charge/dist/S4Chg.face new file mode 100644 index 0000000..0b633f5 Binary files /dev/null and b/s4e-charge/dist/S4Chg.face differ diff --git a/s4e-charge/dist/S4ChgRestore.face b/s4e-charge/dist/S4ChgRestore.face new file mode 100644 index 0000000..fc71083 Binary files /dev/null and b/s4e-charge/dist/S4ChgRestore.face differ diff --git a/s4e-charge/docs/full_before_after.png b/s4e-charge/docs/full_before_after.png new file mode 100644 index 0000000..d8442b2 Binary files /dev/null and b/s4e-charge/docs/full_before_after.png differ diff --git a/s4e-charge/docs/full_preview.mp4 b/s4e-charge/docs/full_preview.mp4 new file mode 100644 index 0000000..83c00cf Binary files /dev/null and b/s4e-charge/docs/full_preview.mp4 differ diff --git a/s4e-charge/docs/quality_v1_vs_v2.png b/s4e-charge/docs/quality_v1_vs_v2.png new file mode 100644 index 0000000..c7474c8 Binary files /dev/null and b/s4e-charge/docs/quality_v1_vs_v2.png differ diff --git a/s4e-charge/docs/sample_before_after.png b/s4e-charge/docs/sample_before_after.png new file mode 100644 index 0000000..9cb1976 Binary files /dev/null and b/s4e-charge/docs/sample_before_after.png differ diff --git a/s4e-charge/docs/sample_preview.mp4 b/s4e-charge/docs/sample_preview.mp4 new file mode 100644 index 0000000..59982a8 Binary files /dev/null and b/s4e-charge/docs/sample_preview.mp4 differ diff --git a/s4e-charge/flash.sh b/s4e-charge/flash.sh new file mode 100644 index 0000000..39bd3a5 --- /dev/null +++ b/s4e-charge/flash.sh @@ -0,0 +1,299 @@ +#!/bin/sh +# S5 41mm 充电动画 -> S4 eSIM(只写负载) +# 把 flash.sh 与 payload/ 放到同一个目录,例如 /data/s4panim/ +# 然后: sh /data/s4panim/flash.sh 最后: reboot +# 还原用 restore.sh 或安装还原盘 S4ChgRestore.face + +DEV=/dev/system +BASE=/data/s4panim/payload + +echo "target check" +ls -l $DEV + +echo "== charge0 == +dd if=$BASE/000_charge0.bin of=$DEV bs=4 seek=18062976 count=28182 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=18062976 count=28182 +cmp /tmp/chk.bin $BASE/000_charge0.bin + +echo "== charge1 == +dd if=$BASE/001_charge1.bin of=$DEV bs=1 seek=72172032 count=79834 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=72172032 count=79834 +cmp /tmp/chk.bin $BASE/001_charge1.bin + +echo "== charge2 == +dd if=$BASE/002_charge2.bin of=$DEV bs=4 seek=17786752 count=20313 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=17786752 count=20313 +cmp /tmp/chk.bin $BASE/002_charge2.bin + +echo "== charge3 == +dd if=$BASE/003_charge3.bin of=$DEV bs=1 seek=70018048 count=82007 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=70018048 count=82007 +cmp /tmp/chk.bin $BASE/003_charge3.bin + +echo "== charge4 == +dd if=$BASE/004_charge4.bin of=$DEV bs=1 seek=68788224 count=83347 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=68788224 count=83347 +cmp /tmp/chk.bin $BASE/004_charge4.bin + +echo "== charge5 == +dd if=$BASE/005_charge5.bin of=$DEV bs=1 seek=67532800 count=84305 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=67532800 count=84305 +cmp /tmp/chk.bin $BASE/005_charge5.bin + +echo "== charge6 == +dd if=$BASE/006_charge6.bin of=$DEV bs=1 seek=66615296 count=84281 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=66615296 count=84281 +cmp /tmp/chk.bin $BASE/006_charge6.bin + +echo "== charge7 == +dd if=$BASE/007_charge7.bin of=$DEV bs=4 seek=16632704 count=21023 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=16632704 count=21023 +cmp /tmp/chk.bin $BASE/007_charge7.bin + +echo "== charge8 == +dd if=$BASE/008_charge8.bin of=$DEV bs=1 seek=66445312 count=85403 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=66445312 count=85403 +cmp /tmp/chk.bin $BASE/008_charge8.bin + +echo "== charge9 == +dd if=$BASE/009_charge9.bin of=$DEV bs=1 seek=66357760 count=87097 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=66357760 count=87097 +cmp /tmp/chk.bin $BASE/009_charge9.bin + +echo "== charge10 == +dd if=$BASE/010_charge10.bin of=$DEV bs=4 seek=18020864 count=22109 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=18020864 count=22109 +cmp /tmp/chk.bin $BASE/010_charge10.bin + +echo "== charge11 == +dd if=$BASE/011_charge11.bin of=$DEV bs=1 seek=71994368 count=88566 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=71994368 count=88566 +cmp /tmp/chk.bin $BASE/011_charge11.bin + +echo "== charge12 == +dd if=$BASE/012_charge12.bin of=$DEV bs=1 seek=71904256 count=89914 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=71904256 count=89914 +cmp /tmp/chk.bin $BASE/012_charge12.bin + +echo "== charge13 == +dd if=$BASE/013_charge13.bin of=$DEV bs=1 seek=71813120 count=91103 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=71813120 count=91103 +cmp /tmp/chk.bin $BASE/013_charge13.bin + +echo "== charge14 == +dd if=$BASE/014_charge14.bin of=$DEV bs=4 seek=17929856 count=23345 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=17929856 count=23345 +cmp /tmp/chk.bin $BASE/014_charge14.bin + +echo "== charge15 == +dd if=$BASE/015_charge15.bin of=$DEV bs=4 seek=17906176 count=23632 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=17906176 count=23632 +cmp /tmp/chk.bin $BASE/015_charge15.bin + +echo "== charge16 == +dd if=$BASE/016_charge16.bin of=$DEV bs=1 seek=71527936 count=96705 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=71527936 count=96705 +cmp /tmp/chk.bin $BASE/016_charge16.bin + +echo "== charge17 == +dd if=$BASE/017_charge17.bin of=$DEV bs=1 seek=71429120 count=98295 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=71429120 count=98295 +cmp /tmp/chk.bin $BASE/017_charge17.bin + +echo "== charge18 == +dd if=$BASE/018_charge18.bin of=$DEV bs=1 seek=71329280 count=99525 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=71329280 count=99525 +cmp /tmp/chk.bin $BASE/018_charge18.bin + +echo "== charge19 == +dd if=$BASE/019_charge19.bin of=$DEV bs=4 seek=17807104 count=25201 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=17807104 count=25201 +cmp /tmp/chk.bin $BASE/019_charge19.bin + +echo "== charge20 == +dd if=$BASE/020_charge20.bin of=$DEV bs=1 seek=71045632 count=101014 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=71045632 count=101014 +cmp /tmp/chk.bin $BASE/020_charge20.bin + +echo "== charge21 == +dd if=$BASE/021_charge21.bin of=$DEV bs=1 seek=70943744 count=101755 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=70943744 count=101755 +cmp /tmp/chk.bin $BASE/021_charge21.bin + +echo "== charge22 == +dd if=$BASE/022_charge22.bin of=$DEV bs=4 seek=17710080 count=25758 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=17710080 count=25758 +cmp /tmp/chk.bin $BASE/022_charge22.bin + +echo "== charge23 == +dd if=$BASE/023_charge23.bin of=$DEV bs=1 seek=70736384 count=103826 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=70736384 count=103826 +cmp /tmp/chk.bin $BASE/023_charge23.bin + +echo "== charge24 == +dd if=$BASE/024_charge24.bin of=$DEV bs=1 seek=70631936 count=104085 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=70631936 count=104085 +cmp /tmp/chk.bin $BASE/024_charge24.bin + +echo "== charge25 == +dd if=$BASE/025_charge25.bin of=$DEV bs=4 seek=17632000 count=25960 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=17632000 count=25960 +cmp /tmp/chk.bin $BASE/025_charge25.bin + +echo "== charge26 == +dd if=$BASE/026_charge26.bin of=$DEV bs=4 seek=17605888 count=26104 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=17605888 count=26104 +cmp /tmp/chk.bin $BASE/026_charge26.bin + +echo "== charge27 == +dd if=$BASE/027_charge27.bin of=$DEV bs=1 seek=70319616 count=103815 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=70319616 count=103815 +cmp /tmp/chk.bin $BASE/027_charge27.bin + +echo "== charge28 == +dd if=$BASE/028_charge28.bin of=$DEV bs=4 seek=17553024 count=26848 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=17553024 count=26848 +cmp /tmp/chk.bin $BASE/028_charge28.bin + +echo "== charge29 == +dd if=$BASE/029_charge29.bin of=$DEV bs=1 seek=70100480 count=111441 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=70100480 count=111441 +cmp /tmp/chk.bin $BASE/029_charge29.bin + +echo "== charge30 == +dd if=$BASE/030_charge30.bin of=$DEV bs=1 seek=69905408 count=112118 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=69905408 count=112118 +cmp /tmp/chk.bin $BASE/030_charge30.bin + +echo "== charge31 == +dd if=$BASE/031_charge31.bin of=$DEV bs=1 seek=69794304 count=110857 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=69794304 count=110857 +cmp /tmp/chk.bin $BASE/031_charge31.bin + +echo "== charge32 == +dd if=$BASE/032_charge32.bin of=$DEV bs=1 seek=69680640 count=113259 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=69680640 count=113259 +cmp /tmp/chk.bin $BASE/032_charge32.bin + +echo "== charge33 == +dd if=$BASE/033_charge33.bin of=$DEV bs=1 seek=69566464 count=113647 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=69566464 count=113647 +cmp /tmp/chk.bin $BASE/033_charge33.bin + +echo "== charge34 == +dd if=$BASE/034_charge34.bin of=$DEV bs=1 seek=69451776 count=114281 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=69451776 count=114281 +cmp /tmp/chk.bin $BASE/034_charge34.bin + +echo "== charge35 == +dd if=$BASE/035_charge35.bin of=$DEV bs=1 seek=69336064 count=115315 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=69336064 count=115315 +cmp /tmp/chk.bin $BASE/035_charge35.bin + +echo "== charge36 == +dd if=$BASE/036_charge36.bin of=$DEV bs=4 seek=17304960 count=29039 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=17304960 count=29039 +cmp /tmp/chk.bin $BASE/036_charge36.bin + +echo "== charge37 == +dd if=$BASE/037_charge37.bin of=$DEV bs=1 seek=69103616 count=116126 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=69103616 count=116126 +cmp /tmp/chk.bin $BASE/037_charge37.bin + +echo "== charge38 == +dd if=$BASE/038_charge38.bin of=$DEV bs=1 seek=68987392 count=115971 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=68987392 count=115971 +cmp /tmp/chk.bin $BASE/038_charge38.bin + +echo "== charge39 == +dd if=$BASE/039_charge39.bin of=$DEV bs=1 seek=68871680 count=115563 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=68871680 count=115563 +cmp /tmp/chk.bin $BASE/039_charge39.bin + +echo "== charge40 == +dd if=$BASE/040_charge40.bin of=$DEV bs=1 seek=68672512 count=115334 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=68672512 count=115334 +cmp /tmp/chk.bin $BASE/040_charge40.bin + +echo "== charge41 == +dd if=$BASE/041_charge41.bin of=$DEV bs=1 seek=68556288 count=115723 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=68556288 count=115723 +cmp /tmp/chk.bin $BASE/041_charge41.bin + +echo "== charge42 == +dd if=$BASE/042_charge42.bin of=$DEV bs=1 seek=68440064 count=116050 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=68440064 count=116050 +cmp /tmp/chk.bin $BASE/042_charge42.bin + +echo "== charge43 == +dd if=$BASE/043_charge43.bin of=$DEV bs=1 seek=68323328 count=116683 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=68323328 count=116683 +cmp /tmp/chk.bin $BASE/043_charge43.bin + +echo "== charge44 == +dd if=$BASE/044_charge44.bin of=$DEV bs=1 seek=68206080 count=116854 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=68206080 count=116854 +cmp /tmp/chk.bin $BASE/044_charge44.bin + +echo "== charge45 == +dd if=$BASE/045_charge45.bin of=$DEV bs=1 seek=68088320 count=117369 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=68088320 count=117369 +cmp /tmp/chk.bin $BASE/045_charge45.bin + +echo "== charge46 == +dd if=$BASE/046_charge46.bin of=$DEV bs=1 seek=67970048 count=118191 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=67970048 count=118191 +cmp /tmp/chk.bin $BASE/046_charge46.bin + +echo "== charge47 == +dd if=$BASE/047_charge47.bin of=$DEV bs=1 seek=67852800 count=117005 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=67852800 count=117005 +cmp /tmp/chk.bin $BASE/047_charge47.bin + +echo "== charge48 == +dd if=$BASE/048_charge48.bin of=$DEV bs=1 seek=67735040 count=117365 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=67735040 count=117365 +cmp /tmp/chk.bin $BASE/048_charge48.bin + +echo "== charge49 == +dd if=$BASE/049_charge49.bin of=$DEV bs=1 seek=67617280 count=117626 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=67617280 count=117626 +cmp /tmp/chk.bin $BASE/049_charge49.bin + +echo "== charge50 == +dd if=$BASE/050_charge50.bin of=$DEV bs=1 seek=67414528 count=117906 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=67414528 count=117906 +cmp /tmp/chk.bin $BASE/050_charge50.bin + +echo "== charge51 == +dd if=$BASE/051_charge51.bin of=$DEV bs=1 seek=67295744 count=118481 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=67295744 count=118481 +cmp /tmp/chk.bin $BASE/051_charge51.bin + +echo "== charge52 == +dd if=$BASE/052_charge52.bin of=$DEV bs=4 seek=16793984 count=29825 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=16793984 count=29825 +cmp /tmp/chk.bin $BASE/052_charge52.bin + +echo "== charge53 == +dd if=$BASE/053_charge53.bin of=$DEV bs=1 seek=67056128 count=119353 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=67056128 count=119353 +cmp /tmp/chk.bin $BASE/053_charge53.bin + +echo "== charge54 == +dd if=$BASE/054_charge54.bin of=$DEV bs=1 seek=66936832 count=119135 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=66936832 count=119135 +cmp /tmp/chk.bin $BASE/054_charge54.bin + +echo "== charge55 == +dd if=$BASE/055_charge55.bin of=$DEV bs=1 seek=66818560 count=117881 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=66818560 count=117881 +cmp /tmp/chk.bin $BASE/055_charge55.bin + +echo "== charge56 == +dd if=$BASE/056_charge56.bin of=$DEV bs=4 seek=16674944 count=29644 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=16674944 count=29644 +cmp /tmp/chk.bin $BASE/056_charge56.bin + +rm /tmp/chk.bin +echo "all done - run reboot to apply" diff --git a/s4e-charge/main.lua b/s4e-charge/main.lua new file mode 100644 index 0000000..b4687d5 --- /dev/null +++ b/s4e-charge/main.lua @@ -0,0 +1,735 @@ +--[[---------------------------------------------------------------------------- + S4Chg —— S5 41mm(OS4) 充电动画 -> S4 eSIM + + 界面按 CSS 设计稿 1:1 落地(本机用真 MiSans 字体渲染预览验证过): + * 页面底色纯黑(取自封面图四角) + * rgba(255,255,255,0.6) -> #999999(白 60% 叠黑) + * font-weight 600 -> MiSans-Semibold ; 500 -> MiSans-Medium + * vertical-align:middle -> 手工算 y(LVGL 标签没有垂直居中样式) + * 多色文本用 LVGL recolor:"灰#ffffff 白#灰" + * 圆屏安全区:实测所有页面 0 像素越界(大按钮全靠 40px 圆角收进圆内) + + 真机已验证的底层能力: + * 宿主不调用 ui.init -> 加载期建界面 + * SCRIPT_PATH 为空 -> 用 debug.getinfo 反推目录 + * .face 不解包,资源仍在 resource.bin 里 -> 按偏移读出来用 + * /dev/system 可写,dd 读写校验通过 + * 没有 utf8 标准库 -> 自带按位 UTF-8 解码(本文件未用到,留作提醒) +------------------------------------------------------------------------------]] + +local lvgl = require("lvgl") + +local W, H = 466, 466 +local DEV, BLK = "/dev/system", 4 +local TMPH = "/tmp/os4_head.bin" +local TMPR = "/tmp/os4_back.bin" + +-- 等长覆盖只保证「偏移」512 对齐;RLE 素材的长度常常不是 4 的倍数(如 1635 / 3047), +-- 那种记录必须退化成 bs=1(偏移本身对齐,所以 bs=1 也精确),否则会多写相邻文件的字节。 +local function ddBlk(off, len) + if off % BLK == 0 and len % BLK == 0 then return BLK end + return 1 +end +local TMPB = "/tmp/os4_blob.bin" + + +--------------------------------------------------------------------- 自身目录 +local SRC = "" +if debug and debug.getinfo then + local info = debug.getinfo(1, "S") + SRC = (info and info.source) or "" +end +if type(SRC) ~= "string" then SRC = "" end +if SRC:sub(1, 1) == "@" then SRC = SRC:sub(2) end +SRC = SRC:gsub("\\", "/") +local THIS_DIR = SRC:match("^(.*)/[^/]+$") or "" +local SP = THIS_DIR ~= "" and (THIS_DIR .. "/") or "" +local PARENT = SP:gsub("[^/]+/$", "") + +local RES = nil +do + local c = { PARENT .. "resource.bin", SP .. "../resource.bin", SP .. "resource.bin" } + for i = 1, #c do + local f = io.open(c[i], "rb") + if f then f:close(); RES = c[i]; break end + end +end + +--------------------------------------------------------------------- 资源偏移 +-- UI 图片 { resource.bin 内偏移, 长度, 名字 } —— 由 _tools/gen_ui_lua.py 回填 +local UIIMG = { +__UIIMG__ +} + +-- 角色:'replace' = 只负责替换(写 q63 的帧);'restore' = 只负责还原(写回原厂字节) +-- 两份盘共用同一个 watchface id,所以设备上只会占一份空间(见 README) +local ROLE = '__ROLE__' + +-- 负载表 { resource 内偏移, ROMFS 目标偏移, 长度 } —— 由脚本回填 +-- pay = 本盘要写进去的字节;rb = **另一侧字节的 64 B 指纹**(只给版本校验用) +local PAYTBL = { +__PAY__ +__RB__ +} + +local function prepImages() + local paths = {} + if not RES then return paths end + local f = io.open(RES, "rb") + if not f then return paths end + for i = 1, #UIIMG do + local e = UIIMG[i] + f:seek("set", e[1]) + local d = f:read(e[2]) + if d and #d == e[2] then + -- 直接写到 /tmp 根下:不能写子目录(目录不存在,且这台机器的 Lua 没有 mkdir) + local p = "/tmp/ui_" .. e[3] .. ".bin" + local o = io.open(p, "wb") + if o then o:write(d); o:close(); paths[e[3]] = p end + end + end + f:close() + return paths +end + +local IMG = prepImages() + +--------------------------------------------------------------------- 颜色与字体 +local C_GRAY, C_WHITE = '#999999', '#ffffff' +local C_OK, C_ERR = '#43cf7c', '#d43030' +local C_PLATE, C_CIRC = 0x002927, 0x4f4f4f + +local function getFont(name, size) + local f = nil + pcall(function() f = lvgl.Font(name, size) end) + return f +end + +local F_CLOCK = getFont('MiSans-Semibold', 28) +local F_TITLE = getFont('MiSans-Medium', 30) +local F_ITEM = getFont('MiSans-Medium', 28) +local F_BODY = getFont('MiSans-Medium', 24) +local F_VER = getFont('MiSans-Medium', 20) + +--------------------------------------------------------------------- 按下高亮 +-- onPressed / onClicked 是真实的 LVGL 对象方法(就在 onevent 旁边,属性表里可查) +local HL = 0x005149 -- 按下高亮色(比底板 0x002927 明显亮) +local pressed = nil -- { obj = , t = } +local tickCount = 0 + +local function setBg(obj, c) + pcall(function() obj:set { bg_color = c } end) +end + +local function pressRelease() + if pressed then + setBg(pressed.obj, C_PLATE) + pressed = nil + end +end + +--------------------------------------------------------------------- 基础 +local ui = {} +local built = false +local pages = {} +local clocks = {} +local job = { run = false, list = nil, i = 0, errs = 0, variant = 'replace' } +-- 从哪个入口进"开始替换"确认页,确认按钮就执行对应操作: +-- 首页「开始替换」 -> 'replace' +-- 帮助页「回滚修改」 -> 'rollback'(也要先过一遍警告确认,不直接开跑) +local pendingVariant = 'replace' +-- 确认页是从哪进来的,返回就回哪: +-- 首页「开始替换」 -> 'home' ; 帮助页「回滚修改」 -> 'help' +local confirmFrom = 'home' + +local function newPage() + local p = nil + pcall(function() + p = lvgl.Object(nil, { x = 0, y = 0, w = W, h = H, bg_color = 0x000000, + bg_opa = 255, border_width = 0, pad_all = 0 }) + p:clear_flag(lvgl.FLAG.SCROLLABLE) + p:add_flag(lvgl.FLAG.EVENT_BUBBLE) + end) + return p +end + +local function show(p) + if not p then return end + pressRelease() -- 切页时清掉可能残留的高亮 + for k, v in pairs(pages) do + pcall(function() v:add_flag(lvgl.FLAG.HIDDEN) end) + end + pcall(function() p:clear_flag(lvgl.FLAG.HIDDEN) end) +end + +local function bind(obj, fn) + if not obj then return end + -- 成品样本用的是 onevent(lvgl.EVENT.SHORT_CLICKED, fn)(CLICKED 这个常量 + -- 在这台机器上不存在),所以按 SHORT_CLICKED -> onClicked -> CLICKED 依次试 + local ev = lvgl.EVENT + if ev and ev.SHORT_CLICKED then + if pcall(function() obj:onevent(ev.SHORT_CLICKED, function() fn() end) end) then return end + end + if pcall(function() obj:onClicked(function() fn() end) end) then return end + pcall(function() obj:onevent(ev and ev.CLICKED, fn) end) +end + +--------------------------------------------------------------------- 点按 +-- 点按 = 按下高亮 + 抬起恢复 + 执行动作;带超时兜底(手指滑出时 click 不触发) +local function bindTap(obj, onTap) + if not obj then return end + local ok = pcall(function() + obj:onPressed(function() + setBg(obj, HL) + pressed = { obj = obj, t = tickCount } + end) + obj:onClicked(function() + pressRelease() + if onTap then onTap() end + end) + end) + if not ok then bind(obj, onTap) end +end + +local function setClickable(o) + if not o then return end + pcall(function() o:add_flag(lvgl.FLAG.CLICKABLE) end) +end + +local function lab(parent, o) + o.text_font = o.text_font or F_BODY + local l = nil + pcall(function() l = parent:Label(o) end) + return l +end + +local function clockLabel(parent) + -- 居中不用 text_align(它要的是 lv_text_align_t:LEFT=0/CENTER=1/RIGHT=2, + -- 传 ALIGN.CENTER=9 越界会被忽略 -> 实测变成左对齐)。 + -- 改为按真字体量出的文本宽度手工算 x,并把 w 收紧,这样对齐方式就不影响结果。 + local l = lab(parent, { x = 195, y = 3, w = 86, h = 45, text = '--:--', + text_color = C_GRAY, font_size = 28, text_font = F_CLOCK }) + clocks[#clocks + 1] = l + return l +end + +local function pageTitle(parent, text, x, w) + -- 标题 y:框 y=34 h=45,字号 30 行高约 41 -> 34 + (45-41)/2 = 36 + return lab(parent, { x = x, y = 36, w = w, h = 45, text = text, + text_color = C_WHITE, font_size = 30, text_font = F_TITLE }) +end + +local function backArrow(parent, x, onTap) + local im = nil + if IMG.back then + pcall(function() im = parent:Image { src = IMG.back, x = x or 160, y = 43 } end) + setClickable(im) + bind(im, onTap) + end + return im +end + +-- 列表项:底板(整块可点) + 圆形遮罩 + 图标 + 标题 +-- pad_all=0 是关键:LVGL 主题会给 Object 加内边距,不清零的话子元素会整体右下偏移 +local function listItem(parent, y, iconkey, title, onTap) + local plate = nil + pcall(function() + plate = parent:Object { x = 49, y = y, w = 368, h = 88, pad_all = 0, + bg_color = C_PLATE, border_width = 0, radius = 34 } + end) + if plate then + if onTap then + setClickable(plate) + bindTap(plate, onTap) + end + pcall(function() + plate:Object { x = 20, y = 19, w = 50, h = 50, + bg_color = C_CIRC, border_width = 0, radius = 25 } + end) + if IMG[iconkey] then + pcall(function() plate:Image { src = IMG[iconkey], x = 30, y = 29 } end) + end + -- 标题框 h=54,字号 28 行高约 38 -> 垂直居中 y = 17 + 8 = 25(左对齐,不用 text_align) + lab(plate, { x = 80, y = 25, w = 208, h = 54, text = title, + text_color = C_WHITE, font_size = 28, text_font = F_ITEM }) + end + return plate +end + +local function bigButton(parent, text) + local b = nil + pcall(function() + b = parent:Object { x = 108, y = 365, w = 250, h = 80, pad_all = 0, + bg_color = C_PLATE, border_width = 0, radius = 40 } + end) + if b then setClickable(b) end + -- 按钮文字:框 (65,13,120,54) 相对底板;字号 28 行高约 38 -> y = 13 + 8 = 21 + -- "确认替换"/"重启手表" 量得 112px -> 居中 x = 65 + (120-112)/2 = 69 + lab(b, { x = 69, y = 21, w = 122, h = 54, text = text, + text_color = C_WHITE, font_size = 28, text_font = F_ITEM }) + return b +end + +local function clockTick() + local t = '--:--' + if os and type(os.date) == "function" then + local ok, s = pcall(os.date, "%H:%M") + if ok and type(s) == "string" then t = s end + end + for i = 1, #clocks do + pcall(function() clocks[i]:set { text = t } end) + end +end + +--------------------------------------------------------------------- 外壳 +local function slurp(p, n) + local f = io.open(p, "rb") + if not f then return nil end + local d = f:read(n or "*a") + f:close() + return d +end + +local function sh(cmd) + if type(os.execute) ~= "function" then return -1 end + local a, b, c = os.execute(cmd) + if type(a) == "number" then return a end + if a == true then return 0 end + if type(c) == "number" then return c end + if type(b) == "number" then return b end + return -1 +end + +--------------------------------------------------------------------- 预检查 +local desc1 = nil + +local function setDesc1(txt, color) + if not desc1 then return end + pcall(function() desc1:set { text = txt, text_color = color } end) +end + +local function doCheck() + if not RES then + setDesc1('找不到 resource.bin,无法操作。', C_ERR) + return false + end + if sh("dd if=" .. DEV .. " of=/tmp/os4_hdr.bin bs=1 count=48") ~= 0 then + setDesc1('读取分区失败(dd 不可用或无权限)。', C_ERR) + return false + end + local h = slurp("/tmp/os4_hdr.bin", 48) + if not h or #h < 48 then + setDesc1('读不到分区头。', C_ERR) + return false + end + if h:sub(1, 8) ~= "-rom1fs-" then + setDesc1('预检查失败:分区头不是 ROMFS。', C_ERR) + return false + end + local z = h:find("\0", 17, true) + local vol = h:sub(17, (z or 33) - 1) + if vol ~= "system" then + setDesc1('预检查失败:卷名是 ' .. vol .. '。', C_ERR) + return false + end + local size = string.unpack(" size then bad = bad + 1 end + total = total + r[3] + end + if bad > 0 then + setDesc1(string.format('预检查失败:%d 项越界或未对齐。', bad), C_ERR) + return false + end + -- 顺带确认 resource.bin 第一条能读出来 + local f = io.open(RES, "rb") + if f then + f:seek("set", list[1][1]) + local probe = f:read(list[1][3]) + f:close() + if not probe or #probe ~= list[1][3] then + setDesc1('预检查失败:resource.bin 读取异常。', C_ERR) + return false + end + end + setDesc1(string.format('预检查通过 | %d 条 | %.2fMB\n您可以返回主页开始修改。', + #list, total / 1048576), C_OK) + return true +end + +--------------------------------------------------------------------- 替换 +local logLabel = nil +local rebootBtn = nil +local logLines = {} + +local function logOut(s) + logLines[#logLines + 1] = s + while #logLines > 8 do table.remove(logLines, 1) end + if logLabel then + pcall(function() logLabel:set { text = table.concat(logLines, "\n") } end) + end +end + +local function showReboot() + if rebootBtn then pcall(function() rebootBtn:clear_flag(lvgl.FLAG.HIDDEN) end) end +end + +local function readChunk(off, len, path, bsz) + if os.remove then pcall(os.remove, path) end + sh(string.format("dd if=%s of=%s bs=%d skip=%d count=%d", + DEV, path, bsz, off // bsz, len // bsz)) + return slurp(path, len) +end + +-- 大块 + 尾块:偏移都是 512 对齐,RLE 素材的长度却常常不是 4/512 的倍数(4485 / 196005 …)。 +-- 老写法对这种长度只能 bs=1 -> 一条 19 万字节要 38 万次单字节读写,慢到偶发失败。 +-- 这里把它切成 ≤3 段:bs=512 的大块 + bs=4 的次块 + 最后 1~3 字节 bs=1,syscall 直接降两个数量级。 +local BIG = 512 + +local function segs(off, len) + local out = {} + if off % BIG == 0 then + local head = len - (len % BIG) + if head > 0 then out[#out + 1] = { BIG, 0, head } end + local rest = len - head + local t4 = rest - (rest % 4) + if t4 > 0 then out[#out + 1] = { 4, head, t4 } end + if rest - t4 > 0 then out[#out + 1] = { 1, head + t4, rest - t4 } end + else + out[1] = { 1, 0, len } + end + return out +end + +local function ddWrite(off, len, data) + for _, s in ipairs(segs(off, len)) do + local b, rel, n = s[1], s[2], s[3] + -- 每段单独一个临时文件:这样 dd 只需要 seek(输出偏移),不依赖输入侧的 skip + local o = io.open(TMPB, "wb") + if not o then return false end + o:write(data:sub(rel + 1, rel + n)) + o:close() + sh(string.format("dd if=%s of=%s bs=%d seek=%d count=%d", + TMPB, DEV, b, (off + rel) // b, n // b)) + end + return true +end + +local function ddRead(off, len) + local parts = {} + for _, s in ipairs(segs(off, len)) do + local b, rel, n = s[1], s[2], s[3] + if os.remove then pcall(os.remove, TMPR) end + sh(string.format("dd if=%s of=%s bs=%d skip=%d count=%d", + DEV, TMPR, b, (off + rel) // b, n // b)) + local d = slurp(TMPR, n) + if not d or #d ~= n then return nil end + parts[#parts + 1] = d + end + return table.concat(parts) +end + +-- 每条三步:① 版本校验(设备上那段必须等于「原厂」或「替换后」)② 写入 ③ 整段回读比对 +local function writeOne(rec, want) + local f = io.open(RES, "rb") + if not f then return 'err' end + f:seek("set", rec[1]) + local data = f:read(rec[3]) + f:close() + if not data or #data ~= rec[3] then return 'err' end + local mismatch = false + if want then + local n = math.min(64, rec[3], want[3]) + local wf = io.open(RES, "rb") + wf:seek("set", want[1]) + local wdata = wf:read(n) + wf:close() + local cur = readChunk(rec[2], n, TMPH, 1) + if not cur or #cur ~= n or not wdata or #wdata ~= n then return 'err' end + -- 设备上现在是「原厂」或「已改」都算正常;都不是(比如上次只写了一半)也**照样写**, + -- 只记一笔 —— 跳过去反而修不好那种半截状态。 + if cur ~= data:sub(1, n) and cur ~= wdata then mismatch = true end + end + local ok, lastBack = false, nil + for attempt = 1, 3 do + if not ddWrite(rec[2], rec[3], data) then return 'err' end + sh("sync") + lastBack = ddRead(rec[2], rec[3]) + if lastBack and #lastBack == rec[3] and lastBack == data then ok = true break end + logOut(string.format('> 第 %d 条校验不过,重试 %d/3', job.i, attempt)) + end + if not ok then + -- 计划 B:常规「分段写」3 次不过时,改成整条一次 dd(bs=4 或 bs=1)。 + -- 有些驱动对「同一区域被拆成多次小写」比「一次大写」更敏感,换个写法常常就过了。 + local b = (rec[3] % 4 == 0) and 4 or 1 + local o = io.open(TMPB, "wb") + if o then + o:write(data) + o:close() + sh(string.format("dd if=%s of=%s bs=%d seek=%d count=%d", + TMPB, DEV, b, rec[2] // b, rec[3] // b)) + sh("sync") + local back2 = ddRead(rec[2], rec[3]) + if back2 and #back2 == rec[3] and back2 == data then + ok = true + logOut(string.format('> 第 %d 条改用整条 bs=%d 写入后校验通过', job.i, b)) + end + end + end + if not ok then + -- 诊断:把「哪一条 / 目标偏移 / 长度 / 读回多少字节 / 第一个不同的字节」打出来 + local why = '读回 nil' + if lastBack then + if #lastBack ~= rec[3] then + why = string.format('读回 %d 字节', #lastBack) + else + why = '内容全同?' + for i = 1, #lastBack do + if lastBack:byte(i) ~= data:byte(i) then + why = string.format('首差@%d 得%02X 期%02X', i - 1, lastBack:byte(i), data:byte(i)) + break + end + end + end + end + logOut(string.format('> 失败 #%d off=0x%X len=%d %s', job.i, rec[2], rec[3], why)) + return 'err' + end + return mismatch and 'warn' or 'ok' +end + +local function startJob(variant) + if job.run then return end + local list = (variant == 'rollback') and PAYTBL.rb or PAYTBL.pay + if not list or #list == 0 then + setDesc1('负载表为空,已中止。', C_ERR) + return + end + job.run, job.list, job.i, job.errs, job.variant = true, list, 0, 0, variant + logLines = {} + logOut(variant == 'rollback' and '> 开始回滚' or '> 开始替换') + logOut('> 读取 resource.bin') + if rebootBtn then pcall(function() rebootBtn:add_flag(lvgl.FLAG.HIDDEN) end) end + show(pages.running) +end + +local function tick() + tickCount = tickCount + 1 + -- 兜底:按下超过 1.6s 没收到 click(手指滑出等)就恢复高亮 + if pressed and (tickCount - pressed.t) > 40 then pressRelease() end + if not job.run or not job.list then return end + job.i = job.i + 1 + local n = #job.list + if job.i > n then + job.run = false + local s = (job.variant == 'rollback') + and '> 回滚完毕,请重启手表。' + or '> 替换完毕,请重启手表。若发现问题,请回滚修改。' + logOut(s) + logOut(string.format('> 共 %d 条,失败 %d 条%s', n, job.errs, + (job.vers or 0) > 0 and string.format(',写入前状态异常 %d 条(已照样写入)', job.vers) or '')) + showReboot() + return + end + local rb = (job.variant == 'rollback') and PAYTBL.pay or PAYTBL.rb + local r = writeOne(job.list[job.i], rb and rb[job.i]) + if r == 'warn' then + job.vers = (job.vers or 0) + 1 + elseif r ~= 'ok' then + job.errs = job.errs + 1 + end + if job.i % 4 == 0 or job.i == n then + logOut(string.format('> 写入 %d/%d 失败 %d', job.i, n, job.errs)) + end +end + +--------------------------------------------------------------------- 页面 +local function buildHome() + local p = newPage() + if not p then return nil end + clockLabel(p) + pageTitle(p, ROLE == 'restore' and 'S4Chg还原' or 'S4Chg', 159, 158) + listItem(p, 85, 'ic_help', '快速帮助', function() show(pages.help) end) + listItem(p, 181, 'ic_replace', ROLE == 'restore' and '开始还原' or '开始替换', function() + pendingVariant = 'replace' + confirmFrom = 'home' + show(pages.confirm) + end) + listItem(p, 277, 'ic_about', '关于', function() show(pages.about) end) + return p +end + +local function buildHelp() + local p = newPage() + if not p then return nil end + clockLabel(p) + backArrow(p, 160, function() show(pages.home) end) + local t = pageTitle(p, '快速帮助', 183, 130) + setClickable(t) + bind(t, function() show(pages.home) end) + + listItem(p, 85, 'ic_check', '环境预检查', function() doCheck() end) + desc1 = lab(p, { x = 65, y = 176, w = 338, h = 72, + text = '检查原始资源状态、分区可写情况等。', + text_color = C_GRAY, font_size = 24, text_font = F_BODY }) + + -- 拆盘之后:本盘只装一份负载,「回滚/恢复」由另一份盘负责 + listItem(p, 256, 'ic_rollback', ROLE == 'restore' and '恢复修改' or '回滚修改', function() + if ROLE == 'restore' then + setDesc1('本盘是还原盘。要重新换回动画,请安装 S4Chg.face。', C_GRAY) + else + setDesc1('本盘只负责替换。要还原原厂动画,请安装 S4ChgRestore.face。', C_GRAY) + end + end) + lab(p, { x = 65, y = 344, w = 334, h = 72, + text = ROLE == 'restore' and '把充电动画写回原厂样式。' + or '本盘不含回滚数据(体积减半),还原请装 S4ChgRestore.face。', + text_color = C_GRAY, font_size = 24, text_font = F_BODY }) + return p +end + +local function buildConfirm() + local p = newPage() + if not p then return nil end + clockLabel(p) + -- 确认页的返回:回到进来时的那个页面(首页 或 快速帮助) + local function goBack() show(pages[confirmFrom] or pages.home) end + backArrow(p, 160, goBack) + local t = pageTitle(p, '开始替换', 183, 130) + setClickable(t) + bind(t, goBack) + + -- 多色文本**不能**用 recolor:AP 里那几处 recolor 是图片重着色(bg_img_recolor) + -- 和表盘编辑器的主题重着色,不是文本属性;实测标记会被原样显示出来。 + -- 改为拆成多个标签 + 手工算 x(宽度用真 MiSans 24px 量出)。 + -- 折行也显式写死:因为"替换图标包之前,请先确认自己"量得 336px,几乎等于 338px 框宽, + -- 且型号那行 500px 必然折行,只有写死才能让多色片段位置确定。 + lab(p, { x = 64, y = 90, w = 338, h = 70, + text = '替换充电动画之前,请先确认自己\n的系统版本号为 ', + text_color = C_GRAY, font_size = 24, text_font = F_BODY }) + lab(p, { x = 241, y = 123, w = 104, h = 33, text = '3.202.79', + text_color = C_WHITE, font_size = 24, text_font = F_BODY }) + lab(p, { x = 340, y = 123, w = 30, h = 33, text = '!', + text_color = C_GRAY, font_size = 24, text_font = F_BODY }) + lab(p, { x = 64, y = 189, w = 338, h = 33, text = '请确认自己的手表型号为', + text_color = C_GRAY, font_size = 24, text_font = F_BODY }) + lab(p, { x = 64, y = 222, w = 338, h = 70, + text = 'Xiaomi Watch S4 eSIM(非\n15 周年纪念版)', + text_color = C_WHITE, font_size = 24, text_font = F_BODY }) + lab(p, { x = 242, y = 255, w = 30, h = 33, text = '!', + text_color = C_GRAY, font_size = 24, text_font = F_BODY }) + lab(p, { x = 64, y = 321, w = 338, h = 33, text = '搞机有风险,变砖后果自负。', + text_color = C_GRAY, font_size = 24, text_font = F_BODY }) + + local b = bigButton(p, '确认替换') + bindTap(b, function() startJob(pendingVariant) end) + return p +end + +local function buildAbout() + local p = newPage() + if not p then return nil end + clockLabel(p) + backArrow(p, 191, function() show(pages.home) end) + local t = pageTitle(p, '关于', 214, 64) + setClickable(t) + bind(t, function() show(pages.home) end) + + -- 信息卡:底板 + 圆形 logo + 大标题 + 版本号 + pcall(function() + p:Object { x = 52, y = 79, w = 362, h = 204, pad_all = 0, + bg_color = C_PLATE, border_width = 0, radius = 30 } + end) + if IMG.logo then + pcall(function() p:Image { src = IMG.logo, x = 194, y = 108 } end) + end + -- "OS4 Icons" 28px 量得 138px -> 居中 x = 66 + (334-138)/2 = 164 + lab(p, { x = 164, y = 192, w = 138, h = 38, text = ROLE == 'restore' and 'S4Chg还原' or 'S4Chg', + text_color = C_WHITE, font_size = 28, text_font = F_ITEM }) + -- "1.0.0" 20px 量得 45px -> 居中 x = 66 + (334-45)/2 = 210 + lab(p, { x = 210, y = 227, w = 45, h = 27, text = '1.0.0', + text_color = C_GRAY, font_size = 20, text_font = F_VER }) + + -- QQ 联系方式:结构与列表项一致(无动作,所以不给它绑点击) + listItem(p, 291, 'ic_qq', '732681995', nil) + return p +end + +local function buildRunning() + local p = newPage() + if not p then return nil end + clockLabel(p) + -- 本页没有返回图标(规格:顶栏同首页),所以标题要屏幕居中: + -- "正在替换" 30px 量得 120px -> x = 233 - 120/2 = 173(不是帮助页的 183) + pageTitle(p, '正在替换', 173, 130) + logLabel = lab(p, { x = 64, y = 90, w = 338, h = 269, text = '', + text_color = C_WHITE, font_size = 24, text_font = F_BODY }) + rebootBtn = bigButton(p, '重启手表') + -- 替换过程中不显示,替换完再显示 + if rebootBtn then pcall(function() rebootBtn:add_flag(lvgl.FLAG.HIDDEN) end) end + bindTap(rebootBtn, function() + logOut('> 正在重启…') + sh("sync") + sh("reboot") + end) + return p +end + +--------------------------------------------------------------------- 组装 +local function build() + if built then return end + built = true + pages.home = buildHome() + pages.help = buildHelp() + pages.confirm = buildConfirm() + pages.about = buildAbout() + pages.running = buildRunning() + if pages.home then show(pages.home) end + clockTick() + pcall(function() + ui.timer = lvgl.Timer { period = 40, repeat_count = -1, cb = function() tick() end } + if ui.timer and ui.timer.resume then ui.timer:resume() end + end) + pcall(function() + ui.clockTimer = lvgl.Timer { period = 1000, repeat_count = -1, + cb = function() clockTick() end } + if ui.clockTimer and ui.clockTimer.resume then ui.clockTimer:resume() end + end) +end + +local okbuild, errbuild = pcall(build) +if not okbuild then + pcall(function() + local r = lvgl.Object(nil, { x = 0, y = 0, w = W, h = H, bg_color = 0x000000, + bg_opa = 255, border_width = 0 }) + r:Label { x = 20, y = 20, w = W - 40, h = H - 40, + text = 'BUILD FAILED:\n' .. tostring(errbuild), + text_color = '#ffffff', font_size = 13, text_font = F_BODY } + end) +end + +ui.init = function() + pcall(build) + return ui.root +end + +function pageOnPause() + if ui.timer then pcall(function() ui.timer:pause() end) end + if ui.clockTimer then pcall(function() ui.clockTimer:pause() end) end +end + +function pageOnResume() + if ui.timer then pcall(function() ui.timer:resume() end) end + if ui.clockTimer then pcall(function() ui.clockTimer:resume() end) end +end + +return ui diff --git a/s4e-charge/restore.sh b/s4e-charge/restore.sh new file mode 100644 index 0000000..4f52c7f --- /dev/null +++ b/s4e-charge/restore.sh @@ -0,0 +1,296 @@ +#!/bin/sh +# 把 S4 eSIM 的充电动画写回原厂(还原盘) + +DEV=/dev/system +BASE=/data/s4panim/payload_restore + +echo "target check" +ls -l $DEV + +echo "== charge0 == +dd if=$BASE/000_charge0.bin of=$DEV bs=4 seek=18062976 count=28182 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=18062976 count=28182 +cmp /tmp/chk.bin $BASE/000_charge0.bin + +echo "== charge1 == +dd if=$BASE/001_charge1.bin of=$DEV bs=1 seek=72172032 count=79834 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=72172032 count=79834 +cmp /tmp/chk.bin $BASE/001_charge1.bin + +echo "== charge2 == +dd if=$BASE/002_charge2.bin of=$DEV bs=4 seek=17786752 count=20313 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=17786752 count=20313 +cmp /tmp/chk.bin $BASE/002_charge2.bin + +echo "== charge3 == +dd if=$BASE/003_charge3.bin of=$DEV bs=1 seek=70018048 count=82007 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=70018048 count=82007 +cmp /tmp/chk.bin $BASE/003_charge3.bin + +echo "== charge4 == +dd if=$BASE/004_charge4.bin of=$DEV bs=1 seek=68788224 count=83347 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=68788224 count=83347 +cmp /tmp/chk.bin $BASE/004_charge4.bin + +echo "== charge5 == +dd if=$BASE/005_charge5.bin of=$DEV bs=1 seek=67532800 count=84305 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=67532800 count=84305 +cmp /tmp/chk.bin $BASE/005_charge5.bin + +echo "== charge6 == +dd if=$BASE/006_charge6.bin of=$DEV bs=1 seek=66615296 count=84281 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=66615296 count=84281 +cmp /tmp/chk.bin $BASE/006_charge6.bin + +echo "== charge7 == +dd if=$BASE/007_charge7.bin of=$DEV bs=4 seek=16632704 count=21023 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=16632704 count=21023 +cmp /tmp/chk.bin $BASE/007_charge7.bin + +echo "== charge8 == +dd if=$BASE/008_charge8.bin of=$DEV bs=1 seek=66445312 count=85403 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=66445312 count=85403 +cmp /tmp/chk.bin $BASE/008_charge8.bin + +echo "== charge9 == +dd if=$BASE/009_charge9.bin of=$DEV bs=1 seek=66357760 count=87097 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=66357760 count=87097 +cmp /tmp/chk.bin $BASE/009_charge9.bin + +echo "== charge10 == +dd if=$BASE/010_charge10.bin of=$DEV bs=4 seek=18020864 count=22109 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=18020864 count=22109 +cmp /tmp/chk.bin $BASE/010_charge10.bin + +echo "== charge11 == +dd if=$BASE/011_charge11.bin of=$DEV bs=1 seek=71994368 count=88566 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=71994368 count=88566 +cmp /tmp/chk.bin $BASE/011_charge11.bin + +echo "== charge12 == +dd if=$BASE/012_charge12.bin of=$DEV bs=1 seek=71904256 count=89914 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=71904256 count=89914 +cmp /tmp/chk.bin $BASE/012_charge12.bin + +echo "== charge13 == +dd if=$BASE/013_charge13.bin of=$DEV bs=1 seek=71813120 count=91103 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=71813120 count=91103 +cmp /tmp/chk.bin $BASE/013_charge13.bin + +echo "== charge14 == +dd if=$BASE/014_charge14.bin of=$DEV bs=4 seek=17929856 count=23345 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=17929856 count=23345 +cmp /tmp/chk.bin $BASE/014_charge14.bin + +echo "== charge15 == +dd if=$BASE/015_charge15.bin of=$DEV bs=4 seek=17906176 count=23632 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=17906176 count=23632 +cmp /tmp/chk.bin $BASE/015_charge15.bin + +echo "== charge16 == +dd if=$BASE/016_charge16.bin of=$DEV bs=1 seek=71527936 count=96705 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=71527936 count=96705 +cmp /tmp/chk.bin $BASE/016_charge16.bin + +echo "== charge17 == +dd if=$BASE/017_charge17.bin of=$DEV bs=1 seek=71429120 count=98295 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=71429120 count=98295 +cmp /tmp/chk.bin $BASE/017_charge17.bin + +echo "== charge18 == +dd if=$BASE/018_charge18.bin of=$DEV bs=1 seek=71329280 count=99525 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=71329280 count=99525 +cmp /tmp/chk.bin $BASE/018_charge18.bin + +echo "== charge19 == +dd if=$BASE/019_charge19.bin of=$DEV bs=4 seek=17807104 count=25201 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=17807104 count=25201 +cmp /tmp/chk.bin $BASE/019_charge19.bin + +echo "== charge20 == +dd if=$BASE/020_charge20.bin of=$DEV bs=1 seek=71045632 count=101014 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=71045632 count=101014 +cmp /tmp/chk.bin $BASE/020_charge20.bin + +echo "== charge21 == +dd if=$BASE/021_charge21.bin of=$DEV bs=1 seek=70943744 count=101755 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=70943744 count=101755 +cmp /tmp/chk.bin $BASE/021_charge21.bin + +echo "== charge22 == +dd if=$BASE/022_charge22.bin of=$DEV bs=4 seek=17710080 count=25758 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=17710080 count=25758 +cmp /tmp/chk.bin $BASE/022_charge22.bin + +echo "== charge23 == +dd if=$BASE/023_charge23.bin of=$DEV bs=1 seek=70736384 count=103826 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=70736384 count=103826 +cmp /tmp/chk.bin $BASE/023_charge23.bin + +echo "== charge24 == +dd if=$BASE/024_charge24.bin of=$DEV bs=1 seek=70631936 count=104085 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=70631936 count=104085 +cmp /tmp/chk.bin $BASE/024_charge24.bin + +echo "== charge25 == +dd if=$BASE/025_charge25.bin of=$DEV bs=4 seek=17632000 count=25960 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=17632000 count=25960 +cmp /tmp/chk.bin $BASE/025_charge25.bin + +echo "== charge26 == +dd if=$BASE/026_charge26.bin of=$DEV bs=4 seek=17605888 count=26104 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=17605888 count=26104 +cmp /tmp/chk.bin $BASE/026_charge26.bin + +echo "== charge27 == +dd if=$BASE/027_charge27.bin of=$DEV bs=1 seek=70319616 count=103815 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=70319616 count=103815 +cmp /tmp/chk.bin $BASE/027_charge27.bin + +echo "== charge28 == +dd if=$BASE/028_charge28.bin of=$DEV bs=4 seek=17553024 count=26848 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=17553024 count=26848 +cmp /tmp/chk.bin $BASE/028_charge28.bin + +echo "== charge29 == +dd if=$BASE/029_charge29.bin of=$DEV bs=1 seek=70100480 count=111441 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=70100480 count=111441 +cmp /tmp/chk.bin $BASE/029_charge29.bin + +echo "== charge30 == +dd if=$BASE/030_charge30.bin of=$DEV bs=1 seek=69905408 count=112118 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=69905408 count=112118 +cmp /tmp/chk.bin $BASE/030_charge30.bin + +echo "== charge31 == +dd if=$BASE/031_charge31.bin of=$DEV bs=1 seek=69794304 count=110857 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=69794304 count=110857 +cmp /tmp/chk.bin $BASE/031_charge31.bin + +echo "== charge32 == +dd if=$BASE/032_charge32.bin of=$DEV bs=1 seek=69680640 count=113259 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=69680640 count=113259 +cmp /tmp/chk.bin $BASE/032_charge32.bin + +echo "== charge33 == +dd if=$BASE/033_charge33.bin of=$DEV bs=1 seek=69566464 count=113647 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=69566464 count=113647 +cmp /tmp/chk.bin $BASE/033_charge33.bin + +echo "== charge34 == +dd if=$BASE/034_charge34.bin of=$DEV bs=1 seek=69451776 count=114281 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=69451776 count=114281 +cmp /tmp/chk.bin $BASE/034_charge34.bin + +echo "== charge35 == +dd if=$BASE/035_charge35.bin of=$DEV bs=1 seek=69336064 count=115315 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=69336064 count=115315 +cmp /tmp/chk.bin $BASE/035_charge35.bin + +echo "== charge36 == +dd if=$BASE/036_charge36.bin of=$DEV bs=4 seek=17304960 count=29039 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=17304960 count=29039 +cmp /tmp/chk.bin $BASE/036_charge36.bin + +echo "== charge37 == +dd if=$BASE/037_charge37.bin of=$DEV bs=1 seek=69103616 count=116126 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=69103616 count=116126 +cmp /tmp/chk.bin $BASE/037_charge37.bin + +echo "== charge38 == +dd if=$BASE/038_charge38.bin of=$DEV bs=1 seek=68987392 count=115971 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=68987392 count=115971 +cmp /tmp/chk.bin $BASE/038_charge38.bin + +echo "== charge39 == +dd if=$BASE/039_charge39.bin of=$DEV bs=1 seek=68871680 count=115563 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=68871680 count=115563 +cmp /tmp/chk.bin $BASE/039_charge39.bin + +echo "== charge40 == +dd if=$BASE/040_charge40.bin of=$DEV bs=1 seek=68672512 count=115334 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=68672512 count=115334 +cmp /tmp/chk.bin $BASE/040_charge40.bin + +echo "== charge41 == +dd if=$BASE/041_charge41.bin of=$DEV bs=1 seek=68556288 count=115723 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=68556288 count=115723 +cmp /tmp/chk.bin $BASE/041_charge41.bin + +echo "== charge42 == +dd if=$BASE/042_charge42.bin of=$DEV bs=1 seek=68440064 count=116050 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=68440064 count=116050 +cmp /tmp/chk.bin $BASE/042_charge42.bin + +echo "== charge43 == +dd if=$BASE/043_charge43.bin of=$DEV bs=1 seek=68323328 count=116683 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=68323328 count=116683 +cmp /tmp/chk.bin $BASE/043_charge43.bin + +echo "== charge44 == +dd if=$BASE/044_charge44.bin of=$DEV bs=1 seek=68206080 count=116854 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=68206080 count=116854 +cmp /tmp/chk.bin $BASE/044_charge44.bin + +echo "== charge45 == +dd if=$BASE/045_charge45.bin of=$DEV bs=1 seek=68088320 count=117369 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=68088320 count=117369 +cmp /tmp/chk.bin $BASE/045_charge45.bin + +echo "== charge46 == +dd if=$BASE/046_charge46.bin of=$DEV bs=1 seek=67970048 count=118191 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=67970048 count=118191 +cmp /tmp/chk.bin $BASE/046_charge46.bin + +echo "== charge47 == +dd if=$BASE/047_charge47.bin of=$DEV bs=1 seek=67852800 count=117005 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=67852800 count=117005 +cmp /tmp/chk.bin $BASE/047_charge47.bin + +echo "== charge48 == +dd if=$BASE/048_charge48.bin of=$DEV bs=1 seek=67735040 count=117365 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=67735040 count=117365 +cmp /tmp/chk.bin $BASE/048_charge48.bin + +echo "== charge49 == +dd if=$BASE/049_charge49.bin of=$DEV bs=1 seek=67617280 count=117626 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=67617280 count=117626 +cmp /tmp/chk.bin $BASE/049_charge49.bin + +echo "== charge50 == +dd if=$BASE/050_charge50.bin of=$DEV bs=1 seek=67414528 count=117906 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=67414528 count=117906 +cmp /tmp/chk.bin $BASE/050_charge50.bin + +echo "== charge51 == +dd if=$BASE/051_charge51.bin of=$DEV bs=1 seek=67295744 count=118481 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=67295744 count=118481 +cmp /tmp/chk.bin $BASE/051_charge51.bin + +echo "== charge52 == +dd if=$BASE/052_charge52.bin of=$DEV bs=4 seek=16793984 count=29825 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=16793984 count=29825 +cmp /tmp/chk.bin $BASE/052_charge52.bin + +echo "== charge53 == +dd if=$BASE/053_charge53.bin of=$DEV bs=1 seek=67056128 count=119353 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=67056128 count=119353 +cmp /tmp/chk.bin $BASE/053_charge53.bin + +echo "== charge54 == +dd if=$BASE/054_charge54.bin of=$DEV bs=1 seek=66936832 count=119135 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=66936832 count=119135 +cmp /tmp/chk.bin $BASE/054_charge54.bin + +echo "== charge55 == +dd if=$BASE/055_charge55.bin of=$DEV bs=1 seek=66818560 count=117881 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=66818560 count=117881 +cmp /tmp/chk.bin $BASE/055_charge55.bin + +echo "== charge56 == +dd if=$BASE/056_charge56.bin of=$DEV bs=4 seek=16674944 count=29644 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=16674944 count=29644 +cmp /tmp/chk.bin $BASE/056_charge56.bin + +rm /tmp/chk.bin +echo "restore done - run reboot to apply" diff --git a/s4e-charge/tools/build_faces.py b/s4e-charge/tools/build_faces.py new file mode 100644 index 0000000..8d11496 --- /dev/null +++ b/s4e-charge/tools/build_faces.py @@ -0,0 +1,81 @@ +"""overlay + 官方模板 -> Compiler.exe -> .face(两份盘共用同一个 watchface id) + +要点(都是踩过的坑): + * preview.png 必须是 326x326,Compiler.exe 会直接拒绝别的尺寸 + * watchfaceId 要带机型家族前缀:S4 家族 = 362150xxx + * 用 Python 的 copytree 拷目录,别用 PowerShell 的 Copy-Item -Recurse(会拍平) + * replace 盘与 restore 盘用**同一个 id**:装一份就顶掉另一份,设备上只占一份空间 +""" +import os, json, shutil, subprocess, sys +from paths import WORK, TEMPLATE + +TPL = TEMPLATE +OVER = os.path.join(WORK, 'overlay') +BUILD = os.path.join(WORK, 'build') + +TARGETS = {'S4Chg': ('362150103', 'S5 41mm 充电动画 -> S4 eSIM(只写负载)'), + 'S4ChgRestore': ('362150103', 'S4 eSIM 充电动画还原盘(只写原厂字节)')} + + +def prepare(name): + oid, _ = TARGETS[name] + ov = os.path.join(OVER, name) + dst = os.path.join(BUILD, name) + if not os.path.exists(TPL): + raise SystemExit('找不到官方模板:%s\n请把 FangAiden/LuaDevTemplate 放到 vendor/ 下(见 README)' % TPL) + if os.path.exists(dst): + shutil.rmtree(dst) + shutil.copytree(TPL, dst) + + old = os.path.join(dst, 'watchface', 'fprj', 'LuaDevTemplate.fprj') + if os.path.exists(old): + os.remove(old) + + cfg = json.load(open(os.path.join(ov, 'watchface.config.json'), encoding='utf-8')) + cfg['watchfaceId'] = oid + json.dump(cfg, open(os.path.join(dst, 'watchface.config.json'), 'w', encoding='utf-8'), + indent=4, ensure_ascii=False) + + shutil.copyfile(os.path.join(ov, 'watchface', 'fprj', name + '.fprj'), + os.path.join(dst, 'watchface', 'fprj', name + '.fprj')) + shutil.copyfile(os.path.join(ov, 'watchface', 'fprj', 'images', 'preview.png'), + os.path.join(dst, 'watchface', 'fprj', 'images', 'preview.png')) + shutil.copyfile(os.path.join(ov, 'watchface', 'fprj', 'app', 'lua', 'main.lua'), + os.path.join(dst, 'watchface', 'fprj', 'app', 'lua', 'main.lua')) + + img = os.path.join(dst, 'watchface', 'fprj', 'app', 'images') + os.makedirs(img, exist_ok=True) + ovimg = os.path.join(ov, 'watchface', 'fprj', 'app', 'images') + for d in sorted(x for x in os.listdir(ovimg) if os.path.isdir(os.path.join(ovimg, x))): + tgt = os.path.join(img, d) + if os.path.exists(tgt): + shutil.rmtree(tgt) + shutil.copytree(os.path.join(ovimg, d), tgt) + print(' %s: app/images/%s/ -> %d files' % (name, d, len(os.listdir(tgt)))) + return dst + + +def build(dst): + ps = os.path.join(dst, 'scripts', 'build_face.ps1') + r = subprocess.run(['powershell', '-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', ps], + capture_output=True, text=True, errors='replace') + for l in [x for x in (r.stdout or '').splitlines() if x.strip()][-6:]: + print(' |', l) + if r.returncode != 0: + print(' !! build failed, rc=', r.returncode) + return r.returncode == 0 + + +if __name__ == '__main__': + only = sys.argv[1] if len(sys.argv) > 1 else None + for name in TARGETS: + if only and name != only: + continue + print('== %s (id=%s) ==' % (name, TARGETS[name][0])) + dst = prepare(name) + build(dst) + face = os.path.join(dst, 'bin', name + '.face') + if os.path.exists(face): + print(' OK %s %d bytes' % (face, os.path.getsize(face))) + else: + print(' missing output') diff --git a/s4e-charge/tools/check_clip.py b/s4e-charge/tools/check_clip.py new file mode 100644 index 0000000..fc8ad4c --- /dev/null +++ b/s4e-charge/tools/check_clip.py @@ -0,0 +1,58 @@ +"""用真实渲染结果判"会不会被圆屏切掉"。 + +做法:渲染一张**不套圆形遮罩**的 RGBA,然后量所有非透明像素到圆心的距离。 +这样自动考虑字体实际字宽、折行、圆角、图片缩放 —— 比按元素外框估算准得多。 +""" +import os, sys, math +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from PIL import Image, ImageDraw +from render_ui import draw_label, W, H +from pages import PAGES + +CX = CY = 233.0 +R = 233.0 + + +def render_raw(items): + img = Image.new('RGBA', (W, None) if False else (W, H), (0, 0, 0, 0)) + d = ImageDraw.Draw(img) + from render_ui import _rgb, font + for it in items: + k = it.get('kind') + if k == 'rect': + box = [it['x'], it['y'], it['x'] + it['w'] - 1, it['y'] + it['h'] - 1] + r = it.get('radius', 0) + if r: + d.rounded_rectangle(box, radius=r, fill=_rgb(it['bg']) + (255,)) + else: + d.rectangle(box, fill=_rgb(it['bg']) + (255,)) + elif k == 'circle': + d.ellipse([it['x'], it['y'], it['x'] + it['w'] - 1, it['y'] + it['h'] - 1], + fill=_rgb(it['bg']) + (255,)) + elif k == 'image': + if os.path.exists(it['src']): + im = Image.open(it['src']).convert('RGBA').resize((it['w'], it['h']), Image.LANCZOS) + img.alpha_composite(im, (it['x'], it['y'])) + elif k == 'label': + draw_label(img, d, it) + return img + + +print(f'{"page":<10} {"max r":>7} {"outside px":>11} worst point / note') +print('-' * 78) +for name, items in PAGES.items(): + im = render_raw(items) + px = im.load() + worst, wpt, outside = 0.0, None, 0 + for y in range(H): + for x in range(W): + if px[x, y][3] > 8: + d = math.hypot(x - CX, y - CY) + if d > worst: + worst, wpt = d, (x, y) + if d > R: + outside += 1 + note = '全部在圆内' if worst <= R else f'会被切 {outside} px' + print(f'{name:<10} {worst:>7.1f} {outside:>11} {wpt} {note}') +print('-' * 78) +print(f'圆心 ({CX:.0f},{CY:.0f}) 半径 {R:.0f} 画布 {W}x{H}') diff --git a/s4e-charge/tools/finalize_faces.py b/s4e-charge/tools/finalize_faces.py new file mode 100644 index 0000000..566cded --- /dev/null +++ b/s4e-charge/tools/finalize_faces.py @@ -0,0 +1,72 @@ +"""产出最终 .face(两份盘各一份,都不带工具链变体)。 + + * Compiler.exe 写 byte5 = 0x00 + 自己的随机 id 到 40..49 + * 官方 set_face_id.ps1 会把 byte5 改成 10 并写 id;固件里 25 个真表盘 byte5 ∈ {0,1} + => 忠实产物 = Compiler.exe 原始输出,只替换 40..49(b5=0 那份) + 体积原因:本盘各 ~16 MB,不再额外复制工具链变体(要的话去掉下面的 SKIP_TOOLCHAIN 即可) +""" +import os, sys, shutil, subprocess, hashlib +from paths import WORK, DIST + +BUILD = os.path.join(WORK, 'build') +TARGETS = {'S4Chg': '362150103', 'S4ChgRestore': '362150103'} +SKIP_TOOLCHAIN = True + + +def compile_raw(name): + w = os.path.join(BUILD, name) + exe = os.path.join(w, 'watchface', 'tools', 'Compiler.exe') + if not os.path.exists(exe): + print(' %s: 跳过(没有构建树,先跑 build_faces.py %s)' % (name, name)) + return None + fprj = os.path.join(w, 'watchface', 'fprj', name + '.fprj') + out = os.path.join(WORK, 'raw_' + name) + if os.path.exists(out): + shutil.rmtree(out) + os.makedirs(out) + subprocess.run([exe, '-b', fprj, out, name + '.face', TARGETS[name]], + capture_output=True, text=True, errors='replace') + p = os.path.join(out, name + '.face') + return p if os.path.exists(p) else None + + +def patch_id(src, dst, wid): + d = bytearray(open(src, 'rb').read()) + if d[0:4] != b'\x5a\xa5\x34\x12': + raise SystemExit('bad magic') + if len(wid) > 10: + raise SystemExit('id too long') + d[40:50] = wid.encode() + b'\x00' * (10 - len(wid)) + open(dst, 'wb').write(bytes(d)) + return dst + + +def main(): + os.makedirs(DIST, exist_ok=True) + only = sys.argv[1] if len(sys.argv) > 1 else None + report = [] + for name, wid in TARGETS.items(): + if only and only != name: + continue + raw = compile_raw(name) + if not raw: + continue + faithful = patch_id(raw, os.path.join(DIST, name + '.face'), wid) + if not SKIP_TOOLCHAIN: + tc_src = os.path.join(BUILD, name, 'bin', name + '.face') + tc = os.path.join(DIST, name + '_toolchain.face') + if os.path.exists(tc_src): + shutil.copyfile(tc_src, tc) + for p in (faithful,): + if os.path.exists(p): + d = open(p, 'rb').read() + report.append((os.path.basename(p), len(d), d[4], d[5], d[40:50], + hashlib.sha256(d).hexdigest()[:16])) + print('%-28s%11s b4 b5 id sha256[:16]' % ('file', 'bytes')) + for f, n, b4, b5, sid, h in report: + print('%-28s%11d %-3d %-3d %-18r %s' % (f, n, b4, b5, sid, h)) + print('->', DIST) + + +if __name__ == '__main__': + main() diff --git a/s4e-charge/tools/gen_ui_lua.py b/s4e-charge/tools/gen_ui_lua.py new file mode 100644 index 0000000..7648291 --- /dev/null +++ b/s4e-charge/tools/gen_ui_lua.py @@ -0,0 +1,127 @@ +"""规范化构建:表盘源码 + 资源 -> .face(两遍,自动回填 resource.bin 内的偏移) + + main.lua <- **源码**,含 __UIIMG__ / __PAY__ / __RB__ / __ROLE__ 占位符 + .work/overlay/ <- 中间产物(回填后的 Lua 落在这里) + .work/build/ <- 编译工作区 + dist/.face <- 交付物 + +两份盘(replace / restore)共用同一套流程,只是负载来源不同: + blob_replace/{payload,fp}.bin -> S4Chg.face + blob_restore/{payload,fp}.bin -> S4ChgRestore.face +rb 表里放的是**另一侧字节前 64 B 的指纹**(只给版本校验用),所以一个 .face 只装一份负载。 +""" +import os, re, sys, json, subprocess +from paths import PROJ, WORK + +ROLE = (sys.argv[1] if len(sys.argv) > 1 else 'replace').lower() +NAME = 'S4Chg' if ROLE == 'replace' else 'S4ChgRestore' +RECDIR = 'payload' if ROLE == 'replace' else 'payload_restore' +TOOLS = os.path.dirname(os.path.abspath(__file__)) +SRC = os.path.join(PROJ, 'main.lua') +OVERLAY_APP = os.path.join(WORK, 'overlay', NAME, 'watchface', 'fprj', 'app') +RESOLVED = os.path.join(OVERLAY_APP, 'lua', 'main.lua') +FACE = os.path.join(WORK, 'build', NAME, 'bin', NAME + '.face') +PY = sys.executable + +UINAMES = ['back', 'ic_help', 'ic_replace', 'ic_about', 'ic_check', 'ic_rollback', + 'ic_qq', 'logo'] + + +def run(script, *args): + r = subprocess.run([PY, os.path.join(TOOLS, script)] + list(args), + capture_output=True, text=True, encoding='utf-8', errors='replace') + if r.returncode != 0: + print(r.stdout[-2000:]); print(r.stderr[-2000:]) + raise SystemExit(f'{script} 执行失败') + return r.stdout + + +def find_blob(face, blob): + hits, start = [], 0 + while True: + i = face.find(blob, start) + if i < 0: + break + hits.append(i) + start = i + 1 + if len(hits) > 4: + break + return hits + + +def main(): + tpl = open(SRC, encoding='utf-8').read() + if '__UIIMG__' not in tpl: + raise SystemExit('main.lua 里没有占位符 —— 它应该是源码模板,不要把回填后的版本存回去') + print('=== %s(role=%s)===' % (NAME, ROLE)) + blobs = json.load(open(os.path.join(PROJ, 'blob_' + ROLE, 'offsets.json'), encoding='utf-8')) + pay_blob = open(os.path.join(PROJ, 'blob_' + ROLE, 'payload.bin'), 'rb').read() + fp_blob = open(os.path.join(PROJ, 'blob_' + ROLE, 'fp.bin'), 'rb').read() + ml, mlf = blobs['magic_len'], blobs['magic_fp_len'] + + print('第 1 遍:用占位符编一次,量出资源在 resource.bin 里的位置') + run('make_overlays.py', ROLE) + run('build_faces.py', NAME) + face = open(FACE, 'rb').read() + print(' face %d bytes' % len(face)) + + ui_rows, bad = [], 0 + for name in UINAMES: + blob = open(os.path.join(PROJ, 'ui_images', name + '.bin'), 'rb').read() + hits = find_blob(face, blob) + if len(hits) != 1: + print(' !! %s 命中 %d 次(应为 1)' % (name, len(hits))); bad += 1 + if hits: + ui_rows.append((hits[0], len(blob), name)) + if bad: + raise SystemExit('有资源无法唯一定位') + pay_base = face.find(pay_blob[:15]) + fp_base = face.find(fp_blob[:15]) + for tag, blob, base in (('payload', pay_blob, pay_base), ('fp', fp_blob, fp_base)): + if face.count(blob[:15]) != 1: + raise SystemExit('%s 魔数命中 %d 次(应为 1)' % (tag, face.count(blob[:15]))) + if face.find(blob) != base: + raise SystemExit('%s 不是原样连续存放' % tag) + pay = [(pay_base + ml + r['rel'], r['target'], r['length']) for r in blobs['records']] + rb = [(fp_base + mlf + r['fprel'], r['target'], r['fplen']) for r in blobs['records']] + print(' 单条目:payload @0x%x(%d B)/ fp @0x%x(%d B),共 %d 条记录' + % (pay_base, len(pay_blob), fp_base, len(fp_blob), len(pay))) + + ui_txt = '\n'.join(' {%d,%d,"%s"},' % (o, l, n) for o, l, n in ui_rows) + pay_txt = ' pay = {\n' + '\n'.join(' {%d,%d,%d},' % t for t in pay) + '\n },' + rb_txt = ' rb = {\n' + '\n'.join(' {%d,%d,%d},' % t for t in rb) + '\n },' + resolved = (tpl.replace('__UIIMG__', ui_txt).replace('__PAY__', pay_txt) + .replace('__RB__', rb_txt).replace('__ROLE__', ROLE)) + assert '__UIIMG__' not in resolved and '__ROLE__' not in resolved + os.makedirs(os.path.dirname(RESOLVED), exist_ok=True) + open(RESOLVED, 'w', encoding='utf-8', newline='\n').write(resolved) + print('第 2 遍:回填后的 Lua(%d bytes),重新编译' % len(resolved)) + run('build_faces.py', NAME) + print(run('finalize_faces.py', NAME).strip()) + + face2 = open(FACE, 'rb').read() + drift = 0 + for name in UINAMES: + blob = open(os.path.join(PROJ, 'ui_images', name + '.bin'), 'rb').read() + hits = find_blob(face2, blob) + row = [r for r in ui_rows if r[2] == name] + if not row or not hits or hits[0] != row[0][0]: + print(' !! 偏移漂移 %s' % name); drift += 1 + if face2.find(pay_blob) != pay_base or face2.find(fp_blob) != fp_base: + print(' !! 负载偏移漂移'); drift += 1 + files = [(f, open(os.path.join(PROJ, RECDIR, f), 'rb').read()) + for f in sorted(os.listdir(os.path.join(PROJ, RECDIR))) if f.endswith('.bin')] + ok = sum(1 for off, tgt, ln in pay + if any(len(x) == ln and face2[off:off + ln] == x for _, x in files)) + if ok != len(pay): + print(' !! pay 只有 %d/%d 条吻合' % (ok, len(pay))); drift += 1 + okf = sum(1 for (off, tgt, ln), r in zip(rb, blobs['records']) + if face2[off:off + ln] == fp_blob[mlf + r['fprel']:mlf + r['fprel'] + ln]) + if okf != len(rb): + print(' !! fp 只有 %d/%d 条吻合' % (okf, len(rb))); drift += 1 + print('偏移校验:%s' % ('全部吻合,0 漂移' if drift == 0 else '%d 处异常' % drift)) + print('交付物:', os.path.join(PROJ, 'dist', NAME + '.face')) + + +if __name__ == '__main__': + main() diff --git a/s4e-charge/tools/iconpack.py b/s4e-charge/tools/iconpack.py new file mode 100644 index 0000000..68c8672 --- /dev/null +++ b/s4e-charge/tools/iconpack.py @@ -0,0 +1,447 @@ +"""Build / apply a system-app launcher-icon replacement for a Vela `vela_system.bin`. + +Why "same length" is the whole ball game +---------------------------------------- +ROMFS packs superblock -> inode -> name -> file data strictly back to back, and +every `next` / `spec` pointer is an ABSOLUTE offset. If a replacement file +changes length by even one byte, every later offset shifts and the image is +corrupt. So a replacement must encode to EXACTLY the original byte length: + + * source PNG == original dimensions -> encode, lengths match, done. + * source PNG != original dimensions -> two options: + (default) resize to the original dimensions (icon renders identically) + --pad keep the source dimensions, zero-pad the container to the + original length (renders smaller unless the launcher scales) + +Usage: + iconpack.py list + iconpack.py make [--pad] [--patched] + iconpack.py apply +""" +import os, sys, json, struct, hashlib, shutil +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from romfs import Romfs, img_header +from lvgl_bin import rle_compress, pad_rle +from PIL import Image + +CF_ARGB8888, CF_I8, MAGIC = 0x10, 0x0a, 0x19 +FMT = {CF_ARGB8888: 'ARGB8888', CF_I8: 'I8'} +RESAMPLE = Image.LANCZOS + + +# ----------------------------------------------------------------- encoders +def encode_argb8888(img): + rgba = img.convert('RGBA') + w, h = rgba.size + px = rgba.load() + out = bytearray() + for y in range(h): + row = bytearray() + for x in range(w): + r, g, b, a = px[x, y] + row += bytes((b, g, r, a)) + out += row + return bytes(out) + + +def encode_i8(img, alpha_levels=16, sharpen_up=0.0): + """I8 encoder with an ALPHA-AWARE palette. + + The naive version quantised RGB only and then stored one *averaged* alpha + per palette entry. Any colour shared by an opaque body pixel and a + semi-transparent edge pixel collapsed to a mid alpha, and fully transparent + pixels inherited that same entry -- so the transparent margin became faintly + visible (measured: perpetual_calendar haze 0.000 -> 1.000, i.e. a grey veil + around the whole icon). + + Here the palette is built over (quantised RGB, quantised alpha) PAIRS, so a + colour can own several entries at different alphas and alpha 0 stays alpha 0. + """ + rgba = img.convert('RGBA') + w, h = rgba.size + src = rgba.load() + + # average colour of the visible pixels: transparent pixels must not invent + # their own colour clusters + rs = gs = bs = n = 0 + for y in range(h): + for x in range(w): + r, g, b, a = src[x, y] + if a > 0: + rs += r; gs += g; bs += b; n += 1 + avg = (rs // n, gs // n, bs // n) if n else (0, 0, 0) + + step = max(1, 256 // max(1, alpha_levels)) + flat = Image.new('RGB', (w, h)) + fp = flat.load() + qa = [[0] * w for _ in range(h)] + for y in range(h): + for x in range(w): + r, g, b, a = src[x, y] + fp[x, y] = (r, g, b) if a > 0 else avg + q = int(round(a / float(step))) * step + qa[y][x] = 255 if q > 255 else q + + p = flat.quantize(colors=256, method=Image.MEDIANCUT) + pal_rgb = p.getpalette()[:768] + idx = p.tobytes() + + cnt = {} + for y in range(h): + row = y * w + for x in range(w): + k = (idx[row + x], qa[y][x]) + cnt[k] = cnt.get(k, 0) + 1 + keep = sorted(cnt.items(), key=lambda kv: -kv[1])[:256] + + pal = bytearray() + remap = {} + by_rgb = {} + for slot, ((pi, al), _c) in enumerate(keep): + r = pal_rgb[pi * 3] if pi * 3 + 2 < len(pal_rgb) else 0 + g = pal_rgb[pi * 3 + 1] if pi * 3 + 2 < len(pal_rgb) else 0 + b = pal_rgb[pi * 3 + 2] if pi * 3 + 2 < len(pal_rgb) else 0 + pal += bytes((b, g, r, al)) + remap[(pi, al)] = slot + by_rgb.setdefault(pi, []).append((al, slot)) + removed = 256 - len(keep) + if removed > 0: + pal += b'\x00\x00\x00\x00' * removed + for pi in by_rgb: + by_rgb[pi].sort() + + out = bytearray(w * h) + for y in range(h): + row = y * w + for x in range(w): + pi = idx[row + x] + al = qa[y][x] + s = remap.get((pi, al)) + if s is None: + cand = by_rgb.get(pi) + if cand: + s = min(cand, key=lambda t: abs(t[0] - al))[1] + else: + s = 0 + out[row + x] = s + return bytes(pal) + bytes(out) + + +def encode_i8_palette(img, pal_bgra, alpha_tol=12): + """Quantise to an EXISTING 256-entry BGRA palette (as stored in an I8 blob). + + Reusing the source icon's own palette preserves the artwork's gradient shades + and its anti-aliased edge levels. A freshly clustered palette cannot: the + plate gradient eats the 256 entries, the edge collapses to a few alpha steps + and dropped pixels land on wrong entries -- measured on remote_camera as a + jagged plate rim plus stray coloured speckles. + + The search is constrained by alpha so that + * fully transparent source pixels can only pick transparent entries + (no veil, no speckles), and + * fully opaque source pixels can only pick opaque entries + (the body never goes translucent). + """ + rgba = img.convert('RGBA') + w, h = rgba.size + px = rgba.load() + + ent = [(pal_bgra[i * 4 + 2], pal_bgra[i * 4 + 1], + pal_bgra[i * 4], pal_bgra[i * 4 + 3]) for i in range(256)] + clear = [i for i, e in enumerate(ent) if e[3] == 0] + if not clear: # guarantee a transparent entry exists + worst = min(range(256), key=lambda i: ent[i][3]) + ent[worst] = (0, 0, 0, 0) + clear = [worst] + solid = [i for i, e in enumerate(ent) if e[3] >= 248] + by_alpha = {} + for i, e in enumerate(ent): + by_alpha.setdefault(e[3], []).append(i) + + def premul(e): + r, g, b, a = e + return (r * a // 255, g * a // 255, b * a // 255, a) + + P = [premul(e) for e in ent] + cache = {} + out = bytearray(w * h) + for y in range(h): + row = y * w + for x in range(w): + r, g, b, a = px[x, y] + key = (r >> 2, g >> 2, b >> 2, a >> 3) + s = cache.get(key) + if s is None: + if a == 0: + cand = clear + elif a >= 248 and solid: + cand = solid + else: + cand = [] + for da in range(0, alpha_tol + 1): + cand += by_alpha.get(a + da, ()) if a + da <= 255 else () + if da: + cand += by_alpha.get(a - da, ()) if a - da >= 0 else () + if not cand: + cand = range(256) + pr, pg, pb, pa = premul((r, g, b, a)) + best, bd = cand[0], None + for i in cand: + q = P[i] + d = ((pr - q[0]) ** 2 + (pg - q[1]) ** 2 + (pb - q[2]) ** 2 + + 2 * (pa - q[3]) ** 2) + if bd is None or d < bd: + bd, best = d, i + s = best + cache[key] = s + out[row + x] = s + + pal = bytearray() + for (r, g, b, a) in ent: + pal += bytes((b, g, r, a)) + return bytes(pal) + bytes(out) + + +def encode(img, cf, w, h, flags=0, pad_to=None, palette=None): + if cf == CF_ARGB8888: + body = encode_argb8888(img) + elif palette is not None: + body = encode_i8_palette(img, palette) + else: + body = encode_i8(img) + stride = w * (4 if cf == CF_ARGB8888 else 1) + blob = struct.pack(' pad_to: + raise ValueError(f'encoded {len(blob)} > slot {pad_to}') + blob += b'\x00' * (pad_to - len(blob)) + return blob + + +# ----------------------------------------------------------------- helpers +def raw_i8_strided(img, stride, colors=256): + """I8 像素流:1024 B 调色板(BGRA)+ stride*h 个索引(行尾按 header 的 stride 补 0)。 + + RLE 槽位专用:用 FASTOCTREE 自适应减色 + 关闭抖动 —— 颜色越少、相邻像素越容易出现 + 长游程,RLE 流就越短(这是能不能塞进原 csize 的关键)。 + """ + q = img.convert('RGBA').quantize(colors=max(2, min(256, colors)), + method=Image.FASTOCTREE, dither=Image.NONE) + if q.palette.mode == 'RGBA': + pal = list(q.getpalette('RGBA')) + else: + rgb = q.getpalette('RGB') + pal = [v for i in range(256) for v in (rgb[i * 3], rgb[i * 3 + 1], rgb[i * 3 + 2], 255)] + pal = (pal + [0] * 1024)[:1024] + pal_b = bytearray() + for i in range(256): # 存储序:B, G, R, A + r, g, b, a = pal[i * 4:i * 4 + 4] + pal_b += bytes((b, g, r, a)) + idx = q.tobytes() + w, h = img.size + rows = bytearray() + for y in range(h): + rows += idx[y * w:(y + 1) * w] + if stride > w: + rows += b'\x00' * (stride - w) + return bytes(pal_b) + bytes(rows) + + +# 从大到小试:优先保真,装不下再减色 +RLE_COLOR_STEPS = (256, 192, 160, 128, 96, 80, 64, 48, 40, 32, 24, 16, 12, 8, 6, 4) + + +def encode_into_slot(img, stock_blob, slot_len=None, colors=None): + """把 img 编成与 stock_blob **逐字节等长** 的 LVGL v9 blob(沿用原 header)。 + + 真实固件里这些 UI 素材有两种槽位形态,都必须支持,否则 ROMFS 后面所有偏移全平移: + * ARGB8888 未压缩(144x144 图标那种):像素流 + 零填充到原长度 + * I8 + RLE 压缩(walkie_talkie 里几乎全是):量化 -> RLE -> 用 ctrl=0 空指令补齐到原 csize + (颜色数从 256 逐级往下试,取第一个能塞进原 csize 的) + 返回 (blob, how);不支持 / 压缩后仍超长时返回 (None, 原因)。 + """ + h = img_header(stock_blob) + if h is None: + return None, '不是 LVGL bin' + w, ht, cf, flags = h['w'], h['h'], h['cf'], h['flags'] + if slot_len is None: + slot_len = len(stock_blob) + out_img = img.convert('RGBA') + if out_img.size != (w, ht): + # 约定:**长宽比不一致的素材直接不替换**(保持原厂图),而不是拉伸或裁切 —— + # 拉伸会把方块变长方形;裁切会切掉按钮两端。宁可保持原厂,也不给设备塞变形图。 + pw, ph = out_img.size + if abs(pw / ph - w / ht) / (w / ht) > 0.005: + return None, ('长宽比不一致(源 %dx%d = %.3f,槽位 %dx%d = %.3f)-> 按约定跳过,保持原厂' + % (pw, ph, pw / ph, w, ht, w / ht)) + out_img = out_img.resize((w, ht), RESAMPLE) + if cf == CF_ARGB8888 and not (flags & 0x08): + blob = stock_blob[:12] + encode_argb8888(out_img) + if len(blob) > slot_len: + return None, 'ARGB 编码 %d > 槽位 %d' % (len(blob), slot_len) + return blob + b'\x00' * (slot_len - len(blob)), 'ARGB(补 %d)' % (slot_len - len(blob)) + if cf == CF_I8 and (flags & 0x08): + method, csize, usize = struct.unpack(' csize %d(已减到 %d 色)' % (len(comp), csize, n_last) + n, raw, comp = best + comp = comp + bytes((1, raw[-1])) # 把流做到正好 usize 字节,和原厂一致 + padded = pad_rle(comp, csize) + if padded is None: + return None, '补齐失败' + blob = stock_blob[:12] + struct.pack(' inode for every launcher icon. + + alarm/launcher.bin -> alarm + sports/training/launcher.bin -> sports_training + phone/contacts_launcher.bin -> phone_contacts + """ + out = {} + for p, i in r.entries(): + if not p.rsplit('/', 1)[-1].endswith('launcher.bin'): + continue + stem = p[:-4] # drop '.bin' + if stem.endswith('_launcher'): + stem = stem[:-len('_launcher')] + elif stem.endswith('/launcher'): + stem = stem[:-len('/launcher')] + out[stem.replace('/', '_')] = i + return out + + +def describe(r, i): + h = img_header(r.read(i)) + if not h: + return '?', '?', '?' + return f'{h["w"]}x{h["h"]}', FMT.get(h['cf'], f'0x{h["cf"]:02x}'), h + + +def cmd_list(image): + r = Romfs(image) + print(f'{os.path.basename(image)} volume={r.volume} romfs_size={r.sb_size}') + for app, i in sorted(targets(r).items()): + dim, fmt, _ = describe(r, i) + print(f' {app:<26} data@0x{i.data_off:08x} len={i.size:<7} {dim} {fmt}') + + +def cmd_make(image, pngdir, outdir, pad=False, write_patched=False): + r = Romfs(image) + tg = targets(r) + os.makedirs(outdir, exist_ok=True) + payload_dir = os.path.join(outdir, 'payload') + os.makedirs(payload_dir, exist_ok=True) + patch = bytearray() + rollback = bytearray() + records, skipped = [], [] + for fn in sorted(os.listdir(pngdir)): + if not fn.lower().endswith('.png'): + continue + app = fn[:-4] + if app not in tg: + skipped.append((app, 'no such app')) + continue + ino = tg[app] + orig = r.read(ino) + _, fmt, h = describe(r, ino) + if h is None: + skipped.append((app, f'not LVGL image (len={ino.size})')) + continue + src = Image.open(os.path.join(pngdir, fn)).convert('RGBA') + src_dim = f'{src.size[0]}x{src.size[1]}' + resized = False + if src.size != (h['w'], h['h']): + if pad: + tw, th = src.size + blob = encode(src, h['cf'], tw, th, h['flags'], pad_to=len(orig)) + else: + src = src.resize((h['w'], h['h']), RESAMPLE) + tw, th = src.size + resized = True + blob = encode(src, h['cf'], tw, th, h['flags']) + else: + tw, th = src.size + blob = encode(src, h['cf'], tw, th, h['flags']) + if len(blob) != len(orig): + skipped.append((app, f'length {len(blob)} != {len(orig)}')) + continue + patch += struct.pack('12}{"bytes":>8} {"fmt":<10}{"stock":>9}{"source":>9} note') + for m in records: + note = 'resized' if m['resized'] else ('padded' if m['padded'] else '') + print(f'{m["app"]:<26}0x{m["offset"]:08x}{m["length"]:>8} {m["format"]:<10}' + f'{m["stock_dim"]:>9}{m["source_dim"]:>9} {note}') + for a, why in skipped: + print(f' SKIP {a}: {why}') + print(f'\n{len(records)} replacement(s)') + print(f' payload -> {payload_dir}') + print(f' patch -> {os.path.join(outdir, "icons.patch")} ({len(patch)} bytes)') + print(f' rollback -> {os.path.join(outdir, "icons.rollback")} ({len(rollback)} bytes)') + print(f' manifest -> {os.path.join(outdir, "manifest.json")}') + if write_patched: + out = os.path.join(outdir, 'vela_app_patched.bin') + cmd_apply(image, os.path.join(outdir, 'icons.patch'), out) + + +def cmd_apply(image, patch, out): + data = bytearray(open(image, 'rb').read()) + blob = open(patch, 'rb').read() + off = n = 0 + while off < len(blob): + addr, ln = struct.unpack(' {out}') + + +if __name__ == '__main__': + a = sys.argv[1:] + if not a: + print(__doc__) + elif a[0] == 'list': + cmd_list(a[1]) + elif a[0] == 'make': + cmd_make(a[1], a[2], a[3], '--pad' in a, '--patched' in a) + elif a[0] == 'apply': + cmd_apply(a[1], a[2], a[3]) + else: + print(__doc__) diff --git a/s4e-charge/tools/lua_balance.py b/s4e-charge/tools/lua_balance.py new file mode 100644 index 0000000..a33ebba --- /dev/null +++ b/s4e-charge/tools/lua_balance.py @@ -0,0 +1,72 @@ +"""Crude but effective Lua block-balance checker: strips comments and strings, +then counts block openers vs `end`.""" +import sys, re + +def strip(src): + out = [] + i, n = 0, len(src) + while i < n: + c = src[i] + # long bracket comment / string --[[ ]] , --[==[ ]==] + m = re.match(r'--\[(=*)\[', src[i:]) + if m: + close = ']' + m.group(1) + ']' + j = src.find(close, i) + i = n if j < 0 else j + len(close) + out.append(' ') + continue + if src.startswith('--', i): + j = src.find('\n', i) + i = n if j < 0 else j + out.append(' ') + continue + m = re.match(r'\[(=*)\[', src[i:]) + if m: + close = ']' + m.group(1) + ']' + j = src.find(close, i) + i = n if j < 0 else j + len(close) + out.append('""') + continue + if c in '"\'': + j = i + 1 + while j < n: + if src[j] == '\\': + j += 2 + continue + if src[j] == c: + break + j += 1 + i = j + 1 + out.append('""') + continue + out.append(c) + i += 1 + return ''.join(out) + + +def check(path): + src = open(path, encoding='utf-8').read() + code = strip(src) + def cnt(k): + return len(re.findall(r'(? n: + raise ValueError('truncated literal run') + out += data[i:i + nb] + i += nb + else: + cnt = ctrl + if i + blk > n: + raise ValueError('truncated repeat unit') + unit = data[i:i + blk] + i += blk + out += unit * cnt + if out_len is not None and len(out) >= out_len: + break + return bytes(out) + + +def rle_compress(raw, max_run=127): + """LVGL RLE 编码器(rle_decompress 的逆向,blk=1)。 + + 贪心:先找同字节游程(>=2 用「重复」指令),否则攒「原样」指令。 + 输出长度总是 <= 原图像素流,因为最坏情况每条 literal 只多 1 个控制字节。 + """ + out = bytearray() + i, n = 0, len(raw) + while i < n: + v = raw[i] + j = i + 1 + while j < n and raw[j] == v and j - i < max_run: + j += 1 + run = j - i + if run >= 2: + out.append(run); out.append(v); i = j + continue + lit = bytearray() + k = i + while k < n and len(lit) < max_run: + if k + 1 < n and raw[k] == raw[k + 1]: + break + lit.append(raw[k]); k += 1 + if not lit: # 理论上到不了这里 + lit.append(v); k = i + 1 + out.append(0x80 | len(lit)); out += lit + i = k + return bytes(out) + + +def pad_rle(comp, want): + """把 RLE 流补到 want 字节:用 ctrl=0(重复 0 次 = 产出 0 像素)的 2 字节空指令。 + + ctrl=0 是合法指令(`len = 0`,两个越界判断都不触发),所以设备端解码器读完 csize + 字节既不会越界、也不会返回 INVALID —— 这是「等长覆盖」能成立的关键。 + 差值为奇数时先把一条 literal 拆成两条(payload 不变、多 1 个控制字节)。 + """ + d = want - len(comp) + if d < 0: + return None + if d % 2: + b = bytearray(comp) + for idx in range(len(b) - 1, -1, -1): + if b[idx] & 0x80 and (b[idx] & 0x7F) >= 2: + cnt = b[idx] & 0x7F + lit = bytes(b[idx + 1: idx + 1 + cnt]) + half = cnt // 2 + b[idx: idx + 1 + cnt] = (bytes([0x80 | half]) + lit[:half] + + bytes([0x80 | (cnt - half)]) + lit[half:]) + break + else: + return None + comp = bytes(b) + d -= 1 + return comp + b'\x00\x00' * (d // 2) + + +def parse_header(blob): + if len(blob) < 12 or blob[0] != MAGIC: + return None + magic, cf, flags, w, h, stride, rsv = struct.unpack(' 分不到几个颜色。 + 新方案**直接沿用 q63 原帧的调色板 + 索引**(零量化误差,圆环逐像素完全一致), + 只把"很暗的像素"归到一个索引上换取 RLE 长度,阈值从 0 起逐档加大,取**能塞进槽位的最小阈值**。 + 阈值只影响接近黑的雾面区(4~32/255),亮环完全不动。 + +等长要点: + 1) 数据总长必须等于原槽位长度;usize 尾巴必须补够(少一字节设备就不画) + 2) pad_rle_safe 只用 00 00 / 80 这类空指令补齐 +""" +import os, sys, json, struct, shutil, hashlib +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from paths import INPUTS, WORK +from romfs import Romfs +import lvgl_bin as L +from PIL import Image +import numpy as np + +STOCK = os.path.join(INPUTS, 'target', 'vela_system.bin') # S4 eSIM +SOURCE = os.path.join(INPUTS, 'source', 'vela_system.bin') # q63 +OUTDIR = os.path.join(WORK, 'charge') +PATCHED = os.path.join(OUTDIR, 'vela_system_charge.bin') +DIRP = 'charging/' +SAMPLE = None if (len(sys.argv) > 1 and sys.argv[1] == 'all') else [0, 14, 28, 42, 56] +THRS = (0, 4, 8, 12, 16, 20, 24, 32, 40, 48, 64, 96, 128, 192, 255) +FALLBACK_COLORS = (256, 192, 128, 96, 64, 48, 32) + + +def num_of(path): + return int(path.rsplit('charge', 1)[-1].replace('.bin', '')) + + +def collect(rom): + d = {} + for p, i in rom.entries(): + if i.type == 1 or not p.startswith(DIRP) or not p.endswith('.bin'): + continue + d[num_of(p)] = (p, i, rom.read(i)) + return d + + +def src_indexed(blob): + """取出 I8 RLE 帧的原始调色板(256x4 BGRA)与索引图(H,W)。""" + h = L.parse_header(blob) + method, csize, usize = struct.unpack(' 0: + n = min(short, 127) + ex += bytes((n, 0)) + short -= n + return comp + bytes(ex) + + +def main(): + os.makedirs(OUTDIR, exist_ok=True) + stock, src = Romfs(STOCK), Romfs(SOURCE) + slots, srcs = collect(stock), collect(src) + tl, sl = sorted(slots), sorted(srcs) + print('目标 %d 帧(%d..%d,%.1f MB) 源 %d 帧(%d..%d,%.1f MB)' % ( + len(tl), tl[0], tl[-1], sum(len(slots[n][2]) for n in tl) / 1048576, + len(sl), sl[0], sl[-1], sum(len(srcs[n][2]) for n in sl) / 1048576)) + todo = tl if SAMPLE is None else [n for n in SAMPLE if n in slots] + print('本次要改的帧:%s' % todo) + + records, decode_ck, used, fallback = [], 0, {}, 0 + for n in todo: + path, ino, blob = slots[n] + h = L.parse_header(blob) + w, ht, stride = h['w'], h['h'], max(h['stride'], h['w']) + method, csize, usize = struct.unpack(' 60 + e_br = float(d[br].mean()) if br.any() else 0.0 + e_dk = float(d[~br].mean()) if (~br).any() else 0.0 + used[how] = used.get(how, 0) + 1 + print(' 帧 %-3d <- q63 第 %-3d 帧 槽位 %6d B 编码 %6d B 亮区均差 %.2f 暗区均差 %.2f %s' + % (n, m, len(blob), len(comp), e_br, e_dk, how)) + records.append(dict(name='charge%d' % n, src='charge%d' % m, offset=ino.data_off, + length=len(blob), data=new, how=how)) + print('回解自检通过 %d / %d(兜底 %d 帧)' % (decode_ck, len(records), fallback)) + print('编码方式分布:%s' % used) + + shutil.copyfile(STOCK, PATCHED) + with open(PATCHED, 'r+b') as f: + for r in records: + f.seek(r['offset']) + f.write(r['data']) + json.dump(dict(device='Xiaomi Watch S4 eSIM (o62lte) vela_system.bin', target_dir=DIRP, + source='Xiaomi Watch S5 41mm (q63) charging frames', + sample='all' if SAMPLE is None else SAMPLE, + image_sha256=hashlib.sha256(stock.data).hexdigest(), + records=[dict(name=r['name'], offset=r['offset'], length=r['length'], + src=r['src'], how=r['how']) for r in records]), + open(os.path.join(OUTDIR, 'manifest.json'), 'w', encoding='utf-8'), indent=1, ensure_ascii=False) + r2 = Romfs(PATCHED) + a = [(p, i.off, i.next, i.spec, i.size, i.cksum) for p, i in stock.entries()] + b = [(p, i.off, i.next, i.spec, i.size, i.cksum) for p, i in r2.entries()] + print('\ninode 表逐位不变:%s(%d 条)' % (a == b, len(a))) + print('superblock 一致:%s' % (stock.sb_size == r2.sb_size and stock.sb_cksum == r2.sb_cksum)) + sh = open(PATCHED, 'rb').read() + print('回放(改后镜像 == 计划字节):%s' % all( + sh[r['offset']:r['offset'] + r['length']] == r['data'] for r in records)) + print('负载合计 %.2f MB;写出 %s' % (sum(r['length'] for r in records) / 1048576, + os.path.join(OUTDIR, 'manifest.json'))) + + +def pal_lum(pal, vals): + return pal[vals][:, :3].max(axis=1) # 只取最大值,通道序无关 + + +def rgb_of(pal, idx): + return pal[idx][:, :, [2, 1, 0]] + + +if __name__ == '__main__': + main() diff --git a/s4e-charge/tools/make_dd_payload.py b/s4e-charge/tools/make_dd_payload.py new file mode 100644 index 0000000..9265621 --- /dev/null +++ b/s4e-charge/tools/make_dd_payload.py @@ -0,0 +1,136 @@ +"""按「角色」切负载:replace(只负责替换)/ restore(只负责还原)。 + +拆盘的要点:一个 .face 只装一份负载,体积减半。 + * replace 盘:pay = q63 的帧(新字节);rb = **原厂字节前 64 B 的指纹**(只用于版本校验) + * restore 盘:pay = 原厂字节; rb = **新字节前 64 B 的指纹** +指纹表让「写入前版本校验 + 写入后回读比对」两道保险都还在,代价只有 117*64 ≈ 7.5 KB。 +两份盘用同一个 watchface id,装其中一份就会顶掉另一份,设备上始终只占一份空间。 + +产出: + /payload[_restore]/ NNN_.bin + payload.lua + /blob_/payload.bin 单条目:魔数 + 顺序拼接 + /blob_/fp.bin 指纹单条目 + /flash.sh | restore.sh 线性 nsh 脚本(replace 盘还有 restore.sh 的对应物) + /payload.json 机器可读清单 +""" +import os, sys, json, hashlib +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from paths import PROJ, INPUTS, WORK + +ROLE = (sys.argv[1] if len(sys.argv) > 1 else 'replace').lower() +assert ROLE in ('replace', 'restore'), ROLE +STOCK = os.path.join(INPUTS, 'target', 'vela_system.bin') +PATCHED = os.path.join(WORK, 'charge', 'vela_system_charge.bin') +OUT = PROJ + +recs_in = json.load(open(os.path.join(WORK, 'charge', 'manifest.json'), encoding='utf-8')) +stock = open(STOCK, 'rb').read() +patched = open(PATCHED, 'rb').read() +assert len(stock) == len(patched) + +SRC = patched if ROLE == 'replace' else stock # 要写进设备的字节 +OTHER = stock if ROLE == 'replace' else patched # 另一侧的字节(做指纹) +FPDIR = os.path.join(OUT, 'payload' if ROLE == 'replace' else 'payload_restore') +BLOBDIR = os.path.join(OUT, 'blob_' + ROLE) +FP_LEN = 64 + +# 不再往偏移 0 写那 32 字节的「canary」:写 superblock 头没有意义, +# 而且它极可能就是真机上「总是失败 1」的那一条(偏移 0 的写/读回最容易出岔子)。 +assert stock[:8] == b'-rom1fs-' +records = [] +for r in recs_in['records']: + off, ln = r['offset'], r['length'] + records.append(dict(name=r['name'], offset=off, length=ln, data=SRC[off:off + ln], + changed=(stock[off:off + ln] != patched[off:off + ln]) if ROLE == 'replace' else True, + fp=OTHER[off:off + FP_LEN])) +emitted = [r for r in records if r['changed']] +dropped = [r for r in records if not r['changed']] + + +def write_pack(d): + os.makedirs(d, exist_ok=True) + for f in os.listdir(d): + os.remove(os.path.join(d, f)) + rows = [] + for i, r in enumerate(emitted): + fn = '%03d_%s.bin' % (i, r['name']) + open(os.path.join(d, fn), 'wb').write(r['data']) + assert r['offset'] % 4 == 0, r['name'] + rows.append(dict(name=r['name'], file=fn, offset=r['offset'], length=r['length'])) + with open(os.path.join(d, 'payload.lua'), 'w', encoding='utf-8', newline='\n') as f: + f.write('-- generated by tools/make_dd_payload.py (%s) -- do not hand-edit\n' % ROLE) + f.write('-- dd: -if / -of /dev/system bs=4 seek=offset/4 count=length/4\n') + f.write('return {\n') + for r in rows: + f.write(' { name = "%s", file = "%s", offset = %d, length = %d },\n' + % (r['name'], r['file'], r['offset'], r['length'])) + f.write('}\n') + return rows + + +lua_rows = write_pack(FPDIR) + +MAGIC_PAY = b'\x00S4C-PAYv1\x00\xa5\x5a\xc3\x3c' # 15 B,必须全局唯一 +MAGIC_FP = b'\x00S4C-FPv1\x00\x5a\xa5\x3c\xc3' +os.makedirs(BLOBDIR, exist_ok=True) +for f in os.listdir(BLOBDIR): + os.remove(os.path.join(BLOBDIR, f)) +pay_blob = MAGIC_PAY + b''.join(r['data'] for r in emitted) +fp_blob = MAGIC_FP + b''.join(r['fp'] for r in emitted) +open(os.path.join(BLOBDIR, 'payload.bin'), 'wb').write(pay_blob) +open(os.path.join(BLOBDIR, 'fp.bin'), 'wb').write(fp_blob) +rel, off, fpoff = [], 0, 0 +for r in emitted: + rel.append(dict(name=r['name'], rel=off, fprel=fpoff, target=r['offset'], + length=r['length'], fplen=len(r['fp']))) + off += r['length'] + fpoff += len(r['fp']) +assert fpoff == len(fp_blob) - len(MAGIC_FP), (fpoff, len(fp_blob)) +json.dump(dict(role=ROLE, magic_len=len(MAGIC_PAY), magic_fp_len=len(MAGIC_FP), fp_len=FP_LEN, + records=rel, pay_bytes=len(pay_blob), fp_bytes=len(fp_blob)), + open(os.path.join(BLOBDIR, 'offsets.json'), 'w', encoding='utf-8'), indent=1) +print('blob_%s/ 单条目: payload.bin %d B + fp.bin %d B(%d 条记录)' + % (ROLE, len(pay_blob), len(fp_blob), len(rel))) + + +def script(fname, rows, header, base, verify, tail): + with open(os.path.join(OUT, fname), 'w', encoding='utf-8', newline='\n') as f: + f.write('#!/bin/sh\n') + for line in header: + f.write('# ' + line + '\n') + f.write('\nDEV=/dev/system\nBASE=' + base + '\n\n') + f.write('echo "target check"\nls -l $DEV\n\n') + for r in rows: + b = 4 if (r['offset'] % 4 == 0 and r['length'] % 4 == 0) else 1 + f.write('echo "== ' + r['name'] + ' ==\n') + f.write('dd if=$BASE/%s of=$DEV bs=%d seek=%d count=%d\n' + % (r['file'], b, r['offset'] // b, r['length'] // b)) + if verify: + f.write('dd if=$DEV of=/tmp/chk.bin bs=%d skip=%d count=%d\n' + % (b, r['offset'] // b, r['length'] // b)) + f.write('cmp /tmp/chk.bin $BASE/%s\n\n' % r['file']) + f.write(tail + '\n') + + +if ROLE == 'replace': + script('flash.sh', lua_rows, + ['S5 41mm 充电动画 -> S4 eSIM(只写负载)', + '把 flash.sh 与 payload/ 放到同一个目录,例如 /data/s4panim/', + '然后: sh /data/s4panim/flash.sh 最后: reboot', + '还原用 restore.sh 或安装还原盘 S4ChgRestore.face'], + '/data/s4panim/payload', True, + 'rm /tmp/chk.bin\necho "all done - run reboot to apply"') +else: + script('restore.sh', lua_rows, + ['把 S4 eSIM 的充电动画写回原厂(还原盘)'], + '/data/s4panim/payload_restore', True, + 'rm /tmp/chk.bin\necho "restore done - run reboot to apply"') + +json.dump(dict(device='Xiaomi Watch S4 eSIM (o62lte)', target='/dev/system', + role=ROLE, kind='charging-anim', + dd='dd if=/ of=/dev/system bs=4 seek= count=', + block_size=4, records=lua_rows, + dropped_identical=[r['name'] for r in dropped]), + open(os.path.join(OUT, 'payload.json'), 'w', encoding='utf-8'), indent=1, ensure_ascii=False) +total = sum(r['length'] for r in lua_rows) +print('role=%s: emitted %d records, %.2f MB; dropped %d' % (ROLE, len(lua_rows), total / 1048576, len(dropped))) diff --git a/s4e-charge/tools/make_overlays.py b/s4e-charge/tools/make_overlays.py new file mode 100644 index 0000000..6f43631 --- /dev/null +++ b/s4e-charge/tools/make_overlays.py @@ -0,0 +1,75 @@ +"""为官方 LuaDevTemplate 生成可编译的 overlay。两份盘:S4Chg(替换)/ S4ChgRestore(还原)。""" +import os, sys, json, shutil +from PIL import Image, ImageFont +from paths import PROJ, WORK + +ROLE = (sys.argv[1] if len(sys.argv) > 1 else 'replace').lower() +NAME = 'S4Chg' if ROLE == 'replace' else 'S4ChgRestore' +OUT = os.path.join(WORK, 'overlay') +ASSETS = os.path.join(PROJ, 'assets') + +DEVICE_TYPE = "362" # 与官方模板一致(362 = S4 家族) +ENTRY_WIDGET = "app_lua%2Fmain.lua" +PREVIEW_SIZE = 326 # Compiler.exe 对 362 设备强制 326x326 +SCREEN_SIZE = 466 +WF_ID = "362150103" # 两份盘共用同一个 id:装一份就顶掉另一份 +FONTS = [r'C:\Windows\Fonts\msyhbd.ttc', r'C:\Windows\Fonts\msyh.ttc', + r'C:\Windows\Fonts\arialbd.ttf', r'C:\Windows\Fonts\arial.ttf'] + + +def font(size): + for p in FONTS: + try: + return ImageFont.truetype(p, size) + except OSError: + continue + return ImageFont.load_default() + + +def make_preview(path): + Image.open(os.path.join(ASSETS, 'cover.png')).convert('RGBA') \ + .resize((PREVIEW_SIZE, PREVIEW_SIZE), Image.LANCZOS).save(path) + + +def write_fprj(path, title): + xml = ('\r\n' + f'\r\n' + f' \r\n' + f' \r\n' + ' \r\n\r\n') + open(path, 'w', encoding='utf-16', newline='').write(xml) + + +def build(name, role): + base = os.path.join(OUT, name) + if os.path.exists(base): + shutil.rmtree(base) + fprj = os.path.join(base, 'watchface', 'fprj') + os.makedirs(os.path.join(fprj, 'images'), exist_ok=True) + os.makedirs(os.path.join(fprj, 'app', 'lua'), exist_ok=True) + + cfg = {"projectName": name, "watchfaceId": WF_ID, "power_consumption": "3", + "resourceBin": {"lvglVersion": 9, "colorFormat": "I8", "compress": "NONE", + "input": "watchface/fprj/images/preview.png", "name": "preview"}} + json.dump(cfg, open(os.path.join(base, 'watchface.config.json'), 'w', encoding='utf-8'), + indent=4, ensure_ascii=False) + write_fprj(os.path.join(fprj, f'{name}.fprj'), name) + make_preview(os.path.join(fprj, 'images', 'preview.png')) + shutil.copyfile(os.path.join(PROJ, 'main.lua'), + os.path.join(fprj, 'app', 'lua', 'main.lua')) + # 只放「合并后的单条目」(blob_/) —— .face 里每个文件 = 一个资源条目 + blob = os.path.join(PROJ, 'blob_' + role) + if not os.path.isdir(blob): + raise SystemExit('缺少 blob_%s/ —— 先跑 tools/make_dd_payload.py %s' % (role, role)) + for src, rel in ((blob, 'pay'), (os.path.join(PROJ, 'ui_images'), 'ui')): + dst = os.path.join(fprj, 'app', 'images', rel) + if os.path.exists(dst): + shutil.rmtree(dst) + shutil.copytree(src, dst) + return base + + +if __name__ == '__main__': + build(NAME, ROLE) + print('overlay written to', os.path.join(OUT, NAME)) diff --git a/s4e-charge/tools/make_ui_images.py b/s4e-charge/tools/make_ui_images.py new file mode 100644 index 0000000..bcdcb11 --- /dev/null +++ b/s4e-charge/tools/make_ui_images.py @@ -0,0 +1,58 @@ +"""把 assets/ 里的 UI 素材转成 LVGL v9 二进制图片(.bin),供表盘运行时加载。 + +格式(已在真机验证): + header 12B: magic=0x19 | cf | flags | w | h | stride | reserved + cf=0x10 ARGB8888 -> 数据为 BGRA,长度 = w*h*4 +编码器用 iconpack.encode(与图标包同一套,字节级验证过)。 +""" +import os, sys +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +import iconpack as ip +from PIL import Image, ImageDraw + +from paths import PROJ, DOCS +ASSETS = os.path.join(PROJ, 'assets') +OUT = os.path.join(PROJ, 'ui_images') + +# 显示尺寸取自 CSS:列表图标 30x30,返回箭头 17x28 +SPEC = [ + ('back', 'backarrow.png', 17, 28), + ('ic_help', 'MdiLightbulbOutline.png', 30, 30), + ('ic_replace', 'MdiBandage.png', 30, 30), + ('ic_about', 'MdiInformationOutline.png', 30, 30), + ('ic_check', 'MdiClipboardTextSearchOutline.png', 30, 30), + ('ic_rollback', 'MdiReplyAllOutline.png', 30, 30), + ('ic_qq', 'qq.png', 30, 30), +] + +os.makedirs(OUT, exist_ok=True) +for name, png, w, h in SPEC: + p = os.path.join(ASSETS, png) + if not os.path.exists(p): + print(f' 缺少 {png}'); continue + img = Image.open(p).convert('RGBA') + if img.size != (w, h): + img = img.resize((w, h), Image.LANCZOS) + blob = ip.encode(img, ip.CF_ARGB8888, w, h, 0) + dst = os.path.join(OUT, name + '.bin') + open(dst, 'wb').write(blob) + print(f' {name:<12} <- {png:<34} {w}x{h} {len(blob)} bytes') + +# 关于页的 logo:封面图缩到 78x78,并加圆形遮罩 +# (封面是圆形图案 + 黑色方角,不裁成圆的话会在墨绿卡片上露出黑方块) +logo_src = os.path.join(ASSETS, 'cover.png') +if os.path.exists(logo_src): + L = 78 + img = Image.open(logo_src).convert('RGBA').resize((L, L), Image.LANCZOS) + mask = Image.new('L', (L, L), 0) + ImageDraw.Draw(mask).ellipse([0, 0, L - 1, L - 1], fill=255) + img.putalpha(mask) + blob = ip.encode(img, ip.CF_ARGB8888, L, L, 0) + open(os.path.join(OUT, 'logo.bin'), 'wb').write(blob) + # 顺便存一份 PNG 给本机预览用(注意别放进打包目录,会一起被打进去) + prev = os.path.join(DOCS, 'logo_preview.png') + os.makedirs(os.path.dirname(prev), exist_ok=True) + img.save(prev) + print(f' {"logo":<12} <- cover.png(圆形裁切) {L}x{L} {len(blob)} bytes') + +print('written to', OUT) diff --git a/s4e-charge/tools/pages.py b/s4e-charge/tools/pages.py new file mode 100644 index 0000000..cb8f259 --- /dev/null +++ b/s4e-charge/tools/pages.py @@ -0,0 +1,180 @@ +"""页面规格 + 本机渲染器:按 CSS 逐条落地,用真 MiSans 字体和真素材渲染预览。 + +CSS -> 实际能力 的映射(已在真机验证过): + left/top/width/height -> x / y / w / h(466 画布绝对坐标) + background -> bg_color + border-radius -> radius(50x50 圆的 radius=25) + color (alpha 1) -> text_color '#rrggbb' + color (alpha 0.6) -> 白 60% 叠在纯黑底上 = #999999(页面底色是黑,见下) + font-weight 600 -> MiSans-Semibold + font-weight 500 -> MiSans-Medium + vertical-align middle -> vcenter=True + 多色文本 -> LVGL recolor: "灰#ffffff 白#灰" +""" +import os + +from paths import PROJ, DOCS +ROOT = PROJ +ASSETS = os.path.join(ROOT, 'assets') +W = H = 466 +BG = (0, 0, 0) # 封面四角纯黑 -> 页面底色 +GRAY = '999999' # rgba(255,255,255,0.6) 叠黑 +WHITE = 'ffffff' +PLATE = '002927' # rgba(0,41,39,1) +CIRCLE = '4f4f4f' # rgba(79,79,79,1) +OK = '43cf7c' # rgba(67,207,124,1) +ERR = 'd43030' # rgba(212,48,48,1) + +S600, S500 = 'semibold', 'medium' + +# 列表项几何(来自第一个表项的 CSS,其余按同一相对关系排布) +ITEM_X, ITEM_W, ITEM_H, ITEM_R = 49, 368, 88, 34 +PITCH = 96 # 88 + 8px 间距 +ICONC_DX, ICONC_DY, ICONC_SZ = 20, 19, 50 # 圆形遮罩 69-49 / 104-85 +ICON_DX, ICON_DY, ICON_SZ = 30, 29, 30 # 图标图片 79-49 / 114-85 +TITLE_DX, TITLE_DY, TITLE_W, TITLE_H = 80, 17, 208, 54 # 129-49 / 102-85 + + +def item(y, icon, title): + """一个列表项:底板 + 圆形遮罩 + 图标 + 标题""" + return [ + dict(kind='rect', x=ITEM_X, y=y, w=ITEM_W, h=ITEM_H, bg=PLATE, radius=ITEM_R), + dict(kind='circle', x=ITEM_X + ICONC_DX, y=y + ICONC_DY, + w=ICONC_SZ, h=ICONC_SZ, bg=CIRCLE), + dict(kind='image', x=ITEM_X + ICON_DX, y=y + ICON_DY, + w=ICON_SZ, h=ICON_SZ, src=os.path.join(ASSETS, icon)), + dict(kind='label', x=ITEM_X + TITLE_DX, y=y + TITLE_DY, + w=TITLE_W, h=TITLE_H, text=title, size=28, weight=S500, + color=WHITE, align='left', vcenter=True, lh=37.13), + ] + + +def clock(t='10:24'): + return dict(kind='label', x=168, y=0, w=130, h=45, text=t, size=28, + weight=S600, color=GRAY, align='center', vcenter=True, lh=33.6) + + +def title(text, x=106, w=255): + return dict(kind='label', x=x, y=34, w=w, h=45, text=text, size=30, + weight=S500, color=WHITE, align='center', vcenter=True, lh=44.8) + + +def back_arrow(): + return dict(kind='image', x=160, y=43, w=17, h=28, + src=os.path.join(ASSETS, 'backarrow.png')) + + +def big_button(y=365, text='确认替换'): + return [ + dict(kind='rect', x=108, y=y, w=250, h=80, bg=PLATE, radius=40), + dict(kind='label', x=173, y=y + 13, w=120, h=54, text=text, size=28, + weight=S500, color=WHITE, align='center', vcenter=True, lh=37.13), + ] + + +# ---------------------------------------------------------------- 1. 首页 +PAGE_HOME = [ + clock(), + title('OS4 Icons'), + *item(85, 'MdiLightbulbOutline.png', '快速帮助'), + *item(181, 'MdiBandage.png', '开始替换'), + *item(277, 'MdiInformationOutline.png', '关于'), +] + +# ---------------------------------------------------------------- 2. 快速帮助 +_HELP_HEAD = [clock(), back_arrow(), title('快速帮助', x=179, w=128)] +_HELP_I1 = item(85, 'MdiClipboardTextSearchOutline.png', '环境预检查') +_HELP_D1 = dict(kind='label', x=65, y=176, w=338, h=72, + text='检查原始资源状态、分区可写情况等。', size=24, weight=S500, + color=GRAY, align='left', vtop=True, lh=31.82) +_HELP_I2 = item(256, 'MdiReplyAllOutline.png', '回滚修改') +_HELP_D2 = dict(kind='label', x=65, y=344, w=334, h=72, + text='将图标恢复到系统原始样式。', size=24, weight=S500, + color=GRAY, align='left', vtop=True, lh=31.82) + +PAGE_HELP = _HELP_HEAD + _HELP_I1 + [_HELP_D1] + _HELP_I2 + [_HELP_D2] + +# 帮助页的两种结果状态(同一标签换色换字) +HELP_DESC_OK = dict(kind='label', x=65, y=176, w=338, h=72, + text='预检查通过 | 38 条 | 2.58MB\n您可以返回主页开始修改。', + size=24, weight=S500, color=OK, align='left', vtop=True, lh=31.82) +HELP_DESC_ERR = dict(kind='label', x=65, y=176, w=338, h=72, + text='预检查失败:分区头不是 ROMFS,位置 0x00。', + size=24, weight=S500, color=ERR, align='left', vtop=True, lh=31.82) + +PAGE_HELP_OK = _HELP_HEAD + _HELP_I1 + [HELP_DESC_OK] + _HELP_I2 + [_HELP_D2] +PAGE_HELP_ERR = _HELP_HEAD + _HELP_I1 + [HELP_DESC_ERR] + _HELP_I2 + [_HELP_D2] + +# ---------------------------------------------------------------- 3. 开始替换 +# 多色文本**不能**用 recolor(AP 里那几处是图片重着色,不是文本属性), +# 所以拆成多个标签 + 手工算 x。折行也写死,否则多色片段位置不确定。 +# 宽度用真 MiSans 24px 量得: +# "的系统版本号为 " = 177 "3.202.79" = 99 +# "Xiaomi Watch S4 eSIM(非" = 313 "15 周年纪念版)" = 178 +WARN_LABELS = [ + dict(kind='label', x=64, y=90, w=338, h=70, + text='替换图标包之前,请先确认自己\n的系统版本号为 ', + size=24, weight=S500, color=GRAY, align='left', vtop=True, lh=33), + dict(kind='label', x=241, y=123, w=104, h=33, text='3.202.79', + size=24, weight=S500, color=WHITE, align='left', vtop=True, lh=33), + dict(kind='label', x=340, y=123, w=30, h=33, text='!', + size=24, weight=S500, color=GRAY, align='left', vtop=True, lh=33), + dict(kind='label', x=64, y=189, w=338, h=33, text='请确认自己的手表型号为', + size=24, weight=S500, color=GRAY, align='left', vtop=True, lh=33), + dict(kind='label', x=64, y=222, w=338, h=70, + text='Xiaomi Watch S4 eSIM(非\n15 周年纪念版)', + size=24, weight=S500, color=WHITE, align='left', vtop=True, lh=33), + dict(kind='label', x=242, y=255, w=30, h=33, text='!', + size=24, weight=S500, color=GRAY, align='left', vtop=True, lh=33), + dict(kind='label', x=64, y=321, w=338, h=33, text='搞机有风险,变砖后果自负。', + size=24, weight=S500, color=GRAY, align='left', vtop=True, lh=33), +] + +PAGE_CONFIRM = [ + clock(), + back_arrow(), + title('开始替换', x=179, w=128), + *WARN_LABELS, + *big_button(365, '确认替换'), +] + +# ---------------------------------------------------------------- 4. 替换中 +LOG_MID = ('> 开始替换\n' + '> 读取 resource.bin\n' + '> 写入 12/38\n' + '> 写入 38/38 完成,0 失败\n' + '> 替换完毕,请重启手表。若发现问题,请回滚修改。') + +PAGE_RUNNING = [ + clock(), + title('正在替换', x=179, w=128), + dict(kind='label', x=64, y=90, w=338, h=269, text=LOG_MID, size=24, + weight=S500, color=WHITE, align='left', vtop=True, lh=31.82), + *big_button(365, '重启手表'), +] + +# ---------------------------------------------------------------- 5. 关于 +PAGE_ABOUT = [ + clock(), + dict(kind='image', x=191, y=43, w=17, h=28, + src=os.path.join(ASSETS, 'backarrow.png')), + title('关于', x=212, w=64), + dict(kind='rect', x=52, y=79, w=362, h=204, bg=PLATE, radius=30), + dict(kind='image', x=194, y=108, w=78, h=78, + src=os.path.join(DOCS, 'logo_preview.png')), + dict(kind='label', x=164, y=192, w=138, h=38, text='OS4 Icons', + size=28, weight=S500, color=WHITE, align='left', vtop=True, lh=38), + dict(kind='label', x=210, y=227, w=45, h=27, text='1.0.0', + size=20, weight=S500, color=GRAY, align='left', vtop=True, lh=27), + *item(291, 'qq.png', '732681995'), +] + +PAGES = { + 'home': PAGE_HOME, + 'help': PAGE_HELP, + 'help_ok': PAGE_HELP_OK, + 'help_err': PAGE_HELP_ERR, + 'confirm': PAGE_CONFIRM, + 'about': PAGE_ABOUT, + 'running': PAGE_RUNNING, +} diff --git a/s4e-charge/tools/paths.py b/s4e-charge/tools/paths.py new file mode 100644 index 0000000..a2da22a --- /dev/null +++ b/s4e-charge/tools/paths.py @@ -0,0 +1,35 @@ +"""项目里所有脚本的路径入口 —— 换机器 / 换目录只看这一个文件。 + +默认布局(都相对项目根): + inputs/target/ 目标固件的分区镜像(要自己放,见 README) + inputs/source/ 图标来源的分区镜像(要自己放) + inputs/fonts/ 可选:MiSans-Medium.ttf / MiSans-Semibold.ttf(渲染预览用) + vendor/ 第三方 LuaDevTemplate-main(里面有 Compiler.exe,要自己放) + .work/ 构建工作区(产物,可删) + dist/ 交付的 .face + docs/ 说明与预览图 + +环境变量可覆盖:ICON_EDIT_INPUTS / ICON_EDIT_VENDOR / ICON_EDIT_WORK / + ICON_EDIT_DIST / ICON_EDIT_DOCS / ICON_EDIT_FONTS +""" +import os + +PROJ = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + + +def _p(env, default): + return os.environ.get(env) or os.path.join(PROJ, default) + + +INPUTS = _p('ICON_EDIT_INPUTS', 'inputs') +VENDOR = _p('ICON_EDIT_VENDOR', 'vendor') +WORK = _p('ICON_EDIT_WORK', '.work') +DIST = _p('ICON_EDIT_DIST', 'dist') +DOCS = _p('ICON_EDIT_DOCS', 'docs') +FONTS = os.environ.get('ICON_EDIT_FONTS') or os.path.join(INPUTS, 'fonts') +TEMPLATE = os.path.join(VENDOR, 'LuaDevTemplate-main') +INPUT_TARGET = os.path.join(INPUTS, 'target') +INPUT_SOURCE = os.path.join(INPUTS, 'source') + +for _d in (WORK, DIST, DOCS, INPUT_TARGET, INPUT_SOURCE): + os.makedirs(_d, exist_ok=True) diff --git a/s4e-charge/tools/render_ui.py b/s4e-charge/tools/render_ui.py new file mode 100644 index 0000000..3c75dc5 --- /dev/null +++ b/s4e-charge/tools/render_ui.py @@ -0,0 +1,193 @@ +"""按页面规格渲染 466x466 预览图(真 MiSans 字体 + 真素材 + 圆形遮罩)。 + +支持 kind: rect / circle / image / label +label 支持 recolor 标记 "#RRGGBB 文字#"(LVGL 的写法),以及 align/vcenter/vtop/lh +""" +import os, re, sys +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from PIL import Image, ImageDraw, ImageFont + +from paths import PROJ, INPUTS, FONTS, WORK +ROOT = PROJ +FONT_FILES = {'semibold': 'MiSans-Semibold.ttf', 'medium': 'MiSans-Medium.ttf', + 'regular': 'MiSans-Regular.ttf'} +W = H = 466 +_fc = {} + + +def _font_from_firmware(name): + """从 inputs/target/vela_font.bin 里现取 MiSans(固件里就有这几个 ttf)。""" + p = os.path.join(INPUTS, 'target', 'vela_font.bin') + if not os.path.exists(p): + return None + try: + from romfs import Romfs + r = Romfs(p) + except Exception: + return None + for path, ino in r.entries(): + if path == name: + d = os.path.join(WORK, 'fonts') + os.makedirs(d, exist_ok=True) + out = os.path.join(d, name) + open(out, 'wb').write(r.read(ino)) + return out + return None + + +def font(weight, size): + k = (weight, size) + if k in _fc: + return _fc[k] + name = FONT_FILES.get(weight, FONT_FILES['medium']) + p = os.path.join(FONTS, name) + if not os.path.exists(p): + p = _font_from_firmware(name) or p + if not os.path.exists(p): + for alt in (r'C:\Windows\Fonts\msyh.ttc', r'C:\Windows\Fonts\msyhbd.ttc', + r'C:\Windows\Fonts\arial.ttf'): + if os.path.exists(alt): + p = alt + print(' [warn] %s 缺失,预览改用 %s(只影响预览缩略图)' % (name, os.path.basename(alt))) + break + _fc[k] = ImageFont.truetype(p, size) if os.path.exists(p) else ImageFont.load_default() + return _fc[k] + + +def _rgb(c): + return tuple(int(c[i:i + 2], 16) for i in (0, 2, 4)) + + +def split_recolor(text, base): + """LVGL 写法: '灰#ffffff 白#灰' + - # + 6 位十六进制 -> 切到该颜色(该字符不显示) + - 再次遇到单独的 # -> 结束着色,回到基础色(该字符不显示) + """ + runs, buf, cur, incol = [], '', base, False + i = 0 + while i < len(text): + if text[i] == '#': + if re.match(r'[0-9a-fA-F]{6}', text[i + 1:i + 7]): + runs.append((cur, buf)); buf = '' + cur = text[i + 1:i + 7].lower(); incol = True + i += 7 + continue + if incol: + runs.append((cur, buf)); buf = '' + cur = base; incol = False + i += 1 + continue + buf += text[i]; i += 1 + runs.append((cur, buf)) + return runs + + +def wrap_runs(d, runs, f, maxw): + """按像素宽度折行,返回 [[(color, text), ...], ...](尽量保持原换行)""" + lines = [[]] + for color, chunk in runs: + for k, seg in enumerate(chunk.split('\n')): + if k: + lines.append([]) + if not seg: + continue + cur = '' + for ch in seg: + if d.textlength(cur + ch, font=f) > maxw and cur: + lines[-1].append((color, cur)) + lines.append([]) + cur = ch + else: + cur += ch + if cur: + lines[-1].append((color, cur)) + return lines + + +OVERFLOW = [] # (标签文本前 12 字, css_lh, 行数, 块高, 框高) + + +def draw_label(img, d, it): + f = font(it.get('weight', 'medium'), it['size']) + runs = split_recolor(it['text'], it.get('color', 'ffffff')) \ + if it.get('recolor') else [(it.get('color', 'ffffff'), it['text'])] + asc, desc = f.getmetrics() + natural = asc + desc + lh = max(it.get('lh', 0), natural) # LVGL 用字体自身行高,取较大者预测 + lines = wrap_runs(d, runs, f, it['w']) + total = lh * len(lines) + if total > it['h'] + 0.5: + OVERFLOW.append((it['text'][:14].replace('\n', '/'), + round(it.get('lh', 0), 2), len(lines), + round(total, 1), it['h'], round(natural, 2))) + if it.get('vcenter'): + y = it['y'] + (it['h'] - total) / 2 + elif it.get('vtop'): + y = it['y'] + else: + y = it['y'] + (it['h'] - total) / 2 + for ln in lines: + wsum = sum(d.textlength(t, font=f) for _, t in ln) + if it.get('align') == 'center': + x = it['x'] + (it['w'] - wsum) / 2 + elif it.get('align') == 'right': + x = it['x'] + it['w'] - wsum + else: + x = it['x'] + for color, t in ln: + d.text((x, y), t, font=f, fill=_rgb(color) + (255,)) + x += d.textlength(t, font=f) + y += lh + + +def render(items, out, mask=True, ring=False): + img = Image.new('RGBA', (W, H), (0, 0, 0, 0)) + d = ImageDraw.Draw(img) + for it in items: + k = it.get('kind') + if k == 'rect': + box = [it['x'], it['y'], it['x'] + it['w'] - 1, it['y'] + it['h'] - 1] + r = it.get('radius', 0) + (d.rounded_rectangle if r else d.rectangle)( + box, radius=r, fill=_rgb(it['bg']) + (255,)) if r else d.rectangle( + box, fill=_rgb(it['bg']) + (255,)) + elif k == 'circle': + box = [it['x'], it['y'], it['x'] + it['w'] - 1, it['y'] + it['h'] - 1] + d.ellipse(box, fill=_rgb(it['bg']) + (255,)) + elif k == 'image': + p = it['src'] + if os.path.exists(p): + im = Image.open(p).convert('RGBA').resize((it['w'], it['h']), Image.LANCZOS) + img.alpha_composite(im, (it['x'], it['y'])) + elif k == 'label': + draw_label(img, d, it) + if mask: + m = Image.new('L', (W, H), 0) + ImageDraw.Draw(m).ellipse([0, 0, W - 1, H - 1], fill=255) + img.putalpha(m) + base = Image.new('RGB', (W, H), (0, 0, 0)) + base.paste(img, (0, 0), img) + if ring: + ImageDraw.Draw(base).ellipse([0, 0, W - 1, H - 1], outline=(80, 80, 90)) + base.save(out) + return out + + +if __name__ == '__main__': + from pages import PAGES + outs = [] + for name, items in PAGES.items(): + p = os.path.join(ROOT, '_out', f'ui_{name}.png') + render(items, p) + outs.append(p) + print('wrote', p) + # 拼一张总览图 + cols = 3 + rows = (len(outs) + cols - 1) // cols + sheet = Image.new('RGB', (cols * (W + 12) + 12, rows * (H + 12) + 12), (28, 28, 32)) + for i, p in enumerate(outs): + im = Image.open(p) + sheet.paste(im, (12 + (i % cols) * (W + 12), 12 + (i // cols) * (H + 12))) + sp = os.path.join(ROOT, '_out', 'ui_all.png') + sheet.save(sp) + print('wrote', sp, sheet.size) diff --git a/s4e-charge/tools/romfs.py b/s4e-charge/tools/romfs.py new file mode 100644 index 0000000..753dda2 --- /dev/null +++ b/s4e-charge/tools/romfs.py @@ -0,0 +1,112 @@ +"""Minimal reader for Xiaomi Vela ROMFS partition images (Linux 'rom1fs' variant). + +Layout (big-endian): + super : magic "-rom1fs-", u32 size, u32 checksum, name (NUL-term, padded to 16) + inode : u32 next (low 4 bits = ROMFH_ type flags), u32 spec, u32 size, u32 checksum, + name (NUL-term, padded to 16), then file data immediately after. + type : 0=hardlink (spec = target inode offset), 1=dir (spec = offset of first child), + 2=regular file, 8=exec bit OR-ed in. + For a directory, its children are the chain starting at `spec`, linked by + `next & ~0xF`, and they stay inside the directory's own block. +""" +import struct, os + +ROUND16 = lambda n: (n + 15) & ~15 + +class Inode: + __slots__ = ('off', 'next', 'spec', 'size', 'cksum', 'name', 'namelen', 'data_off', 'kind') + + @property + def type_flags(self): + return self.next & 0xF + + @property + def type(self): + return self.next & 0x7 + + @property + def exec_bit(self): + return bool(self.next & 0x8) + + @property + def next_off(self): + return self.next & ~0xF + + KIND = {0: 'HRD', 1: 'DIR', 2: 'REG', 3: 'SYM', 4: 'BLK', 5: 'CHR', 6: 'SCK', 7: 'FIF'} + + def __repr__(self): + return (f'') + + +class Romfs: + def __init__(self, path): + self.path = path + self.data = open(path, 'rb').read() + d = self.data + if d[0:8] != b'-rom1fs-': + raise ValueError('not a romfs image') + self.sb_size, self.sb_cksum = struct.unpack('>II', d[8:16]) + end = d.index(b'\x00', 16) + self.volume = d[16:end].decode('latin1') + self.root_off = 16 + ROUND16(end - 16 + 1) + + def inode(self, off): + i = Inode() + i.off = off + i.next, i.spec, i.size, i.cksum = struct.unpack('>IIII', self.data[off:off+16]) + p = off + 16 + end = self.data.find(b'\x00', p, p + 256) + if end < 0: + raise ValueError(f'bad name at 0x{off:x}') + i.name = self.data[p:end].decode('latin1') + i.namelen = end - p + i.data_off = off + 16 + ROUND16(i.namelen + 1) + return i + + def walk(self, first, limit, prefix=''): + """Yield (path, Inode) for every entry reachable from `first` below `limit`. + + `limit` bounds the sibling chain. The LAST entry of a chain has next==0, + but its own children still live after it: they inherit the parent's limit + instead of being bounded by that zero. + """ + off = first + seen = set() + while off and (limit == 0 or off < limit) and off not in seen: + seen.add(off) + i = self.inode(off) + path = prefix + i.name + yield path, i + if i.type == 1 and i.spec: # directory -> first child at spec + yield from self.walk(i.spec, i.next_off or limit, path + '/') + off = i.next_off + + def entries(self): + """All entries of the image, as (path, Inode).""" + return self.walk(self.root_off + 0x20, len(self.data)) + + def read(self, ino): + return self.data[ino.data_off:ino.data_off + ino.size] + + +def img_header(blob): + if len(blob) < 12 or blob[0] != 0x19: + return None + magic, cf, flags, w, h, stride, res = struct.unpack(' 2 and sys.argv[2] not in path: + continue + kind = ('DIR ' if i.type == 1 else 'LNK ' if i.type == 0 else 'FILE') + print(f'{kind} {i.size:>9} {path}') + print(f'-- total entries: {n}') diff --git a/s4e-charge/tools/verify_anim.py b/s4e-charge/tools/verify_anim.py new file mode 100644 index 0000000..d4515a6 --- /dev/null +++ b/s4e-charge/tools/verify_anim.py @@ -0,0 +1,106 @@ +# -*- coding: utf-8 -*- +"""独立校验交付的 .face(只看交付物 + 固件镜像,不看中间产物)。 + + rome: replace -> dist/S4Chg.face pay 应等于**改后镜像**的字节,fp 应等于**原厂**前 64 B + restore -> dist/S4ChgRestore.face pay 应等于**原厂**字节, fp 应等于**改后镜像**前 64 B + ① 从 .face 里抠出 Lua 表(pay / rb),按表里的偏移取字节 + ② 每条 pay 长度必须 == 原厂槽位长度(等长覆盖的底线),且能按 LVGL RLE 解出 464x464 + ③ rb 每条必须是 64 B 指纹,且等于另一侧字节的前 64 B(版本校验数据正确) + ④ 目标镜像的 inode 表 / superblock / 分区长度与原厂逐位一致 +""" +import os, re, sys +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from paths import PROJ, INPUTS, WORK +from romfs import Romfs +import lvgl_bin as L + +ROLE = (sys.argv[1] if len(sys.argv) > 1 else 'replace').lower() +NAME = 'S4Chg' if ROLE == 'replace' else 'S4ChgRestore' +FACE = os.path.join(PROJ, 'dist', NAME + '.face') +STOCK = os.path.join(INPUTS, 'target', 'vela_system.bin') +PATCHED = os.path.join(WORK, 'charge', 'vela_system_charge.bin') +DIRP = 'charging/' +FP_LEN = 64 +ok = fail = 0 + + +def ck(cond, msg): + global ok, fail + if cond: + ok += 1 + else: + fail += 1 + print(' !! ' + msg) + + +face = open(FACE, 'rb').read() +src = face.decode('latin1') +m = re.search(r'pay = \{(.*?)\n \},', src, re.S) +m2 = re.search(r'rb = \{(.*?)\n \},', src, re.S) +ck(m is not None and m2 is not None, '在 .face 里找不到 pay / rb 表') +pay = [tuple(int(x) for x in r) for r in re.findall(r'\{(\d+),(\d+),(\d+)\}', m.group(1))] +rb = [tuple(int(x) for x in r) for r in re.findall(r'\{(\d+),(\d+),(\d+)\}', m2.group(1))] +print('%s:pay %d 条,rb %d 条' % (NAME, len(pay), len(rb))) + +stock_b = open(STOCK, 'rb').read() +patch_b = open(PATCHED, 'rb').read() +c = re.search(r"local ROLE = '(\w+)'", src) +ck(c is not None and c.group(1) == ROLE, 'Lua 里的 ROLE 不是 %s' % ROLE) +EXPECT_PAY = patch_b if ROLE == 'replace' else stock_b +EXPECT_FP = stock_b if ROLE == 'replace' else patch_b +stock = Romfs(STOCK) +slots = {} +for p, i in stock.entries(): + if i.type == 1 or not p.startswith(DIRP) or not p.endswith('.bin') or i.size == 0: + continue + slots[i.data_off] = (p, i.size) + +canary = [r for r in pay if r[1] == 0 and r[2] == 32] +ck(len(canary) <= 1, 'canary 记录不止一条') +if canary: + ck(face[canary[0][0]:canary[0][0] + 32] == stock_b[:32], 'canary 不对') +data = [r for r in pay if not (r[1] == 0 and r[2] == 32)] +bad_len = bad_dec = bad_bytes = 0 +for o, t, ln in data: + blob = face[o:o + ln] + if t not in slots or slots[t][1] != ln: + bad_len += 1; continue + if EXPECT_PAY[t:t + ln] != blob: + bad_bytes += 1 + h = L.parse_header(blob) + if h is None or not (h['flags'] & L.FLAG_COMPRESSED) or h['w'] != 464 or h['h'] != 464: + bad_dec += 1; continue + # ★ 设备端解码器要求流解出**正好 usize 字节**(1024 + w*h + 1),少一字节就整帧不画 + import struct as _s + _m, _cs, _us = _s.unpack('0.5% 就跳过 —— 拉伸会把方块变长方形、裁切会切掉按钮两端,宁可留原厂。所以 `walkie_talkie/image/stop.bin`(源 176×96 vs 槽位 144×96)和 `walkie_talkie/guidance/finish_guidance.bin`(176×96 vs 160×96)**保持原厂**(本机逐字节确认过) +- **对讲机那一组是 I8+RLE 压缩槽位**(固件里 `flags=0x08`,长度都不是 4 的倍数)→ 用 `iconpack.encode_into_slot()`:量化(颜色数从 256 逐级往下试)→ RLE → 用 `ctrl=0` 的 2 字节空指令补齐到原 `csize`,做到**逐字节等长**;设备端写入时这类记录自动退化成 `dd bs=1` +- 界面 466×466(设计稿在 `tools/pages.py`,本机真 MiSans 渲染 + 真实像素判圆屏切边) +- 只读预检查 + 写入 + 一键回滚;另外还有一个**不依赖表盘**的 `flash.sh` / `rollback.sh`(有 shell 时直接跑) + +--- + +## 1. 交付物与安装 + +| 文件 | 说明 | +|---|---| +| `dist/S5Icons.face` | **主交付物** 8,024,916 B(sha256 b371a40e8eed53a0…),表盘 id `362150101`,b5=0 | +| `dist/S5Icons_toolchain.face` | 官方工具链原样产物(b5=10);上面那个装不上时换这个 | +| `.work/s4_from_s5/vela_app_s5icons.bin` | 重建后得到的整分区镜像(265 MB,可直接刷机,绕开设备侧脚本) | +| `flash.sh` / `rollback.sh` | 有 nsh shell 时不用表盘:把脚本和 `payload/` 复制到 `/data/s5icons/` 再 `sh flash.sh` | +| `docs/分析报告-S4eSIM.md` | 固件结构 / 为什么必须等长 / 为什么锁版本 的完整分析 | +| `docs/图标命名对照表.md` | 39 个槽位的名字、偏移、格式对照表 | +| `docs/ui_all_466.png` | 466×466 七个页面预览 | +| `docs/s4e_sysicons.png` | 42 张系统素材(22 控制中心 + 8 锁屏/通知/设置/米家 + 12 压力表情):S4 原厂 → 从 S5 41mm 搬来的 对照图 | +| `docs/s4e_pressure.png` | 12 张压力 App 小表情:S4 原厂 → q63 对照图 | + +装法与普通 Lua 表盘相同(`resource.bin` 推到 `/data/app/watchface/market//`)。装好后在表盘上操作: + +``` +首页 →「快速帮助」→「环境预检查」 只读,不写任何字节 +首页 →「开始替换」→ 确认 → 自动写入 149 条 →「重启手表」 + 快速帮助 →「回滚修改」→ 确认 → 写回原厂图标 +``` + +--- + +## 2. 目录 + +``` +main.lua 表盘源码(含 __UIIMG__ / __PAY__ / __RB__ 占位符,构建时回填) +payload/ 80 个负载 .bin(NNN_名字.bin)+ payload.lua 清单 +payload_rollback/ 同结构,内容是原厂字节(回滚用) +ui_images/ 界面素材(LVGL v9 .bin,8 张) +assets/ 设计素材(Mdi 图标、返回箭头、QQ、封面) +assets/override/ 3 张 112×112 自制 PNG(activities / sports_course / interconnect),优先级高于固件来源 +tools/ 全部脚本(见下表) +dist/ 交付的 .face +docs/ 说明、分析报告、预览图 +inputs/ 要你自己放固件分区镜像(见第 4 节,不进仓库) +vendor/ 放第三方 LuaDevTemplate-main(不进仓库) +.work/ 构建工作区(产物,可删) +``` + +| 脚本 | 作用 | +|---|---| +| `tools/paths.py` | 所有路径的唯一入口(inputs / vendor / .work / dist,支持环境变量覆盖) | +| `tools/make_s5_to_s4_inset.py` | S5 图标 → S4 槽位(尺寸归一化 + I8 调色板复用 + 自制 PNG 覆盖) | +| `tools/make_dd_payload.py` | 切分成逐条负载 + 回滚负载 + `flash.sh` / `rollback.sh` / `payload.json` | +| `tools/make_ui_images.py` | 设计素材 PNG → LVGL .bin + 圆形 logo | +| `tools/pages.py` | 466×466 页面规格(唯一的设计稿来源) | +| `tools/render_ui.py` | 用真 MiSans 渲染 7 页预览 | +| `tools/check_clip.py` | 用真实渲染像素判圆屏是否切边 | +| `tools/gen_ui_lua.py` | 两遍构建 → `dist/S5Icons.face`(自动回填偏移 + 0 漂移校验) | +| `tools/romfs.py` `tools/lvgl_bin.py` `tools/iconpack.py` `tools/lua_balance.py` | 共用底层(ROMFS 解析 / LVGL 图片编解码 / Lua 块平衡) | + +--- + +## 3. 目标固件的事实 + +| 项 | 结论 | +|---|---| +| 机型 | Xiaomi Watch S4 eSIM(o62lte),屏幕 466×466 | +| Lua 宿主 | 有,Lua 5.4;7 个 C 模块 activity / dataman / miwear / navigator / screen / topic / vibrator + luavgl | +| 图标槽位 | 39 个 `launcher.bin`(38 个 `/launcher.bin` + `phone/contacts_launcher.bin`),都在 `/dev/app`(265 MB ROMFS 分区) | +| 图标格式 | 33 个 ARGB8888(82,956 B)+ 6 个 I8(21,772 B);144×144 画布 | +| 原厂画法 | 内容 136×136 居中(4px 透明边);I8 槽位必须复用源图调色板,否则边缘锯齿 + 彩点 | + +## 4. 替换内容 + +- **37 个**槽位真正被改写;`voice_alexa`、`wxpay` 与 S5 源逐字节相同,已从负载中剔除 +- **另外搬了 22 张控制中心磁贴**(`control_center/icon/*`,S4 128×128 ← q63 116×116,缩放 + 复用源图 I8 调色板):勿扰 / 静音 / 手电 / 设置 / 飞行 / 电池 / 移动网络 / 抬腕亮屏 / 排水 / 查找手机 / 蓝牙 / 耳机 / 取消 / 加号 / 网络开关。对照图见 `docs/s4e_sysicons.png` + - ⚠ 说明:S4 原厂是**蓝色主题**(选中=实心蓝盘),q63 是**浅色主题**(白盘 + 浅色字形)——q63 那边看着「淡」是它的正常样子(OS4 的浅色控制中心)。 + - ✅ **已实测:效果可以**(用户确认)。 +- **另外 8 个系统 UI 素材也一起搬了**(和 S5 eSIM 那份同一组):`system/icon/lock_icon.bin`(锁屏锁)、`common/icon/system_lock.bin`(状态栏锁)、`common/icon/quiet_mode.bin`(免打扰小图标)、`notifications/no_message.bin`(通知空状态)、`settings/icon/notify.bin`(设置-通知,80→64)、`mijia/enable|disable_donot_disturb.bin`(米家免打扰,80→64)、`esimsms/icon/esim_sms_reminder.bin`(短信提醒)。对照图同 `docs/s4e_sysicons.png`。 +- **压力 App 小表情也搬了 12 张**(S4 没有 health 分区,这些都在 `/dev/app` 的 `pressure/icon/`):4 档情绪 48px(relax/mild/mid/severe)+ 32px 三张 + `press_grade1..4` + `presure_icon`。没搬:96px(两边本来就一样)、`pressure_eomji_bg/_widbg`(S4 没有这两张)、整屏背景与 `measure/*` 动画帧、`reminder.bin`。对照图 `docs/s4e_pressure.png` +- `walkie_talkie` 是 S4 独有、S5 没有,保持原厂 +- `activities`、`sports_course`、`interconnect` 用 `assets/override/` 里的自制 112×112 PNG;`voice_aivs`(小爱同学)用 `assets/override/voice_aivs.png`(用户给的 144×144 PNG)—— 都会裁到实心范围、缩放到该槽位自己的画法(136×136)再归一化 +- 编码策略:S5 文件长度 == S4 槽位长度 → 原样拷贝;否则解码后按 **S4 槽位的色彩格式**重新编码,保证长度逐字节相等 + +--- + +## 5. 重建 + +### 5.1 需要自己准备的输入(放进 `inputs/`,不进仓库) + +| 路径 | 是什么 | 大小 / sha256 前 16 位 | +|---|---|---| +| `inputs/target/vela_app.bin` | 目标固件(o62lte 3.202.79)app 分区 | 265,684,992 / caa66693f49477dd | +| `inputs/source/vela_app.bin` | 图标来源(S5 41mm q63)app 分区 | 206,000,128 / 161a8f7a5f0bd8af | +| `inputs/source/vela_health.bin` | 图标来源 health 分区 | 233,939,968 / 12a3bd2f8b62e969 | +| `inputs/fonts/MiSans-Medium.ttf`、`MiSans-Semibold.ttf` | 可选:渲染预览用;没有就退化成系统字体(只影响预览图) | — | +| `vendor/LuaDevTemplate-main/` | 官方模板(里面有 `Compiler.exe`) | — | + +固件分区镜像就是官方 OTA 包(ZIP)里的 `vela_*.bin` 条目,解压出来即可。 + +### 5.2 步骤 + +```powershell +# 需要 Python 3 + Pillow +python tools/make_s5_to_s4_inset.py # S5 图标 -> S4 槽位(产出 .work/s4_from_s5/vela_app_s5icons.bin) +python tools/make_dd_payload.py # 切成逐条负载 + 回滚 + flash.sh/rollback.sh +python tools/make_ui_images.py # 界面素材 +python tools/gen_ui_lua.py # 两遍构建 -> dist/S5Icons.face +python tools/render_ui.py # 466 预览(可选) +python tools/check_clip.py # 圆屏切边检查(可选) +``` + +### 5.3 只想换自定义图标? + +把 144×144 PNG 按 `docs/图标命名对照表.md` 命名,然后: + +```powershell +python tools/iconpack.py make inputs/target/vela_app.bin <你的PNG目录> .work/mypack --patched +``` + +--- + +## 6. 已知边界 +- **对讲机素材默认不打包**(ICON_WALKIE=1 可打开);160 条对讲机 PNG 仍在 ssets/walkie_talkie/。 +- **控制中心的米家/融合中心那组:默认不打包**(用户 2026-10-02 决定不改;`ICON_MIJIA=1` 可打开)。已确认用户两台设备本来就装了同一个米家手表 App(`com.xiaomi.smarthome.watch.*.rpk`,129 张设备图标),128 张与 q63 **逐字节相同**,所以控制中心那套不需要移植;更多设备图标属于米家 App 内部素材,`/dev/app` 里没有对应槽位。 + + +1. **偏移锁固件版本**:ROMFS 是绝对偏移顺序排布,跨版本套用旧偏移 = 往错误位置写。换固件要重跑上面的流程。 +2. **等长是硬要求**:替换文件长度与原槽位不相等就会让后面所有偏移平移、镜像损坏;脚本对每条都断言长度相等。 +3. **I8 槽位**(compass / debug / lua / perpetual_calendar / remote_camera / wxpay)必须复用源图调色板,否则边缘会出锯齿和彩点。 +4. **真机能力**:`os.execute` / `dd` 可用;`io.popen` 捕获不到输出;`os.execute` 的返回值不可靠(成功也可能是 65280)→ 判据一律看内容。 +5. 首次请只点「环境预检查」,看清输出再替换。 + +## 7. 第三方与风险 + +- 本仓库不含固件、OTA 包、字体;`.face` 由第三方 `Compiler.exe` 编译(见 [../README.md](../README.md)) +- 往分区节点写数据是危险操作;官方 OTA 升级会整分区覆盖,图标回到原厂 diff --git a/s4e-icons/assets/MdiBandage.png b/s4e-icons/assets/MdiBandage.png new file mode 100644 index 0000000..26abc5b Binary files /dev/null and b/s4e-icons/assets/MdiBandage.png differ diff --git a/s4e-icons/assets/MdiClipboardTextSearchOutline.png b/s4e-icons/assets/MdiClipboardTextSearchOutline.png new file mode 100644 index 0000000..85ea2da Binary files /dev/null and b/s4e-icons/assets/MdiClipboardTextSearchOutline.png differ diff --git a/s4e-icons/assets/MdiInformationOutline.png b/s4e-icons/assets/MdiInformationOutline.png new file mode 100644 index 0000000..3af2a29 Binary files /dev/null and b/s4e-icons/assets/MdiInformationOutline.png differ diff --git a/s4e-icons/assets/MdiLightbulbOutline.png b/s4e-icons/assets/MdiLightbulbOutline.png new file mode 100644 index 0000000..7d7819c Binary files /dev/null and b/s4e-icons/assets/MdiLightbulbOutline.png differ diff --git a/s4e-icons/assets/MdiReplyAllOutline.png b/s4e-icons/assets/MdiReplyAllOutline.png new file mode 100644 index 0000000..f419de0 Binary files /dev/null and b/s4e-icons/assets/MdiReplyAllOutline.png differ diff --git a/s4e-icons/assets/backarrow.png b/s4e-icons/assets/backarrow.png new file mode 100644 index 0000000..9e55859 Binary files /dev/null and b/s4e-icons/assets/backarrow.png differ diff --git a/s4e-icons/assets/cover.png b/s4e-icons/assets/cover.png new file mode 100644 index 0000000..23c1cf8 Binary files /dev/null and b/s4e-icons/assets/cover.png differ diff --git a/s4e-icons/assets/override/activities.png b/s4e-icons/assets/override/activities.png new file mode 100644 index 0000000..ae5c552 Binary files /dev/null and b/s4e-icons/assets/override/activities.png differ diff --git a/s4e-icons/assets/override/interconnect.png b/s4e-icons/assets/override/interconnect.png new file mode 100644 index 0000000..5739b3b Binary files /dev/null and b/s4e-icons/assets/override/interconnect.png differ diff --git a/s4e-icons/assets/override/source_smallai.png b/s4e-icons/assets/override/source_smallai.png new file mode 100644 index 0000000..228914f Binary files /dev/null and b/s4e-icons/assets/override/source_smallai.png differ diff --git a/s4e-icons/assets/override/sports_course.png b/s4e-icons/assets/override/sports_course.png new file mode 100644 index 0000000..4b95e98 Binary files /dev/null and b/s4e-icons/assets/override/sports_course.png differ diff --git a/s4e-icons/assets/override/voice_aivs.png b/s4e-icons/assets/override/voice_aivs.png new file mode 100644 index 0000000..228914f Binary files /dev/null and b/s4e-icons/assets/override/voice_aivs.png differ diff --git a/s4e-icons/assets/qq.png b/s4e-icons/assets/qq.png new file mode 100644 index 0000000..3982762 Binary files /dev/null and b/s4e-icons/assets/qq.png differ diff --git a/s4e-icons/assets/walkie_talkie/background.png b/s4e-icons/assets/walkie_talkie/background.png new file mode 100644 index 0000000..879c98f Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/background.png differ diff --git a/s4e-icons/assets/walkie_talkie/cancel_btn.png b/s4e-icons/assets/walkie_talkie/cancel_btn.png new file mode 100644 index 0000000..cc56188 Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/cancel_btn.png differ diff --git a/s4e-icons/assets/walkie_talkie/check.png b/s4e-icons/assets/walkie_talkie/check.png new file mode 100644 index 0000000..bc89d28 Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/check.png differ diff --git a/s4e-icons/assets/walkie_talkie/checked.png b/s4e-icons/assets/walkie_talkie/checked.png new file mode 100644 index 0000000..64bf2b9 Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/checked.png differ diff --git a/s4e-icons/assets/walkie_talkie/close_btn.png b/s4e-icons/assets/walkie_talkie/close_btn.png new file mode 100644 index 0000000..a872247 Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/close_btn.png differ diff --git a/s4e-icons/assets/walkie_talkie/confirm_btn.png b/s4e-icons/assets/walkie_talkie/confirm_btn.png new file mode 100644 index 0000000..932d711 Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/confirm_btn.png differ diff --git a/s4e-icons/assets/walkie_talkie/delete_icon.png b/s4e-icons/assets/walkie_talkie/delete_icon.png new file mode 100644 index 0000000..b3d948d Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/delete_icon.png differ diff --git a/s4e-icons/assets/walkie_talkie/guidance/finish_guidance.png b/s4e-icons/assets/walkie_talkie/guidance/finish_guidance.png new file mode 100644 index 0000000..932d711 Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/guidance/finish_guidance.png differ diff --git a/s4e-icons/assets/walkie_talkie/history_anim/anim0.png b/s4e-icons/assets/walkie_talkie/history_anim/anim0.png new file mode 100644 index 0000000..cdbf172 Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/history_anim/anim0.png differ diff --git a/s4e-icons/assets/walkie_talkie/history_anim/anim1.png b/s4e-icons/assets/walkie_talkie/history_anim/anim1.png new file mode 100644 index 0000000..9eb6f8b Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/history_anim/anim1.png differ diff --git a/s4e-icons/assets/walkie_talkie/history_anim/anim10.png b/s4e-icons/assets/walkie_talkie/history_anim/anim10.png new file mode 100644 index 0000000..927d24a Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/history_anim/anim10.png differ diff --git a/s4e-icons/assets/walkie_talkie/history_anim/anim11.png b/s4e-icons/assets/walkie_talkie/history_anim/anim11.png new file mode 100644 index 0000000..38d3985 Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/history_anim/anim11.png differ diff --git a/s4e-icons/assets/walkie_talkie/history_anim/anim12.png b/s4e-icons/assets/walkie_talkie/history_anim/anim12.png new file mode 100644 index 0000000..32c452c Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/history_anim/anim12.png differ diff --git a/s4e-icons/assets/walkie_talkie/history_anim/anim13.png b/s4e-icons/assets/walkie_talkie/history_anim/anim13.png new file mode 100644 index 0000000..b4338f7 Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/history_anim/anim13.png differ diff --git a/s4e-icons/assets/walkie_talkie/history_anim/anim14.png b/s4e-icons/assets/walkie_talkie/history_anim/anim14.png new file mode 100644 index 0000000..6dd42f1 Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/history_anim/anim14.png differ diff --git a/s4e-icons/assets/walkie_talkie/history_anim/anim15.png b/s4e-icons/assets/walkie_talkie/history_anim/anim15.png new file mode 100644 index 0000000..e4795f0 Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/history_anim/anim15.png differ diff --git a/s4e-icons/assets/walkie_talkie/history_anim/anim16.png b/s4e-icons/assets/walkie_talkie/history_anim/anim16.png new file mode 100644 index 0000000..5cdb8ad Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/history_anim/anim16.png differ diff --git a/s4e-icons/assets/walkie_talkie/history_anim/anim17.png b/s4e-icons/assets/walkie_talkie/history_anim/anim17.png new file mode 100644 index 0000000..9eb6f8b Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/history_anim/anim17.png differ diff --git a/s4e-icons/assets/walkie_talkie/history_anim/anim18.png b/s4e-icons/assets/walkie_talkie/history_anim/anim18.png new file mode 100644 index 0000000..cdbf172 Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/history_anim/anim18.png differ diff --git a/s4e-icons/assets/walkie_talkie/history_anim/anim2.png b/s4e-icons/assets/walkie_talkie/history_anim/anim2.png new file mode 100644 index 0000000..5cdb8ad Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/history_anim/anim2.png differ diff --git a/s4e-icons/assets/walkie_talkie/history_anim/anim3.png b/s4e-icons/assets/walkie_talkie/history_anim/anim3.png new file mode 100644 index 0000000..e4795f0 Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/history_anim/anim3.png differ diff --git a/s4e-icons/assets/walkie_talkie/history_anim/anim4.png b/s4e-icons/assets/walkie_talkie/history_anim/anim4.png new file mode 100644 index 0000000..6dd42f1 Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/history_anim/anim4.png differ diff --git a/s4e-icons/assets/walkie_talkie/history_anim/anim5.png b/s4e-icons/assets/walkie_talkie/history_anim/anim5.png new file mode 100644 index 0000000..b4338f7 Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/history_anim/anim5.png differ diff --git a/s4e-icons/assets/walkie_talkie/history_anim/anim6.png b/s4e-icons/assets/walkie_talkie/history_anim/anim6.png new file mode 100644 index 0000000..32c452c Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/history_anim/anim6.png differ diff --git a/s4e-icons/assets/walkie_talkie/history_anim/anim7.png b/s4e-icons/assets/walkie_talkie/history_anim/anim7.png new file mode 100644 index 0000000..38d3985 Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/history_anim/anim7.png differ diff --git a/s4e-icons/assets/walkie_talkie/history_anim/anim8.png b/s4e-icons/assets/walkie_talkie/history_anim/anim8.png new file mode 100644 index 0000000..927d24a Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/history_anim/anim8.png differ diff --git a/s4e-icons/assets/walkie_talkie/history_anim/anim9.png b/s4e-icons/assets/walkie_talkie/history_anim/anim9.png new file mode 100644 index 0000000..6d5503a Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/history_anim/anim9.png differ diff --git a/s4e-icons/assets/walkie_talkie/history_anim_right/anim0.png b/s4e-icons/assets/walkie_talkie/history_anim_right/anim0.png new file mode 100644 index 0000000..cdbf172 Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/history_anim_right/anim0.png differ diff --git a/s4e-icons/assets/walkie_talkie/history_anim_right/anim1.png b/s4e-icons/assets/walkie_talkie/history_anim_right/anim1.png new file mode 100644 index 0000000..5d99f04 Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/history_anim_right/anim1.png differ diff --git a/s4e-icons/assets/walkie_talkie/history_anim_right/anim10.png b/s4e-icons/assets/walkie_talkie/history_anim_right/anim10.png new file mode 100644 index 0000000..493b920 Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/history_anim_right/anim10.png differ diff --git a/s4e-icons/assets/walkie_talkie/history_anim_right/anim11.png b/s4e-icons/assets/walkie_talkie/history_anim_right/anim11.png new file mode 100644 index 0000000..e596207 Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/history_anim_right/anim11.png differ diff --git a/s4e-icons/assets/walkie_talkie/history_anim_right/anim12.png b/s4e-icons/assets/walkie_talkie/history_anim_right/anim12.png new file mode 100644 index 0000000..44ccabf Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/history_anim_right/anim12.png differ diff --git a/s4e-icons/assets/walkie_talkie/history_anim_right/anim13.png b/s4e-icons/assets/walkie_talkie/history_anim_right/anim13.png new file mode 100644 index 0000000..cc6d96c Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/history_anim_right/anim13.png differ diff --git a/s4e-icons/assets/walkie_talkie/history_anim_right/anim14.png b/s4e-icons/assets/walkie_talkie/history_anim_right/anim14.png new file mode 100644 index 0000000..709fe42 Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/history_anim_right/anim14.png differ diff --git a/s4e-icons/assets/walkie_talkie/history_anim_right/anim15.png b/s4e-icons/assets/walkie_talkie/history_anim_right/anim15.png new file mode 100644 index 0000000..08c909e Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/history_anim_right/anim15.png differ diff --git a/s4e-icons/assets/walkie_talkie/history_anim_right/anim16.png b/s4e-icons/assets/walkie_talkie/history_anim_right/anim16.png new file mode 100644 index 0000000..5b5a34a Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/history_anim_right/anim16.png differ diff --git a/s4e-icons/assets/walkie_talkie/history_anim_right/anim17.png b/s4e-icons/assets/walkie_talkie/history_anim_right/anim17.png new file mode 100644 index 0000000..5d99f04 Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/history_anim_right/anim17.png differ diff --git a/s4e-icons/assets/walkie_talkie/history_anim_right/anim18.png b/s4e-icons/assets/walkie_talkie/history_anim_right/anim18.png new file mode 100644 index 0000000..cdbf172 Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/history_anim_right/anim18.png differ diff --git a/s4e-icons/assets/walkie_talkie/history_anim_right/anim2.png b/s4e-icons/assets/walkie_talkie/history_anim_right/anim2.png new file mode 100644 index 0000000..5b5a34a Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/history_anim_right/anim2.png differ diff --git a/s4e-icons/assets/walkie_talkie/history_anim_right/anim3.png b/s4e-icons/assets/walkie_talkie/history_anim_right/anim3.png new file mode 100644 index 0000000..08c909e Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/history_anim_right/anim3.png differ diff --git a/s4e-icons/assets/walkie_talkie/history_anim_right/anim4.png b/s4e-icons/assets/walkie_talkie/history_anim_right/anim4.png new file mode 100644 index 0000000..709fe42 Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/history_anim_right/anim4.png differ diff --git a/s4e-icons/assets/walkie_talkie/history_anim_right/anim5.png b/s4e-icons/assets/walkie_talkie/history_anim_right/anim5.png new file mode 100644 index 0000000..cc6d96c Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/history_anim_right/anim5.png differ diff --git a/s4e-icons/assets/walkie_talkie/history_anim_right/anim6.png b/s4e-icons/assets/walkie_talkie/history_anim_right/anim6.png new file mode 100644 index 0000000..44ccabf Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/history_anim_right/anim6.png differ diff --git a/s4e-icons/assets/walkie_talkie/history_anim_right/anim7.png b/s4e-icons/assets/walkie_talkie/history_anim_right/anim7.png new file mode 100644 index 0000000..e596207 Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/history_anim_right/anim7.png differ diff --git a/s4e-icons/assets/walkie_talkie/history_anim_right/anim8.png b/s4e-icons/assets/walkie_talkie/history_anim_right/anim8.png new file mode 100644 index 0000000..493b920 Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/history_anim_right/anim8.png differ diff --git a/s4e-icons/assets/walkie_talkie/history_anim_right/anim9.png b/s4e-icons/assets/walkie_talkie/history_anim_right/anim9.png new file mode 100644 index 0000000..2286ae4 Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/history_anim_right/anim9.png differ diff --git a/s4e-icons/assets/walkie_talkie/icon_plus.png b/s4e-icons/assets/walkie_talkie/icon_plus.png new file mode 100644 index 0000000..a1117ab Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/icon_plus.png differ diff --git a/s4e-icons/assets/walkie_talkie/image/Speaker0.png b/s4e-icons/assets/walkie_talkie/image/Speaker0.png new file mode 100644 index 0000000..fc91da7 Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/image/Speaker0.png differ diff --git a/s4e-icons/assets/walkie_talkie/image/Speaker1.png b/s4e-icons/assets/walkie_talkie/image/Speaker1.png new file mode 100644 index 0000000..66fc559 Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/image/Speaker1.png differ diff --git a/s4e-icons/assets/walkie_talkie/image/Speaker10.png b/s4e-icons/assets/walkie_talkie/image/Speaker10.png new file mode 100644 index 0000000..2bb9b59 Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/image/Speaker10.png differ diff --git a/s4e-icons/assets/walkie_talkie/image/Speaker100.png b/s4e-icons/assets/walkie_talkie/image/Speaker100.png new file mode 100644 index 0000000..521ed3e Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/image/Speaker100.png differ diff --git a/s4e-icons/assets/walkie_talkie/image/Speaker101.png b/s4e-icons/assets/walkie_talkie/image/Speaker101.png new file mode 100644 index 0000000..9b955dd Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/image/Speaker101.png differ diff --git a/s4e-icons/assets/walkie_talkie/image/Speaker102.png b/s4e-icons/assets/walkie_talkie/image/Speaker102.png new file mode 100644 index 0000000..7825c03 Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/image/Speaker102.png differ diff --git a/s4e-icons/assets/walkie_talkie/image/Speaker11.png b/s4e-icons/assets/walkie_talkie/image/Speaker11.png new file mode 100644 index 0000000..c4415dd Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/image/Speaker11.png differ diff --git a/s4e-icons/assets/walkie_talkie/image/Speaker12.png b/s4e-icons/assets/walkie_talkie/image/Speaker12.png new file mode 100644 index 0000000..844818b Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/image/Speaker12.png differ diff --git a/s4e-icons/assets/walkie_talkie/image/Speaker13.png b/s4e-icons/assets/walkie_talkie/image/Speaker13.png new file mode 100644 index 0000000..89029fd Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/image/Speaker13.png differ diff --git a/s4e-icons/assets/walkie_talkie/image/Speaker14.png b/s4e-icons/assets/walkie_talkie/image/Speaker14.png new file mode 100644 index 0000000..221f19a Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/image/Speaker14.png differ diff --git a/s4e-icons/assets/walkie_talkie/image/Speaker15.png b/s4e-icons/assets/walkie_talkie/image/Speaker15.png new file mode 100644 index 0000000..4423df7 Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/image/Speaker15.png differ diff --git a/s4e-icons/assets/walkie_talkie/image/Speaker16.png b/s4e-icons/assets/walkie_talkie/image/Speaker16.png new file mode 100644 index 0000000..bd8aa8a Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/image/Speaker16.png differ diff --git a/s4e-icons/assets/walkie_talkie/image/Speaker17.png b/s4e-icons/assets/walkie_talkie/image/Speaker17.png new file mode 100644 index 0000000..9ea2e8b Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/image/Speaker17.png differ diff --git a/s4e-icons/assets/walkie_talkie/image/Speaker18.png b/s4e-icons/assets/walkie_talkie/image/Speaker18.png new file mode 100644 index 0000000..fb5a10b Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/image/Speaker18.png differ diff --git a/s4e-icons/assets/walkie_talkie/image/Speaker19.png b/s4e-icons/assets/walkie_talkie/image/Speaker19.png new file mode 100644 index 0000000..3ad3bf7 Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/image/Speaker19.png differ diff --git a/s4e-icons/assets/walkie_talkie/image/Speaker2.png b/s4e-icons/assets/walkie_talkie/image/Speaker2.png new file mode 100644 index 0000000..8601636 Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/image/Speaker2.png differ diff --git a/s4e-icons/assets/walkie_talkie/image/Speaker20.png b/s4e-icons/assets/walkie_talkie/image/Speaker20.png new file mode 100644 index 0000000..b6ea25b Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/image/Speaker20.png differ diff --git a/s4e-icons/assets/walkie_talkie/image/Speaker21.png b/s4e-icons/assets/walkie_talkie/image/Speaker21.png new file mode 100644 index 0000000..14200fd Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/image/Speaker21.png differ diff --git a/s4e-icons/assets/walkie_talkie/image/Speaker22.png b/s4e-icons/assets/walkie_talkie/image/Speaker22.png new file mode 100644 index 0000000..3d9c48f Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/image/Speaker22.png differ diff --git a/s4e-icons/assets/walkie_talkie/image/Speaker23.png b/s4e-icons/assets/walkie_talkie/image/Speaker23.png new file mode 100644 index 0000000..e91fd3d Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/image/Speaker23.png differ diff --git a/s4e-icons/assets/walkie_talkie/image/Speaker24.png b/s4e-icons/assets/walkie_talkie/image/Speaker24.png new file mode 100644 index 0000000..393fc07 Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/image/Speaker24.png differ diff --git a/s4e-icons/assets/walkie_talkie/image/Speaker25.png b/s4e-icons/assets/walkie_talkie/image/Speaker25.png new file mode 100644 index 0000000..fa9e68e Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/image/Speaker25.png differ diff --git a/s4e-icons/assets/walkie_talkie/image/Speaker26.png b/s4e-icons/assets/walkie_talkie/image/Speaker26.png new file mode 100644 index 0000000..6d43f7d Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/image/Speaker26.png differ diff --git a/s4e-icons/assets/walkie_talkie/image/Speaker27.png b/s4e-icons/assets/walkie_talkie/image/Speaker27.png new file mode 100644 index 0000000..0a65418 Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/image/Speaker27.png differ diff --git a/s4e-icons/assets/walkie_talkie/image/Speaker28.png b/s4e-icons/assets/walkie_talkie/image/Speaker28.png new file mode 100644 index 0000000..b0160e4 Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/image/Speaker28.png differ diff --git a/s4e-icons/assets/walkie_talkie/image/Speaker29.png b/s4e-icons/assets/walkie_talkie/image/Speaker29.png new file mode 100644 index 0000000..1a8c275 Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/image/Speaker29.png differ diff --git a/s4e-icons/assets/walkie_talkie/image/Speaker3.png b/s4e-icons/assets/walkie_talkie/image/Speaker3.png new file mode 100644 index 0000000..142da0d Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/image/Speaker3.png differ diff --git a/s4e-icons/assets/walkie_talkie/image/Speaker30.png b/s4e-icons/assets/walkie_talkie/image/Speaker30.png new file mode 100644 index 0000000..c07d68b Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/image/Speaker30.png differ diff --git a/s4e-icons/assets/walkie_talkie/image/Speaker31.png b/s4e-icons/assets/walkie_talkie/image/Speaker31.png new file mode 100644 index 0000000..b5bf877 Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/image/Speaker31.png differ diff --git a/s4e-icons/assets/walkie_talkie/image/Speaker32.png b/s4e-icons/assets/walkie_talkie/image/Speaker32.png new file mode 100644 index 0000000..c7635aa Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/image/Speaker32.png differ diff --git a/s4e-icons/assets/walkie_talkie/image/Speaker33.png b/s4e-icons/assets/walkie_talkie/image/Speaker33.png new file mode 100644 index 0000000..55c5c23 Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/image/Speaker33.png differ diff --git a/s4e-icons/assets/walkie_talkie/image/Speaker34.png b/s4e-icons/assets/walkie_talkie/image/Speaker34.png new file mode 100644 index 0000000..8291262 Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/image/Speaker34.png differ diff --git a/s4e-icons/assets/walkie_talkie/image/Speaker35.png b/s4e-icons/assets/walkie_talkie/image/Speaker35.png new file mode 100644 index 0000000..7299baa Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/image/Speaker35.png differ diff --git a/s4e-icons/assets/walkie_talkie/image/Speaker36.png b/s4e-icons/assets/walkie_talkie/image/Speaker36.png new file mode 100644 index 0000000..ab8b08e Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/image/Speaker36.png differ diff --git a/s4e-icons/assets/walkie_talkie/image/Speaker37.png b/s4e-icons/assets/walkie_talkie/image/Speaker37.png new file mode 100644 index 0000000..5f76111 Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/image/Speaker37.png differ diff --git a/s4e-icons/assets/walkie_talkie/image/Speaker38.png b/s4e-icons/assets/walkie_talkie/image/Speaker38.png new file mode 100644 index 0000000..41cb88e Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/image/Speaker38.png differ diff --git a/s4e-icons/assets/walkie_talkie/image/Speaker39.png b/s4e-icons/assets/walkie_talkie/image/Speaker39.png new file mode 100644 index 0000000..72a5863 Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/image/Speaker39.png differ diff --git a/s4e-icons/assets/walkie_talkie/image/Speaker4.png b/s4e-icons/assets/walkie_talkie/image/Speaker4.png new file mode 100644 index 0000000..c7ad479 Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/image/Speaker4.png differ diff --git a/s4e-icons/assets/walkie_talkie/image/Speaker40.png b/s4e-icons/assets/walkie_talkie/image/Speaker40.png new file mode 100644 index 0000000..81c7391 Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/image/Speaker40.png differ diff --git a/s4e-icons/assets/walkie_talkie/image/Speaker41.png b/s4e-icons/assets/walkie_talkie/image/Speaker41.png new file mode 100644 index 0000000..9881ad6 Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/image/Speaker41.png differ diff --git a/s4e-icons/assets/walkie_talkie/image/Speaker42.png b/s4e-icons/assets/walkie_talkie/image/Speaker42.png new file mode 100644 index 0000000..7e9ec32 Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/image/Speaker42.png differ diff --git a/s4e-icons/assets/walkie_talkie/image/Speaker43.png b/s4e-icons/assets/walkie_talkie/image/Speaker43.png new file mode 100644 index 0000000..216afb2 Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/image/Speaker43.png differ diff --git a/s4e-icons/assets/walkie_talkie/image/Speaker44.png b/s4e-icons/assets/walkie_talkie/image/Speaker44.png new file mode 100644 index 0000000..873728e Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/image/Speaker44.png differ diff --git a/s4e-icons/assets/walkie_talkie/image/Speaker45.png b/s4e-icons/assets/walkie_talkie/image/Speaker45.png new file mode 100644 index 0000000..70535de Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/image/Speaker45.png differ diff --git a/s4e-icons/assets/walkie_talkie/image/Speaker46.png b/s4e-icons/assets/walkie_talkie/image/Speaker46.png new file mode 100644 index 0000000..43563a3 Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/image/Speaker46.png differ diff --git a/s4e-icons/assets/walkie_talkie/image/Speaker47.png b/s4e-icons/assets/walkie_talkie/image/Speaker47.png new file mode 100644 index 0000000..f7bd1b7 Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/image/Speaker47.png differ diff --git a/s4e-icons/assets/walkie_talkie/image/Speaker48.png b/s4e-icons/assets/walkie_talkie/image/Speaker48.png new file mode 100644 index 0000000..6a942f9 Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/image/Speaker48.png differ diff --git a/s4e-icons/assets/walkie_talkie/image/Speaker49.png b/s4e-icons/assets/walkie_talkie/image/Speaker49.png new file mode 100644 index 0000000..f65235c Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/image/Speaker49.png differ diff --git a/s4e-icons/assets/walkie_talkie/image/Speaker5.png b/s4e-icons/assets/walkie_talkie/image/Speaker5.png new file mode 100644 index 0000000..8681f10 Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/image/Speaker5.png differ diff --git a/s4e-icons/assets/walkie_talkie/image/Speaker50.png b/s4e-icons/assets/walkie_talkie/image/Speaker50.png new file mode 100644 index 0000000..be90a63 Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/image/Speaker50.png differ diff --git a/s4e-icons/assets/walkie_talkie/image/Speaker51.png b/s4e-icons/assets/walkie_talkie/image/Speaker51.png new file mode 100644 index 0000000..d6de73f Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/image/Speaker51.png differ diff --git a/s4e-icons/assets/walkie_talkie/image/Speaker52.png b/s4e-icons/assets/walkie_talkie/image/Speaker52.png new file mode 100644 index 0000000..8b58b2d Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/image/Speaker52.png differ diff --git a/s4e-icons/assets/walkie_talkie/image/Speaker53.png b/s4e-icons/assets/walkie_talkie/image/Speaker53.png new file mode 100644 index 0000000..7b3b206 Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/image/Speaker53.png differ diff --git a/s4e-icons/assets/walkie_talkie/image/Speaker54.png b/s4e-icons/assets/walkie_talkie/image/Speaker54.png new file mode 100644 index 0000000..2367008 Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/image/Speaker54.png differ diff --git a/s4e-icons/assets/walkie_talkie/image/Speaker55.png b/s4e-icons/assets/walkie_talkie/image/Speaker55.png new file mode 100644 index 0000000..dbdf016 Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/image/Speaker55.png differ diff --git a/s4e-icons/assets/walkie_talkie/image/Speaker56.png b/s4e-icons/assets/walkie_talkie/image/Speaker56.png new file mode 100644 index 0000000..1d3eacb Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/image/Speaker56.png differ diff --git a/s4e-icons/assets/walkie_talkie/image/Speaker57.png b/s4e-icons/assets/walkie_talkie/image/Speaker57.png new file mode 100644 index 0000000..97c19e3 Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/image/Speaker57.png differ diff --git a/s4e-icons/assets/walkie_talkie/image/Speaker58.png b/s4e-icons/assets/walkie_talkie/image/Speaker58.png new file mode 100644 index 0000000..a7195d0 Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/image/Speaker58.png differ diff --git a/s4e-icons/assets/walkie_talkie/image/Speaker59.png b/s4e-icons/assets/walkie_talkie/image/Speaker59.png new file mode 100644 index 0000000..3e36cba Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/image/Speaker59.png differ diff --git a/s4e-icons/assets/walkie_talkie/image/Speaker6.png b/s4e-icons/assets/walkie_talkie/image/Speaker6.png new file mode 100644 index 0000000..2503467 Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/image/Speaker6.png differ diff --git a/s4e-icons/assets/walkie_talkie/image/Speaker60.png b/s4e-icons/assets/walkie_talkie/image/Speaker60.png new file mode 100644 index 0000000..cb0e1b9 Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/image/Speaker60.png differ diff --git a/s4e-icons/assets/walkie_talkie/image/Speaker61.png b/s4e-icons/assets/walkie_talkie/image/Speaker61.png new file mode 100644 index 0000000..93b8905 Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/image/Speaker61.png differ diff --git a/s4e-icons/assets/walkie_talkie/image/Speaker62.png b/s4e-icons/assets/walkie_talkie/image/Speaker62.png new file mode 100644 index 0000000..d171dab Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/image/Speaker62.png differ diff --git a/s4e-icons/assets/walkie_talkie/image/Speaker63.png b/s4e-icons/assets/walkie_talkie/image/Speaker63.png new file mode 100644 index 0000000..3bdfd0f Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/image/Speaker63.png differ diff --git a/s4e-icons/assets/walkie_talkie/image/Speaker64.png b/s4e-icons/assets/walkie_talkie/image/Speaker64.png new file mode 100644 index 0000000..392093f Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/image/Speaker64.png differ diff --git a/s4e-icons/assets/walkie_talkie/image/Speaker65.png b/s4e-icons/assets/walkie_talkie/image/Speaker65.png new file mode 100644 index 0000000..e26ca7c Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/image/Speaker65.png differ diff --git a/s4e-icons/assets/walkie_talkie/image/Speaker66.png b/s4e-icons/assets/walkie_talkie/image/Speaker66.png new file mode 100644 index 0000000..57aff14 Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/image/Speaker66.png differ diff --git a/s4e-icons/assets/walkie_talkie/image/Speaker67.png b/s4e-icons/assets/walkie_talkie/image/Speaker67.png new file mode 100644 index 0000000..2b7281a Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/image/Speaker67.png differ diff --git a/s4e-icons/assets/walkie_talkie/image/Speaker68.png b/s4e-icons/assets/walkie_talkie/image/Speaker68.png new file mode 100644 index 0000000..cdb5592 Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/image/Speaker68.png differ diff --git a/s4e-icons/assets/walkie_talkie/image/Speaker69.png b/s4e-icons/assets/walkie_talkie/image/Speaker69.png new file mode 100644 index 0000000..dc7eba5 Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/image/Speaker69.png differ diff --git a/s4e-icons/assets/walkie_talkie/image/Speaker7.png b/s4e-icons/assets/walkie_talkie/image/Speaker7.png new file mode 100644 index 0000000..054a489 Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/image/Speaker7.png differ diff --git a/s4e-icons/assets/walkie_talkie/image/Speaker70.png b/s4e-icons/assets/walkie_talkie/image/Speaker70.png new file mode 100644 index 0000000..6bbe93d Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/image/Speaker70.png differ diff --git a/s4e-icons/assets/walkie_talkie/image/Speaker71.png b/s4e-icons/assets/walkie_talkie/image/Speaker71.png new file mode 100644 index 0000000..bc451fb Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/image/Speaker71.png differ diff --git a/s4e-icons/assets/walkie_talkie/image/Speaker72.png b/s4e-icons/assets/walkie_talkie/image/Speaker72.png new file mode 100644 index 0000000..69b5ee7 Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/image/Speaker72.png differ diff --git a/s4e-icons/assets/walkie_talkie/image/Speaker73.png b/s4e-icons/assets/walkie_talkie/image/Speaker73.png new file mode 100644 index 0000000..909de00 Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/image/Speaker73.png differ diff --git a/s4e-icons/assets/walkie_talkie/image/Speaker74.png b/s4e-icons/assets/walkie_talkie/image/Speaker74.png new file mode 100644 index 0000000..a2d65b2 Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/image/Speaker74.png differ diff --git a/s4e-icons/assets/walkie_talkie/image/Speaker75.png b/s4e-icons/assets/walkie_talkie/image/Speaker75.png new file mode 100644 index 0000000..77d3cf6 Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/image/Speaker75.png differ diff --git a/s4e-icons/assets/walkie_talkie/image/Speaker76.png b/s4e-icons/assets/walkie_talkie/image/Speaker76.png new file mode 100644 index 0000000..4bacc59 Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/image/Speaker76.png differ diff --git a/s4e-icons/assets/walkie_talkie/image/Speaker77.png b/s4e-icons/assets/walkie_talkie/image/Speaker77.png new file mode 100644 index 0000000..3527380 Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/image/Speaker77.png differ diff --git a/s4e-icons/assets/walkie_talkie/image/Speaker78.png b/s4e-icons/assets/walkie_talkie/image/Speaker78.png new file mode 100644 index 0000000..f9cfe31 Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/image/Speaker78.png differ diff --git a/s4e-icons/assets/walkie_talkie/image/Speaker79.png b/s4e-icons/assets/walkie_talkie/image/Speaker79.png new file mode 100644 index 0000000..4583948 Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/image/Speaker79.png differ diff --git a/s4e-icons/assets/walkie_talkie/image/Speaker8.png b/s4e-icons/assets/walkie_talkie/image/Speaker8.png new file mode 100644 index 0000000..6b885ec Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/image/Speaker8.png differ diff --git a/s4e-icons/assets/walkie_talkie/image/Speaker80.png b/s4e-icons/assets/walkie_talkie/image/Speaker80.png new file mode 100644 index 0000000..6dbe5d3 Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/image/Speaker80.png differ diff --git a/s4e-icons/assets/walkie_talkie/image/Speaker81.png b/s4e-icons/assets/walkie_talkie/image/Speaker81.png new file mode 100644 index 0000000..cdff971 Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/image/Speaker81.png differ diff --git a/s4e-icons/assets/walkie_talkie/image/Speaker82.png b/s4e-icons/assets/walkie_talkie/image/Speaker82.png new file mode 100644 index 0000000..304ccea Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/image/Speaker82.png differ diff --git a/s4e-icons/assets/walkie_talkie/image/Speaker83.png b/s4e-icons/assets/walkie_talkie/image/Speaker83.png new file mode 100644 index 0000000..fe62e25 Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/image/Speaker83.png differ diff --git a/s4e-icons/assets/walkie_talkie/image/Speaker84.png b/s4e-icons/assets/walkie_talkie/image/Speaker84.png new file mode 100644 index 0000000..263f40c Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/image/Speaker84.png differ diff --git a/s4e-icons/assets/walkie_talkie/image/Speaker85.png b/s4e-icons/assets/walkie_talkie/image/Speaker85.png new file mode 100644 index 0000000..060ae88 Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/image/Speaker85.png differ diff --git a/s4e-icons/assets/walkie_talkie/image/Speaker86.png b/s4e-icons/assets/walkie_talkie/image/Speaker86.png new file mode 100644 index 0000000..c8fec2a Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/image/Speaker86.png differ diff --git a/s4e-icons/assets/walkie_talkie/image/Speaker87.png b/s4e-icons/assets/walkie_talkie/image/Speaker87.png new file mode 100644 index 0000000..053ee6d Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/image/Speaker87.png differ diff --git a/s4e-icons/assets/walkie_talkie/image/Speaker88.png b/s4e-icons/assets/walkie_talkie/image/Speaker88.png new file mode 100644 index 0000000..15505f5 Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/image/Speaker88.png differ diff --git a/s4e-icons/assets/walkie_talkie/image/Speaker89.png b/s4e-icons/assets/walkie_talkie/image/Speaker89.png new file mode 100644 index 0000000..162e637 Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/image/Speaker89.png differ diff --git a/s4e-icons/assets/walkie_talkie/image/Speaker9.png b/s4e-icons/assets/walkie_talkie/image/Speaker9.png new file mode 100644 index 0000000..03b9b1e Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/image/Speaker9.png differ diff --git a/s4e-icons/assets/walkie_talkie/image/Speaker90.png b/s4e-icons/assets/walkie_talkie/image/Speaker90.png new file mode 100644 index 0000000..ceec461 Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/image/Speaker90.png differ diff --git a/s4e-icons/assets/walkie_talkie/image/Speaker91.png b/s4e-icons/assets/walkie_talkie/image/Speaker91.png new file mode 100644 index 0000000..b3b35ae Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/image/Speaker91.png differ diff --git a/s4e-icons/assets/walkie_talkie/image/Speaker92.png b/s4e-icons/assets/walkie_talkie/image/Speaker92.png new file mode 100644 index 0000000..3464b7d Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/image/Speaker92.png differ diff --git a/s4e-icons/assets/walkie_talkie/image/Speaker93.png b/s4e-icons/assets/walkie_talkie/image/Speaker93.png new file mode 100644 index 0000000..de800f6 Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/image/Speaker93.png differ diff --git a/s4e-icons/assets/walkie_talkie/image/Speaker94.png b/s4e-icons/assets/walkie_talkie/image/Speaker94.png new file mode 100644 index 0000000..51e6c2a Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/image/Speaker94.png differ diff --git a/s4e-icons/assets/walkie_talkie/image/Speaker95.png b/s4e-icons/assets/walkie_talkie/image/Speaker95.png new file mode 100644 index 0000000..a22dffe Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/image/Speaker95.png differ diff --git a/s4e-icons/assets/walkie_talkie/image/Speaker96.png b/s4e-icons/assets/walkie_talkie/image/Speaker96.png new file mode 100644 index 0000000..f7151aa Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/image/Speaker96.png differ diff --git a/s4e-icons/assets/walkie_talkie/image/Speaker97.png b/s4e-icons/assets/walkie_talkie/image/Speaker97.png new file mode 100644 index 0000000..eae1262 Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/image/Speaker97.png differ diff --git a/s4e-icons/assets/walkie_talkie/image/Speaker98.png b/s4e-icons/assets/walkie_talkie/image/Speaker98.png new file mode 100644 index 0000000..328931c Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/image/Speaker98.png differ diff --git a/s4e-icons/assets/walkie_talkie/image/Speaker99.png b/s4e-icons/assets/walkie_talkie/image/Speaker99.png new file mode 100644 index 0000000..11a494e Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/image/Speaker99.png differ diff --git a/s4e-icons/assets/walkie_talkie/image/go_next.png b/s4e-icons/assets/walkie_talkie/image/go_next.png new file mode 100644 index 0000000..e9a1d81 Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/image/go_next.png differ diff --git a/s4e-icons/assets/walkie_talkie/image/stop.png b/s4e-icons/assets/walkie_talkie/image/stop.png new file mode 100644 index 0000000..b611709 Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/image/stop.png differ diff --git a/s4e-icons/assets/walkie_talkie/launcher.png b/s4e-icons/assets/walkie_talkie/launcher.png new file mode 100644 index 0000000..9a611a9 Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/launcher.png differ diff --git a/s4e-icons/assets/walkie_talkie/launcher1.png b/s4e-icons/assets/walkie_talkie/launcher1.png new file mode 100644 index 0000000..60aa469 Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/launcher1.png differ diff --git a/s4e-icons/assets/walkie_talkie/listen.png b/s4e-icons/assets/walkie_talkie/listen.png new file mode 100644 index 0000000..e06f591 Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/listen.png differ diff --git a/s4e-icons/assets/walkie_talkie/login_success.png b/s4e-icons/assets/walkie_talkie/login_success.png new file mode 100644 index 0000000..6e8c826 Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/login_success.png differ diff --git a/s4e-icons/assets/walkie_talkie/next_btn.png b/s4e-icons/assets/walkie_talkie/next_btn.png new file mode 100644 index 0000000..415abc0 Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/next_btn.png differ diff --git a/s4e-icons/assets/walkie_talkie/poc/default_icon.png b/s4e-icons/assets/walkie_talkie/poc/default_icon.png new file mode 100644 index 0000000..d5c33f0 Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/poc/default_icon.png differ diff --git a/s4e-icons/assets/walkie_talkie/poc/group_icon1.png b/s4e-icons/assets/walkie_talkie/poc/group_icon1.png new file mode 100644 index 0000000..9f22404 Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/poc/group_icon1.png differ diff --git a/s4e-icons/assets/walkie_talkie/poc/group_icon2.png b/s4e-icons/assets/walkie_talkie/poc/group_icon2.png new file mode 100644 index 0000000..0adef53 Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/poc/group_icon2.png differ diff --git a/s4e-icons/assets/walkie_talkie/poc/group_icon3.png b/s4e-icons/assets/walkie_talkie/poc/group_icon3.png new file mode 100644 index 0000000..4f1560b Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/poc/group_icon3.png differ diff --git a/s4e-icons/assets/walkie_talkie/poc/group_icon4.png b/s4e-icons/assets/walkie_talkie/poc/group_icon4.png new file mode 100644 index 0000000..5bb064a Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/poc/group_icon4.png differ diff --git a/s4e-icons/assets/walkie_talkie/poc/group_icon5.png b/s4e-icons/assets/walkie_talkie/poc/group_icon5.png new file mode 100644 index 0000000..abd170d Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/poc/group_icon5.png differ diff --git a/s4e-icons/assets/walkie_talkie/poc/member_icon.png b/s4e-icons/assets/walkie_talkie/poc/member_icon.png new file mode 100644 index 0000000..d5c33f0 Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/poc/member_icon.png differ diff --git a/s4e-icons/assets/walkie_talkie/poc/no_sound.png b/s4e-icons/assets/walkie_talkie/poc/no_sound.png new file mode 100644 index 0000000..2244475 Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/poc/no_sound.png differ diff --git a/s4e-icons/assets/walkie_talkie/poc/say_button_mic_icon.png b/s4e-icons/assets/walkie_talkie/poc/say_button_mic_icon.png new file mode 100644 index 0000000..64e5ea1 Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/poc/say_button_mic_icon.png differ diff --git a/s4e-icons/assets/walkie_talkie/poc/sound.png b/s4e-icons/assets/walkie_talkie/poc/sound.png new file mode 100644 index 0000000..9b7b453 Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/poc/sound.png differ diff --git a/s4e-icons/assets/walkie_talkie/poc/talk_listen.png b/s4e-icons/assets/walkie_talkie/poc/talk_listen.png new file mode 100644 index 0000000..c9a9688 Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/poc/talk_listen.png differ diff --git a/s4e-icons/assets/walkie_talkie/poc/toast_listen.png b/s4e-icons/assets/walkie_talkie/poc/toast_listen.png new file mode 100644 index 0000000..c9a9688 Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/poc/toast_listen.png differ diff --git a/s4e-icons/assets/walkie_talkie/poc/toast_say.png b/s4e-icons/assets/walkie_talkie/poc/toast_say.png new file mode 100644 index 0000000..f5b5dec Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/poc/toast_say.png differ diff --git a/s4e-icons/assets/walkie_talkie/poc/tools.png b/s4e-icons/assets/walkie_talkie/poc/tools.png new file mode 100644 index 0000000..e7cf9be Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/poc/tools.png differ diff --git a/s4e-icons/assets/walkie_talkie/poc/voice_icon.png b/s4e-icons/assets/walkie_talkie/poc/voice_icon.png new file mode 100644 index 0000000..85e366d Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/poc/voice_icon.png differ diff --git a/s4e-icons/assets/walkie_talkie/poc/voice_icon_rt.png b/s4e-icons/assets/walkie_talkie/poc/voice_icon_rt.png new file mode 100644 index 0000000..501320a Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/poc/voice_icon_rt.png differ diff --git a/s4e-icons/assets/walkie_talkie/speak.png b/s4e-icons/assets/walkie_talkie/speak.png new file mode 100644 index 0000000..1b715e3 Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/speak.png differ diff --git a/s4e-icons/assets/walkie_talkie/walkie_talkie.png b/s4e-icons/assets/walkie_talkie/walkie_talkie.png new file mode 100644 index 0000000..9a611a9 Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/walkie_talkie.png differ diff --git a/s4e-icons/assets/walkie_talkie/walkie_toast_icon.png b/s4e-icons/assets/walkie_talkie/walkie_toast_icon.png new file mode 100644 index 0000000..81f4e3f Binary files /dev/null and b/s4e-icons/assets/walkie_talkie/walkie_toast_icon.png differ diff --git a/s4e-icons/blob/offsets.json b/s4e-icons/blob/offsets.json new file mode 100644 index 0000000..0cfc9a0 --- /dev/null +++ b/s4e-icons/blob/offsets.json @@ -0,0 +1,914 @@ +{ + "magic_len": 15, + "magic_rb_len": 15, + "records": [ + { + "name": "canary_superblock", + "rel": 0, + "target": 0, + "length": 32 + }, + { + "name": "activities", + "rel": 32, + "target": 265606144, + "length": 21772 + }, + { + "name": "alarm", + "rel": 21804, + "target": 255491584, + "length": 82956 + }, + { + "name": "barometer", + "rel": 104760, + "target": 243424256, + "length": 82956 + }, + { + "name": "breath", + "rel": 187716, + "target": 198528000, + "length": 82956 + }, + { + "name": "calendar", + "rel": 270672, + "target": 197440000, + "length": 82956 + }, + { + "name": "chronograph", + "rel": 353628, + "target": 195329536, + "length": 82956 + }, + { + "name": "compass", + "rel": 436584, + "target": 193598464, + "length": 21772 + }, + { + "name": "debug", + "rel": 458356, + "target": 193044992, + "length": 21772 + }, + { + "name": "esimsms", + "rel": 480128, + "target": 192291328, + "length": 82956 + }, + { + "name": "find_phone", + "rel": 563084, + "target": 191823872, + "length": 82956 + }, + { + "name": "flashlight", + "rel": 646040, + "target": 191740416, + "length": 82956 + }, + { + "name": "health_today", + "rel": 728996, + "target": 170015232, + "length": 82956 + }, + { + "name": "heartrate", + "rel": 811952, + "target": 167684096, + "length": 82956 + }, + { + "name": "innovation_research", + "rel": 894908, + "target": 158267904, + "length": 82956 + }, + { + "name": "interconnect", + "rel": 977864, + "target": 158171136, + "length": 82956 + }, + { + "name": "lua", + "rel": 1060820, + "target": 158028288, + "length": 21772 + }, + { + "name": "media", + "rel": 1082592, + "target": 157868544, + "length": 82956 + }, + { + "name": "mijia", + "rel": 1165548, + "target": 156865024, + "length": 82956 + }, + { + "name": "nfccard", + "rel": 1248504, + "target": 155134976, + "length": 82956 + }, + { + "name": "oxygen", + "rel": 1331460, + "target": 149982208, + "length": 82956 + }, + { + "name": "perpetual_calendar", + "rel": 1414416, + "target": 129220096, + "length": 21772 + }, + { + "name": "phone", + "rel": 1436188, + "target": 128241664, + "length": 82956 + }, + { + "name": "phone_contacts", + "rel": 1519144, + "target": 128459264, + "length": 82956 + }, + { + "name": "pressure", + "rel": 1602100, + "target": 126911488, + "length": 82956 + }, + { + "name": "recorder", + "rel": 1685056, + "target": 108676608, + "length": 82956 + }, + { + "name": "remote_camera", + "rel": 1768012, + "target": 106510848, + "length": 21772 + }, + { + "name": "settings", + "rel": 1789784, + "target": 86933504, + "length": 82956 + }, + { + "name": "sleep", + "rel": 1872740, + "target": 85287936, + "length": 82956 + }, + { + "name": "sports", + "rel": 1955696, + "target": 79359488, + "length": 82956 + }, + { + "name": "sports_course", + "rel": 2038652, + "target": 85140480, + "length": 82956 + }, + { + "name": "sports_record", + "rel": 2121608, + "target": 78000128, + "length": 82956 + }, + { + "name": "sports_training", + "rel": 2204564, + "target": 73838592, + "length": 82956 + }, + { + "name": "timer", + "rel": 2287520, + "target": 61684736, + "length": 82956 + }, + { + "name": "todolist", + "rel": 2370476, + "target": 61509632, + "length": 82956 + }, + { + "name": "voice_aivs", + "rel": 2453432, + "target": 54641664, + "length": 82956 + }, + { + "name": "weather", + "rel": 2536388, + "target": 25027072, + "length": 82956 + }, + { + "name": "womenhealth", + "rel": 2619344, + "target": 293376, + "length": 82956 + }, + { + "name": "cc_add", + "rel": 2702300, + "target": 193594368, + "length": 3340 + }, + { + "name": "cc_air_mode_close", + "rel": 2705640, + "target": 193576448, + "length": 17420 + }, + { + "name": "cc_air_mode_conn", + "rel": 2723060, + "target": 193572864, + "length": 3340 + }, + { + "name": "cc_air_mode_open", + "rel": 2726400, + "target": 193537024, + "length": 17420 + }, + { + "name": "cc_battery_close", + "rel": 2743820, + "target": 193519104, + "length": 17420 + }, + { + "name": "cc_battery_open", + "rel": 2761240, + "target": 193501184, + "length": 17420 + }, + { + "name": "cc_bt_connect", + "rel": 2778660, + "target": 193497600, + "length": 3340 + }, + { + "name": "cc_bt_disconnect", + "rel": 2782000, + "target": 193494016, + "length": 3340 + }, + { + "name": "cc_cancel", + "rel": 2785340, + "target": 193490432, + "length": 3340 + }, + { + "name": "cc_drain_close", + "rel": 2788680, + "target": 193472512, + "length": 17420 + }, + { + "name": "cc_earphone", + "rel": 2806100, + "target": 193413632, + "length": 3340 + }, + { + "name": "cc_find_phone_close", + "rel": 2809440, + "target": 193395712, + "length": 17420 + }, + { + "name": "cc_flashlight", + "rel": 2826860, + "target": 193359872, + "length": 17420 + }, + { + "name": "cc_mobile_network_close", + "rel": 2844280, + "target": 193338368, + "length": 17420 + }, + { + "name": "cc_mobile_network_open", + "rel": 2861700, + "target": 193302528, + "length": 17420 + }, + { + "name": "cc_mute_close", + "rel": 2879120, + "target": 193284608, + "length": 17420 + }, + { + "name": "cc_mute_open", + "rel": 2896540, + "target": 193266688, + "length": 17420 + }, + { + "name": "cc_not_disturb_close", + "rel": 2913960, + "target": 193205760, + "length": 17420 + }, + { + "name": "cc_not_disturb_open", + "rel": 2931380, + "target": 193187840, + "length": 17420 + }, + { + "name": "cc_raise_bright_close", + "rel": 2948800, + "target": 193152000, + "length": 17420 + }, + { + "name": "cc_raise_bright_open", + "rel": 2966220, + "target": 193134080, + "length": 17420 + }, + { + "name": "cc_settings", + "rel": 2983640, + "target": 193116160, + "length": 17420 + }, + { + "name": "sys_lock_icon", + "rel": 3001060, + "target": 62331392, + "length": 2060 + }, + { + "name": "sys_system_lock", + "rel": 3003120, + "target": 194351104, + "length": 3340 + }, + { + "name": "sys_quiet_mode", + "rel": 3006460, + "target": 194466304, + "length": 3340 + }, + { + "name": "sys_no_message", + "rel": 3009800, + "target": 150767104, + "length": 7436 + }, + { + "name": "sys_notify", + "rel": 3017236, + "target": 87050752, + "length": 5132 + }, + { + "name": "sys_esim_sms_reminder", + "rel": 3022368, + "target": 192435200, + "length": 7436 + }, + { + "name": "sys_enable_donot_disturb", + "rel": 3029804, + "target": 156976640, + "length": 5132 + }, + { + "name": "sys_disable_donot_disturb", + "rel": 3034936, + "target": 156989952, + "length": 5132 + }, + { + "name": "sys_presure_relax", + "rel": 3040068, + "target": 127373312, + "length": 3340 + }, + { + "name": "sys_presure_mild", + "rel": 3043408, + "target": 127390208, + "length": 3340 + }, + { + "name": "sys_presure_mid", + "rel": 3046748, + "target": 127407104, + "length": 3340 + }, + { + "name": "sys_presure_severe", + "rel": 3050088, + "target": 127356416, + "length": 3340 + }, + { + "name": "sys_presure_mild32", + "rel": 3053428, + "target": 127387648, + "length": 2060 + }, + { + "name": "sys_presure_mid32", + "rel": 3055488, + "target": 127404544, + "length": 2060 + }, + { + "name": "sys_presure_severe32", + "rel": 3057548, + "target": 127353856, + "length": 2060 + }, + { + "name": "sys_presure_icon", + "rel": 3059608, + "target": 127410688, + "length": 3340 + }, + { + "name": "sys_press_grade1", + "rel": 3062948, + "target": 128046592, + "length": 2060 + }, + { + "name": "sys_press_grade2", + "rel": 3065008, + "target": 128044032, + "length": 2060 + }, + { + "name": "sys_press_grade3", + "rel": 3067068, + "target": 128041472, + "length": 2060 + }, + { + "name": "sys_press_grade4", + "rel": 3069128, + "target": 128038912, + "length": 2060 + }, + { + "name": "cc_network_4g", + "rel": 3071188, + "target": 193241600, + "length": 3340 + }, + { + "name": "cc_network_3g", + "rel": 3074528, + "target": 193263104, + "length": 3340 + }, + { + "name": "cc_signal_strength", + "rel": 3077868, + "target": 193112576, + "length": 3340 + }, + { + "name": "cc_signal_strength1", + "rel": 3081208, + "target": 193108992, + "length": 3340 + }, + { + "name": "cc_signal_strength2", + "rel": 3084548, + "target": 193105408, + "length": 3340 + }, + { + "name": "cc_signal_strength3", + "rel": 3087888, + "target": 193101824, + "length": 3340 + }, + { + "name": "cc_signal_strength4", + "rel": 3091228, + "target": 193098240, + "length": 3340 + }, + { + "name": "cc_location", + "rel": 3094568, + "target": 193356288, + "length": 3340 + }, + { + "name": "cc_network_3g_signal_1", + "rel": 3097908, + "target": 193255936, + "length": 3340 + }, + { + "name": "cc_network_3g_signal_2", + "rel": 3101248, + "target": 193252352, + "length": 3340 + }, + { + "name": "cc_network_3g_signal_3", + "rel": 3104588, + "target": 193248768, + "length": 3340 + }, + { + "name": "cc_network_3g_signal_4", + "rel": 3107928, + "target": 193245184, + "length": 3340 + }, + { + "name": "cc_network_3g_no_signal", + "rel": 3111268, + "target": 193259520, + "length": 3340 + }, + { + "name": "cc_network_4g_signal_1", + "rel": 3114608, + "target": 193234432, + "length": 3340 + }, + { + "name": "cc_network_4g_signal_2", + "rel": 3117948, + "target": 193230848, + "length": 3340 + }, + { + "name": "cc_network_4g_signal_3", + "rel": 3121288, + "target": 193227264, + "length": 3340 + }, + { + "name": "cc_network_4g_signal_4", + "rel": 3124628, + "target": 193223680, + "length": 3340 + }, + { + "name": "cc_network_4g_no_signal", + "rel": 3127968, + "target": 193238016, + "length": 3340 + }, + { + "name": "sys_notifications_car", + "rel": 3131308, + "target": 150829568, + "length": 5260 + }, + { + "name": "sys_notifications_default", + "rel": 3136568, + "target": 150794752, + "length": 32012 + }, + { + "name": "sys_notifications_default_small", + "rel": 3168580, + "target": 150791168, + "length": 3340 + }, + { + "name": "sys_notifications_half_going", + "rel": 3171920, + "target": 150776320, + "length": 3052 + }, + { + "name": "sys_notifications_half_point", + "rel": 3174972, + "target": 150774784, + "length": 1292 + }, + { + "name": "sys_notifications_no_message", + "rel": 3176264, + "target": 150767104, + "length": 7436 + }, + { + "name": "sys_phone_icon_app_h_w_48", + "rel": 3183700, + "target": 128450048, + "length": 3340 + }, + { + "name": "sys_phone_icon_dial", + "rel": 3187040, + "target": 128446464, + "length": 3340 + }, + { + "name": "sys_phone_icon_hash", + "rel": 3190380, + "target": 128442880, + "length": 3340 + }, + { + "name": "sys_phone_icon_headphone_icon", + "rel": 3193720, + "target": 128437248, + "length": 5132 + }, + { + "name": "sys_phone_icon_keypad_icon", + "rel": 3198852, + "target": 128425984, + "length": 5132 + }, + { + "name": "sys_phone_icon_mic_off", + "rel": 3203984, + "target": 128389632, + "length": 3340 + }, + { + "name": "sys_phone_icon_mic_on", + "rel": 3207324, + "target": 128386048, + "length": 3340 + }, + { + "name": "sys_phone_icon_no_calllog", + "rel": 3210664, + "target": 128378368, + "length": 7436 + }, + { + "name": "sys_phone_icon_no_contacts", + "rel": 3218100, + "target": 128370688, + "length": 7436 + }, + { + "name": "sys_phone_icon_phone_icon", + "rel": 3225536, + "target": 128361472, + "length": 5132 + }, + { + "name": "sys_phone_icon_phone_icon_small", + "rel": 3230668, + "target": 128358912, + "length": 2060 + }, + { + "name": "sys_phone_icon_plus", + "rel": 3232728, + "target": 128355328, + "length": 3340 + }, + { + "name": "sys_phone_icon_spk_icon_64", + "rel": 3236068, + "target": 128336896, + "length": 5132 + }, + { + "name": "sys_phone_icon_star", + "rel": 3241200, + "target": 128333312, + "length": 3340 + }, + { + "name": "sys_phone_icon_watch_icon", + "rel": 3244540, + "target": 128327680, + "length": 5132 + }, + { + "name": "sys_phone_icon_watch_icon_small", + "rel": 3249672, + "target": 128325120, + "length": 2060 + }, + { + "name": "sys_phone_widget_widget_contact", + "rel": 3251732, + "target": 128106496, + "length": 13580 + }, + { + "name": "sys_phone_widget_widget_dialpad", + "rel": 3265312, + "target": 128092672, + "length": 13580 + }, + { + "name": "sys_phone_widget_widget_phone", + "rel": 3278892, + "target": 128078848, + "length": 13580 + }, + { + "name": "sys_recorder_ahead3", + "rel": 3292472, + "target": 108767744, + "length": 7436 + }, + { + "name": "sys_recorder_back3", + "rel": 3299908, + "target": 108760064, + "length": 7436 + }, + { + "name": "sys_recorder_main", + "rel": 3307344, + "target": 108673024, + "length": 3340 + }, + { + "name": "sys_recorder_mark", + "rel": 3310684, + "target": 108669440, + "length": 3340 + }, + { + "name": "sys_recorder_mute", + "rel": 3314024, + "target": 108662272, + "length": 3340 + }, + { + "name": "sys_recorder_sound", + "rel": 3317364, + "target": 108655104, + "length": 3340 + }, + { + "name": "sys_recorder_trash", + "rel": 3320704, + "target": 108608512, + "length": 3340 + }, + { + "name": "sys_recorder_warning", + "rel": 3324044, + "target": 108600832, + "length": 7436 + }, + { + "name": "sys_timer_icon_close", + "rel": 3331480, + "target": 62305792, + "length": 3340 + }, + { + "name": "sys_timer_icon_pause", + "rel": 3334820, + "target": 62298624, + "length": 3340 + }, + { + "name": "sys_timer_icon_restart", + "rel": 3338160, + "target": 62295040, + "length": 3340 + }, + { + "name": "sys_timer_icon_start", + "rel": 3341500, + "target": 62025216, + "length": 3340 + }, + { + "name": "sys_timer_icon_stop", + "rel": 3344840, + "target": 62021632, + "length": 3340 + }, + { + "name": "sys_timer_icon_widget_light12", + "rel": 3348180, + "target": 61896704, + "length": 120844 + }, + { + "name": "sys_timer_icon_widget_light12_five", + "rel": 3469024, + "target": 61775360, + "length": 120844 + }, + { + "name": "sys_timer_icon_widget_pause", + "rel": 3589868, + "target": 61771776, + "length": 3340 + }, + { + "name": "sys_timer_icon_widget_start", + "rel": 3593208, + "target": 61768192, + "length": 3340 + }, + { + "name": "sys_todolist_checked", + "rel": 3596548, + "target": 61681152, + "length": 3340 + }, + { + "name": "sys_todolist_create_todo_btn", + "rel": 3599888, + "target": 61664256, + "length": 16396 + }, + { + "name": "sys_todolist_dark12_bg", + "rel": 3616284, + "target": 61600768, + "length": 62988 + }, + { + "name": "sys_todolist_empty", + "rel": 3679272, + "target": 61593088, + "length": 7436 + }, + { + "name": "sys_todolist_reminder_big", + "rel": 3686708, + "target": 61491712, + "length": 17420 + }, + { + "name": "sys_todolist_reminder_small", + "rel": 3704128, + "target": 61484032, + "length": 7436 + }, + { + "name": "sys_todolist_tdl_dark11_bg", + "rel": 3711564, + "target": 61451776, + "length": 32012 + }, + { + "name": "sys_todolist_tdl_light11_bg", + "rel": 3743576, + "target": 61419520, + "length": 32012 + }, + { + "name": "sys_todolist_unchecked", + "rel": 3775588, + "target": 61415936, + "length": 3340 + }, + { + "name": "sys_todolist_widget_dark_icon", + "rel": 3778928, + "target": 61408256, + "length": 7436 + }, + { + "name": "sys_todolist_widget_light_icon", + "rel": 3786364, + "target": 61400576, + "length": 7436 + } + ], + "pay_bytes": 3793815, + "rb_bytes": 3793815 +} \ No newline at end of file diff --git a/s4e-icons/blob/payload.bin b/s4e-icons/blob/payload.bin new file mode 100644 index 0000000..1fc543c Binary files /dev/null and b/s4e-icons/blob/payload.bin differ diff --git a/s4e-icons/blob/rollback.bin b/s4e-icons/blob/rollback.bin new file mode 100644 index 0000000..6e17315 Binary files /dev/null and b/s4e-icons/blob/rollback.bin differ diff --git a/s4e-icons/dist/S5Icons.face b/s4e-icons/dist/S5Icons.face new file mode 100644 index 0000000..feba5fd Binary files /dev/null and b/s4e-icons/dist/S5Icons.face differ diff --git a/s4e-icons/dist/S5Icons_toolchain.face b/s4e-icons/dist/S5Icons_toolchain.face new file mode 100644 index 0000000..d167892 Binary files /dev/null and b/s4e-icons/dist/S5Icons_toolchain.face differ diff --git a/s4e-icons/docs/all_icons_compare.png b/s4e-icons/docs/all_icons_compare.png new file mode 100644 index 0000000..1c63e4c Binary files /dev/null and b/s4e-icons/docs/all_icons_compare.png differ diff --git a/s4e-icons/docs/s4e_pressure.png b/s4e-icons/docs/s4e_pressure.png new file mode 100644 index 0000000..deceeb2 Binary files /dev/null and b/s4e-icons/docs/s4e_pressure.png differ diff --git a/s4e-icons/docs/s4e_sysicons.png b/s4e-icons/docs/s4e_sysicons.png new file mode 100644 index 0000000..ee9510f Binary files /dev/null and b/s4e-icons/docs/s4e_sysicons.png differ diff --git a/s4e-icons/docs/ui_all_466.png b/s4e-icons/docs/ui_all_466.png new file mode 100644 index 0000000..e927c4c Binary files /dev/null and b/s4e-icons/docs/ui_all_466.png differ diff --git a/s4e-icons/docs/分析报告-S4eSIM.md b/s4e-icons/docs/分析报告-S4eSIM.md new file mode 100644 index 0000000..8c95442 --- /dev/null +++ b/s4e-icons/docs/分析报告-S4eSIM.md @@ -0,0 +1,272 @@ +> 本文是当时的分析 / 交付存档,路径按当时的工作目录写。 +> 本项目里的对应关系:tools 对应 _tools,inputs/target/vela_app.bin 对应 _x/vela_app.bin, +> inputs/source 对应 _s5,.work 对应 _out(产物),dist 对应 _out/faces。 + +# Xiaomi Watch S4 eSIM(o62lte)系统应用图标替换 —— 固件包分析报告 + +分析对象:`c53475a6c7367c3583c6882665fd85ae_upd_mijia.watch.o62lte.bin` +固件标识:`ro.build.version=3.202.79, build.time=2026-07-03 19:58:07, model=o62lte` +机型字符串:`Xiaomi Watch S4 eSIM` +包大小:199,385,001 字节 + +--- + +## 1. 固件包是什么 + +外层就是一个 **ZIP(Android SignApk 签名包)**,开头即 `PK\x03\x04`,第一个条目是 `ota.sh`。 + +``` +ota.sh 4524 +vela_app.bin 265684992 <- 应用资源分区(图标在这里) +vela_watchface.bin 92967936 +vela_system.bin 72366080 +vela_font.bin 36146176 +vela_misc.bin 35799040 +vela_vendor.bin 17909760 +vela_ap.bin 11141120 +vela_quickapp.bin 5840896 +vela_i18n.bin 4399104 +vela_cp.bin 3174400 +vela_audio.bin 2730024 +vela_ota.bin 1441792 +vela_tee.bin 1048576 +META-INF/CERT.RSA / CERT.SF / MANIFEST.MF +``` + +`ota.sh` 的全部动作就是逐分区裸写: + +```sh +dd if=/ota/vela_app.bin of=/dev/app bs=32768 verify +dd if=/ota/vela_vendor.bin of=/dev/vendor bs=32768 verify +dd if=/ota/vela_system.bin of=/dev/system bs=32768 verify +... (vendor/i18n/app/quickapp/cp/misc/audio/tee/system/font/ap/watchface) +``` + +签名信息:`CERT.SF` 里 `Created-By: 1.0 (Android SignApk)`、`X-Android-APK-Signed: 2, 3`,`MANIFEST.MF` 对每个分区镜像记录 SHA1。 + +> **结论:这份 OTA 包无法自行重新打包。** 改一个字节就过不了签名校验。 + +`vela_*.bin` 是 **Linux `romfs` 镜像**(magic `-rom1fs-`): + +| 镜像 | 卷名 | 条目数 | romfs 大小 | +|---|---|---|---| +| vela_app.bin | app | 7834 | 265,684,928 | +| vela_watchface.bin | watchface | 312 | 92,967,424 | +| vela_system.bin | system | 1525 | 72,365,728 | +| vela_font.bin | font | 15 | 36,145,776 | +| vela_misc.bin | mcmisc | 1341 | 35,798,176 | +| vela_vendor.bin | vendor | 58 | 17,909,328 | +| vela_quickapp.bin | quickapp | 46 | 5,840,544 | +| vela_i18n.bin | i18n | 277 | 4,399,072 | + +在盘上它们挂到 `/resource/<卷名>`,所以 AP 二进制里出现的是 `/resource/app/...` 这样的绝对路径。 + +### ROMFS 结构(关键) + +``` +super : "-rom1fs-" | u32 size | u32 checksum | name(NUL, 补齐16) +inode : u32 next | u32 spec | u32 size | u32 checksum | name(NUL, 补齐16) | 文件数据紧跟其后 + next 低 4 位 = 类型:0=硬链接(spec=目标inode偏移) 1=目录(spec=首个子项偏移) 2=普通文件 8=可执行 +``` + +**目录项与文件数据是严格顺序紧密排布的**,每一项的偏移完全由它前面所有内容的长度决定。 + +--- + +## 2. 系统自带应用的图标在哪里(已定位) + +AP 二进制里写死了一批路径,直接给出答案: + +``` +/resource/app/alarm/launcher.bin +/resource/app/activities/launcher.bin +/resource/app/calendar/launcher.bin +/resource/app/heartrate/launcher.bin +... 共 39 个 +``` + +精确统计:**38 个 `/launcher.bin`** 加 **1 个 `phone/contacts_launcher.bin`**(联系人图标),共 39 个。 + +即 **每个系统应用目录下都有一个 `launcher.bin`,它就是"应用列表/启动器"里显示的那个图标**。全部位于 `vela_app.bin`(=`/dev/app` 分区,265 MB)。 + +39 个图标全部是 **LVGL v9 二进制图片**: + +``` +u8 magic=0x19 | u8 color_format | u16 flags | u16 w | u16 h | u16 stride | u16 reserved +cf=0x10 ARGB8888 : 12 + 144*144*4 = 82956 字节 +cf=0x0a I8 : 12 + 256*4 + 144*144 = 21772 字节 +``` + +已全部解码验证(38/39,`voice/alexa/launcher.bin` 是 6005 字节的特例,`flags=0x0008`,格式待定)。 + +### 图标清单(偏移为 `vela_app.bin` 内绝对偏移,固件 3.202.79 专用) + +| 应用 | 数据偏移 | 长度 | 尺寸 | 格式 | +|---|---|---|---|---| +| wxpay | 0x0000bc00 | 21772 | 144x144 | I8 | +| womenhealth | 0x00047a00 | 82956 | 144x144 | ARGB8888 | +| weather | 0x017de200 | 82956 | 144x144 | ARGB8888 | +| walkie_talkie | 0x02d45600 | 82956 | 144x144 | ARGB8888 | +| voice/alexa | 0x032fa000 | 6005 | 144x144 | I8(特例) | +| voice/aivs | 0x0341c400 | 82956 | 144x144 | ARGB8888 | +| todolist | 0x03aa9000 | 82956 | 144x144 | ARGB8888 | +| timer | 0x03ad3c00 | 82956 | 144x144 | ARGB8888 | +| sports/training | 0x0466b000 | 82956 | 144x144 | ARGB8888 | +| sports/record | 0x04a63000 | 82956 | 144x144 | ARGB8888 | +| sports | 0x04baee00 | 82956 | 144x144 | ARGB8888 | +| sports/course | 0x05132400 | 82956 | 144x144 | ARGB8888 | +| sleep | 0x05156400 | 82956 | 144x144 | ARGB8888 | +| settings | 0x052e8000 | 82956 | 144x144 | ARGB8888 | +| remote_camera | 0x06593a00 | 21772 | 144x144 | I8 | +| recorder | 0x067a4600 | 82956 | 144x144 | ARGB8888 | +| pressure | 0x07908400 | 82956 | 144x144 | ARGB8888 | +| phone | 0x07a4d000 | 82956 | 144x144 | ARGB8888 | +| phone/contacts_launcher | 0x07a82200 | 82956 | 144x144 | ARGB8888 | +| perpetual_calendar | 0x07b3be00 | 21772 | 144x144 | I8 | +| oxygen | 0x08f08c00 | 82956 | 144x144 | ARGB8888 | +| nfccard | 0x093f2c00 | 82956 | 144x144 | ARGB8888 | +| mijia | 0x09599200 | 82956 | 144x144 | ARGB8888 | +| media | 0x0968e200 | 82956 | 144x144 | ARGB8888 | +| lua | 0x096b5200 | 21772 | 144x144 | I8 | +| interconnect | 0x096d8000 | 82956 | 144x144 | ARGB8888 | +| innovation_research | 0x096efa00 | 82956 | 144x144 | ARGB8888 | +| heartrate | 0x09fea800 | 82956 | 144x144 | ARGB8888 | +| health_today | 0x0a223a00 | 82956 | 144x144 | ARGB8888 | +| flashlight | 0x0b6dba00 | 82956 | 144x144 | ARGB8888 | +| find_phone | 0x0b6f0000 | 82956 | 144x144 | ARGB8888 | +| esimsms | 0x0b762200 | 82956 | 144x144 | ARGB8888 | +| debug | 0x0b81a200 | 21772 | 144x144 | I8 | +| compass | 0x0b8a1400 | 21772 | 144x144 | I8 | +| chronograph | 0x0ba47e00 | 82956 | 144x144 | ARGB8888 | +| calendar | 0x0bc4b200 | 82956 | 144x144 | ARGB8888 | +| breath | 0x0bd54c00 | 82956 | 144x144 | ARGB8888 | +| barometer | 0x0e825c00 | 82956 | 144x144 | ARGB8888 | +| alarm | 0x0f3a7e00 | 82956 | 144x144 | ARGB8888 | + +--- + +## 3. 疑问一:为什么"一个 Lua 表盘点一下就替换成功了" + +从这份固件包里能**确证**的事实: + +1. 系统应用图标 = `vela_app.bin` 里的 `launcher.bin`,而 `vela_app.bin` 是**整个 265 MB 分区镜像**; +2. 官方通道 `ota.sh` 的写入方式是 `dd ... of=/dev/app`,**裸写分区**; +3. 官方 OTA 包是 **SignApk v2/v3 签名的**,因此**不可能**通过"重新打包 OTA"来替换图标; +4. 分区设备节点 `/dev/app`、`/dev/system` 等对用户态是**可打开的字符/块设备**(LoXe 文档也专门警告"不要直接试写未知设备,对分区节点 `dd`/重定向可能立即破坏系统")。 + +把这四点连起来,社区图标包只能走一条路:**绕过签名校验,直接把图标字节写进分区**。 + +所以那个"Lua 表盘"的角色是**运载 + 执行载体**,而不是"表盘 API 能设置图标": + +``` +图标包(做成表盘包的样子) +├── asset/*.bin 实际是一个"补丁表":[(绝对偏移, 该长度字节), ...] +└── main.lua ui.init / 按钮回调里做写入 + │ 点击"立即替换" + ▼ + 按记录 seek+write 到 /dev/app(或整镜像 dd) + ▼ + ROMFS 是只读挂载 + 有页缓存 → 必须重启才能重新读取 + ▼ + 重启后应用列表图标变了 +``` + +**"稍等片刻 + 重启"正是这条链路的指纹**:如果只是普通文件级改动,不需要重启;裸写分区必须重启,因为已经挂载的只读视图和缓存都是旧的。 + +> 这一节里,(1)(2)(3)(4) 是本次从固件包**实证**的;"表盘脚本直接写 `/dev/app`" 是**最符合全部观察的推断**,需要用那个图标包本身(看 `main.lua` 是否出现 `lvgl.fs.open_file`+`/dev/app`+`write`+`seek`)或实机日志来最终确认。 +> 另外一种可能是:表盘只是商店分类/容器格式,真正推送由工具箱的安装通道完成。区分方法同上——看包里有没有 Lua 写盘代码,以及 payload 是"几十 KB 的补丁表"还是"75 MB 的整分区镜像"。 + +--- + +## 4. 疑问二:为什么"只适用于特定固件版本"(例如 3.100.032) + +先纠正一点:**锁版本的不是工具,是包里的那份 payload。** AstroBox 这类工具箱本身跨版本通用;写着"仅适用于 3.100.032"的是那份资源包。 + +原因有四层,前两层是技术必然,后两层是工程现实: + +**① 绝对寻址 + 顺序排布(决定性原因)** +ROMFS 把所有 inode、文件名和文件数据**首尾相接顺序排布**。补丁表里存的是**绝对偏移**(例如 `0x0f3a7e00`)。只要前面对任何一个文件的长度变了 —— 新增一个图标、换一个字体、改一句文案、某个 `.bin` 从 I8 变 ARGB8888(长度差 61184 字节)—— **后面所有偏移全部平移**。此时旧补丁会把图标字节写进别的文件里,轻则图标错乱,重则损坏资源分区。 + +这跟"为什么这个 APK 版本的汉化补丁不能用到下一个版本"是同一类问题:**不是逻辑变了,是逐字节布局变了。** + +**② 一致性** +资源分区与 AP/system 分区是配套发布的(资源清单、图标索引、字体/i18n 版本互相对应)。混搭会出现显示错乱,严重时起不来。所以包作者干脆要求"先降级到配套版本"。 + +**③ 官方 OTA 会整分区覆盖** +任何系统升级都执行 `dd ... of=/dev/app`,**你的图标会被整体重写回去**。所以"锁版本"同时意味着"OTA 之后失效";如果升级后还拿旧偏移去写,就是往错误位置写数据。 + +**④ 作者的取材边界** +那个包和它同一作者发布的[小米手环11固件](https://www.bandbbs.cn/resources/7955/)、[红米手表6固件](https://www.bandbbs.cn/resources/7243/)放在一起 —— 他是做固件 dump 的人,只为"手上有 dump 的那个版本"做偏移表。另外那份 S4 41mm 的包标的是 `3.100.032`,而你这台 eSIM 是 `3.202.79`,本来就不是同一条构建线。 + +**所以"难道每个版本都有不可替代的变化吗"——不是。** 是"按绝对偏移硬写"这个做法天生不能跨版本。**换一种做法就可以跨版本**(见下一节)。 + +--- + +## 5. 更好的做法:不重建 ROMFS,按名字原地替换(已实现并验证) + +因为 144×144 的图标换成另一个 144×144、同色彩格式的图标时,**字节长度完全相同**,所以可以: + +> **在设备上解析 ROMFS → 按路径找到 `/launcher.bin` → 用等长的新图标覆盖它的数据 → inode 表、super block、后续所有偏移全部不动。** + +这样: +- 不需要重建 ROMFS,不需要改 `size`/`checksum`/`next`; +- 不需要"每版本一份包"——偏移是**运行时从当前固件里算出来的**,所以**天然跨固件版本**; +- 传输量从"265 MB(或压缩 75 MB)整分区"降到"每个图标 83 KB / 22 KB"。 + +### 已验证的实现 + +工具:`tools/romfs.py`(ROMFS 解析/遍历)、`tools/iconpack.py`(make / apply / list)、`tools/render_icons.py`(解码为 PNG)。 + +验证结果: + +| 验证项 | 结果 | +|---|---| +| ARGB8888 编码器与原固件字节级一致(alarm 图标往返) | ✅ 完全一致(82956 字节) | +| I8 编码器长度一致 + 视觉误差 | ✅ 长度一致,平均绝对误差 0.22/255 | +| 原地替换后条目数 / 路径 / inode 表 / super block / 文件总长 | ✅ 全部**逐位不变**(7834 条,`inode table identical: True`) | +| 内容变化的文件 | ✅ 恰好只有被替换的那几个 `launcher.bin` | + +用 4 个自制图标(alarm / timer / wxpay / settings)跑通了 +`make → apply → 校验 → 回读渲染`全流程,见 `_out/patched_custom.png`。 + +--- + +## 6. 还缺的那一环:写入通道 + +到这里,"**改什么、改成什么、怎么做到版本无关**"都已经解决。**唯一未解决的是"怎么把这几万字节送进 `/dev/app`"**: + +| 方式 | 说明 | 代价 | +|---|---|---| +| 实机 shell `dd` | 最直接,需要能拿到 shell(或 Recovery) | 需要相应工具/权限 | +| 表盘包 + 脚本写分区节点 | 社区现有做法,整链路已由本次分析解释清楚 | 属于滥用未公开行为,OTA 后失效;写错偏移会损坏分区 | +| 重打包 OTA zip | ❌ 签名校验过不去 | 不可行 | + +要在你的机器上落地,下一步需要的是:**在真机上把 `/dev/app`(或至少 ROMFS 头与目标 inode)读出来**,用 `iconpack.py list` 得到你这一版固件的真实偏移,再生成补丁。这样就不再受"包只适用于某某版本"的限制。 + +--- + +## 7. 风险提示 + +- 对**分区节点**写数据是文档明确警告的危险操作;写错偏移会破坏资源分区,可能导致无法开机。 +- 写之前**务必备份**原始分区(至少备份 ROMFS 头 + inode 表 + 你要改的 `launcher.bin` 原字节,能回滚)。 +- 系统 OTA 会覆盖你的修改;跨版本套用旧补丁 = 往错误位置写数据。 +- 修改系统资源分区可能影响保修;请自行确认设备所有权与当地法律。 +- `voice/alexa/launcher.bin`(6005 字节,`flags=0x0008`)格式尚未确定,不要动它。 + +--- + +## 附:工作目录 + +``` +_x/ 解包后的固件(含 13 个分区镜像) +tools/romfs.py ROMFS 解析与遍历 +tools/iconpack.py list / make / apply 图标补丁 +tools/render_icons.py 把 launcher.bin 解码成 PNG +tools/patch_demo.py 原地等长替换的概念验证 +tools/demo_icons.py 编码器自检 + 生成示例图标 +tools/verify_patch.py 补丁后元数据一致性校验 +_out/icons/ 从原厂固件导出的 38 个应用图标 PNG +_out/launcher_icons_sheet.png 全部图标总览 +_out/before_after.png 原地替换前后对比 +_out/patched_custom.png 自制图标写入后的回读结果 +``` diff --git a/s4e-icons/docs/历史-S4eSIM-系统图标替换-分析报告.md b/s4e-icons/docs/历史-S4eSIM-系统图标替换-分析报告.md new file mode 100644 index 0000000..252e810 --- /dev/null +++ b/s4e-icons/docs/历史-S4eSIM-系统图标替换-分析报告.md @@ -0,0 +1,268 @@ +# Xiaomi Watch S4 eSIM(o62lte)系统应用图标替换 —— 固件包分析报告 + +分析对象:`c53475a6c7367c3583c6882665fd85ae_upd_mijia.watch.o62lte.bin` +固件标识:`ro.build.version=3.202.79, build.time=2026-07-03 19:58:07, model=o62lte` +机型字符串:`Xiaomi Watch S4 eSIM` +包大小:199,385,001 字节 + +--- + +## 1. 固件包是什么 + +外层就是一个 **ZIP(Android SignApk 签名包)**,开头即 `PK\x03\x04`,第一个条目是 `ota.sh`。 + +``` +ota.sh 4524 +vela_app.bin 265684992 <- 应用资源分区(图标在这里) +vela_watchface.bin 92967936 +vela_system.bin 72366080 +vela_font.bin 36146176 +vela_misc.bin 35799040 +vela_vendor.bin 17909760 +vela_ap.bin 11141120 +vela_quickapp.bin 5840896 +vela_i18n.bin 4399104 +vela_cp.bin 3174400 +vela_audio.bin 2730024 +vela_ota.bin 1441792 +vela_tee.bin 1048576 +META-INF/CERT.RSA / CERT.SF / MANIFEST.MF +``` + +`ota.sh` 的全部动作就是逐分区裸写: + +```sh +dd if=/ota/vela_app.bin of=/dev/app bs=32768 verify +dd if=/ota/vela_vendor.bin of=/dev/vendor bs=32768 verify +dd if=/ota/vela_system.bin of=/dev/system bs=32768 verify +... (vendor/i18n/app/quickapp/cp/misc/audio/tee/system/font/ap/watchface) +``` + +签名信息:`CERT.SF` 里 `Created-By: 1.0 (Android SignApk)`、`X-Android-APK-Signed: 2, 3`,`MANIFEST.MF` 对每个分区镜像记录 SHA1。 + +> **结论:这份 OTA 包无法自行重新打包。** 改一个字节就过不了签名校验。 + +`vela_*.bin` 是 **Linux `romfs` 镜像**(magic `-rom1fs-`): + +| 镜像 | 卷名 | 条目数 | romfs 大小 | +|---|---|---|---| +| vela_app.bin | app | 7834 | 265,684,928 | +| vela_watchface.bin | watchface | 312 | 92,967,424 | +| vela_system.bin | system | 1525 | 72,365,728 | +| vela_font.bin | font | 15 | 36,145,776 | +| vela_misc.bin | mcmisc | 1341 | 35,798,176 | +| vela_vendor.bin | vendor | 58 | 17,909,328 | +| vela_quickapp.bin | quickapp | 46 | 5,840,544 | +| vela_i18n.bin | i18n | 277 | 4,399,072 | + +在盘上它们挂到 `/resource/<卷名>`,所以 AP 二进制里出现的是 `/resource/app/...` 这样的绝对路径。 + +### ROMFS 结构(关键) + +``` +super : "-rom1fs-" | u32 size | u32 checksum | name(NUL, 补齐16) +inode : u32 next | u32 spec | u32 size | u32 checksum | name(NUL, 补齐16) | 文件数据紧跟其后 + next 低 4 位 = 类型:0=硬链接(spec=目标inode偏移) 1=目录(spec=首个子项偏移) 2=普通文件 8=可执行 +``` + +**目录项与文件数据是严格顺序紧密排布的**,每一项的偏移完全由它前面所有内容的长度决定。 + +--- + +## 2. 系统自带应用的图标在哪里(已定位) + +AP 二进制里写死了一批路径,直接给出答案: + +``` +/resource/app/alarm/launcher.bin +/resource/app/activities/launcher.bin +/resource/app/calendar/launcher.bin +/resource/app/heartrate/launcher.bin +... 共 39 个 +``` + +精确统计:**38 个 `/launcher.bin`** 加 **1 个 `phone/contacts_launcher.bin`**(联系人图标),共 39 个。 + +即 **每个系统应用目录下都有一个 `launcher.bin`,它就是"应用列表/启动器"里显示的那个图标**。全部位于 `vela_app.bin`(=`/dev/app` 分区,265 MB)。 + +39 个图标全部是 **LVGL v9 二进制图片**: + +``` +u8 magic=0x19 | u8 color_format | u16 flags | u16 w | u16 h | u16 stride | u16 reserved +cf=0x10 ARGB8888 : 12 + 144*144*4 = 82956 字节 +cf=0x0a I8 : 12 + 256*4 + 144*144 = 21772 字节 +``` + +已全部解码验证(38/39,`voice/alexa/launcher.bin` 是 6005 字节的特例,`flags=0x0008`,格式待定)。 + +### 图标清单(偏移为 `vela_app.bin` 内绝对偏移,固件 3.202.79 专用) + +| 应用 | 数据偏移 | 长度 | 尺寸 | 格式 | +|---|---|---|---|---| +| wxpay | 0x0000bc00 | 21772 | 144x144 | I8 | +| womenhealth | 0x00047a00 | 82956 | 144x144 | ARGB8888 | +| weather | 0x017de200 | 82956 | 144x144 | ARGB8888 | +| walkie_talkie | 0x02d45600 | 82956 | 144x144 | ARGB8888 | +| voice/alexa | 0x032fa000 | 6005 | 144x144 | I8(特例) | +| voice/aivs | 0x0341c400 | 82956 | 144x144 | ARGB8888 | +| todolist | 0x03aa9000 | 82956 | 144x144 | ARGB8888 | +| timer | 0x03ad3c00 | 82956 | 144x144 | ARGB8888 | +| sports/training | 0x0466b000 | 82956 | 144x144 | ARGB8888 | +| sports/record | 0x04a63000 | 82956 | 144x144 | ARGB8888 | +| sports | 0x04baee00 | 82956 | 144x144 | ARGB8888 | +| sports/course | 0x05132400 | 82956 | 144x144 | ARGB8888 | +| sleep | 0x05156400 | 82956 | 144x144 | ARGB8888 | +| settings | 0x052e8000 | 82956 | 144x144 | ARGB8888 | +| remote_camera | 0x06593a00 | 21772 | 144x144 | I8 | +| recorder | 0x067a4600 | 82956 | 144x144 | ARGB8888 | +| pressure | 0x07908400 | 82956 | 144x144 | ARGB8888 | +| phone | 0x07a4d000 | 82956 | 144x144 | ARGB8888 | +| phone/contacts_launcher | 0x07a82200 | 82956 | 144x144 | ARGB8888 | +| perpetual_calendar | 0x07b3be00 | 21772 | 144x144 | I8 | +| oxygen | 0x08f08c00 | 82956 | 144x144 | ARGB8888 | +| nfccard | 0x093f2c00 | 82956 | 144x144 | ARGB8888 | +| mijia | 0x09599200 | 82956 | 144x144 | ARGB8888 | +| media | 0x0968e200 | 82956 | 144x144 | ARGB8888 | +| lua | 0x096b5200 | 21772 | 144x144 | I8 | +| interconnect | 0x096d8000 | 82956 | 144x144 | ARGB8888 | +| innovation_research | 0x096efa00 | 82956 | 144x144 | ARGB8888 | +| heartrate | 0x09fea800 | 82956 | 144x144 | ARGB8888 | +| health_today | 0x0a223a00 | 82956 | 144x144 | ARGB8888 | +| flashlight | 0x0b6dba00 | 82956 | 144x144 | ARGB8888 | +| find_phone | 0x0b6f0000 | 82956 | 144x144 | ARGB8888 | +| esimsms | 0x0b762200 | 82956 | 144x144 | ARGB8888 | +| debug | 0x0b81a200 | 21772 | 144x144 | I8 | +| compass | 0x0b8a1400 | 21772 | 144x144 | I8 | +| chronograph | 0x0ba47e00 | 82956 | 144x144 | ARGB8888 | +| calendar | 0x0bc4b200 | 82956 | 144x144 | ARGB8888 | +| breath | 0x0bd54c00 | 82956 | 144x144 | ARGB8888 | +| barometer | 0x0e825c00 | 82956 | 144x144 | ARGB8888 | +| alarm | 0x0f3a7e00 | 82956 | 144x144 | ARGB8888 | + +--- + +## 3. 疑问一:为什么"一个 Lua 表盘点一下就替换成功了" + +从这份固件包里能**确证**的事实: + +1. 系统应用图标 = `vela_app.bin` 里的 `launcher.bin`,而 `vela_app.bin` 是**整个 265 MB 分区镜像**; +2. 官方通道 `ota.sh` 的写入方式是 `dd ... of=/dev/app`,**裸写分区**; +3. 官方 OTA 包是 **SignApk v2/v3 签名的**,因此**不可能**通过"重新打包 OTA"来替换图标; +4. 分区设备节点 `/dev/app`、`/dev/system` 等对用户态是**可打开的字符/块设备**(LoXe 文档也专门警告"不要直接试写未知设备,对分区节点 `dd`/重定向可能立即破坏系统")。 + +把这四点连起来,社区图标包只能走一条路:**绕过签名校验,直接把图标字节写进分区**。 + +所以那个"Lua 表盘"的角色是**运载 + 执行载体**,而不是"表盘 API 能设置图标": + +``` +图标包(做成表盘包的样子) +├── asset/*.bin 实际是一个"补丁表":[(绝对偏移, 该长度字节), ...] +└── main.lua ui.init / 按钮回调里做写入 + │ 点击"立即替换" + ▼ + 按记录 seek+write 到 /dev/app(或整镜像 dd) + ▼ + ROMFS 是只读挂载 + 有页缓存 → 必须重启才能重新读取 + ▼ + 重启后应用列表图标变了 +``` + +**"稍等片刻 + 重启"正是这条链路的指纹**:如果只是普通文件级改动,不需要重启;裸写分区必须重启,因为已经挂载的只读视图和缓存都是旧的。 + +> 这一节里,(1)(2)(3)(4) 是本次从固件包**实证**的;"表盘脚本直接写 `/dev/app`" 是**最符合全部观察的推断**,需要用那个图标包本身(看 `main.lua` 是否出现 `lvgl.fs.open_file`+`/dev/app`+`write`+`seek`)或实机日志来最终确认。 +> 另外一种可能是:表盘只是商店分类/容器格式,真正推送由工具箱的安装通道完成。区分方法同上——看包里有没有 Lua 写盘代码,以及 payload 是"几十 KB 的补丁表"还是"75 MB 的整分区镜像"。 + +--- + +## 4. 疑问二:为什么"只适用于特定固件版本"(例如 3.100.032) + +先纠正一点:**锁版本的不是工具,是包里的那份 payload。** AstroBox 这类工具箱本身跨版本通用;写着"仅适用于 3.100.032"的是那份资源包。 + +原因有四层,前两层是技术必然,后两层是工程现实: + +**① 绝对寻址 + 顺序排布(决定性原因)** +ROMFS 把所有 inode、文件名和文件数据**首尾相接顺序排布**。补丁表里存的是**绝对偏移**(例如 `0x0f3a7e00`)。只要前面对任何一个文件的长度变了 —— 新增一个图标、换一个字体、改一句文案、某个 `.bin` 从 I8 变 ARGB8888(长度差 61184 字节)—— **后面所有偏移全部平移**。此时旧补丁会把图标字节写进别的文件里,轻则图标错乱,重则损坏资源分区。 + +这跟"为什么这个 APK 版本的汉化补丁不能用到下一个版本"是同一类问题:**不是逻辑变了,是逐字节布局变了。** + +**② 一致性** +资源分区与 AP/system 分区是配套发布的(资源清单、图标索引、字体/i18n 版本互相对应)。混搭会出现显示错乱,严重时起不来。所以包作者干脆要求"先降级到配套版本"。 + +**③ 官方 OTA 会整分区覆盖** +任何系统升级都执行 `dd ... of=/dev/app`,**你的图标会被整体重写回去**。所以"锁版本"同时意味着"OTA 之后失效";如果升级后还拿旧偏移去写,就是往错误位置写数据。 + +**④ 作者的取材边界** +那个包和它同一作者发布的[小米手环11固件](https://www.bandbbs.cn/resources/7955/)、[红米手表6固件](https://www.bandbbs.cn/resources/7243/)放在一起 —— 他是做固件 dump 的人,只为"手上有 dump 的那个版本"做偏移表。另外那份 S4 41mm 的包标的是 `3.100.032`,而你这台 eSIM 是 `3.202.79`,本来就不是同一条构建线。 + +**所以"难道每个版本都有不可替代的变化吗"——不是。** 是"按绝对偏移硬写"这个做法天生不能跨版本。**换一种做法就可以跨版本**(见下一节)。 + +--- + +## 5. 更好的做法:不重建 ROMFS,按名字原地替换(已实现并验证) + +因为 144×144 的图标换成另一个 144×144、同色彩格式的图标时,**字节长度完全相同**,所以可以: + +> **在设备上解析 ROMFS → 按路径找到 `/launcher.bin` → 用等长的新图标覆盖它的数据 → inode 表、super block、后续所有偏移全部不动。** + +这样: +- 不需要重建 ROMFS,不需要改 `size`/`checksum`/`next`; +- 不需要"每版本一份包"——偏移是**运行时从当前固件里算出来的**,所以**天然跨固件版本**; +- 传输量从"265 MB(或压缩 75 MB)整分区"降到"每个图标 83 KB / 22 KB"。 + +### 已验证的实现 + +工具:`_tools/romfs.py`(ROMFS 解析/遍历)、`_tools/iconpack.py`(make / apply / list)、`_tools/render_icons.py`(解码为 PNG)。 + +验证结果: + +| 验证项 | 结果 | +|---|---| +| ARGB8888 编码器与原固件字节级一致(alarm 图标往返) | ✅ 完全一致(82956 字节) | +| I8 编码器长度一致 + 视觉误差 | ✅ 长度一致,平均绝对误差 0.22/255 | +| 原地替换后条目数 / 路径 / inode 表 / super block / 文件总长 | ✅ 全部**逐位不变**(7834 条,`inode table identical: True`) | +| 内容变化的文件 | ✅ 恰好只有被替换的那几个 `launcher.bin` | + +用 4 个自制图标(alarm / timer / wxpay / settings)跑通了 +`make → apply → 校验 → 回读渲染`全流程,见 `_out/patched_custom.png`。 + +--- + +## 6. 还缺的那一环:写入通道 + +到这里,"**改什么、改成什么、怎么做到版本无关**"都已经解决。**唯一未解决的是"怎么把这几万字节送进 `/dev/app`"**: + +| 方式 | 说明 | 代价 | +|---|---|---| +| 实机 shell `dd` | 最直接,需要能拿到 shell(或 Recovery) | 需要相应工具/权限 | +| 表盘包 + 脚本写分区节点 | 社区现有做法,整链路已由本次分析解释清楚 | 属于滥用未公开行为,OTA 后失效;写错偏移会损坏分区 | +| 重打包 OTA zip | ❌ 签名校验过不去 | 不可行 | + +要在你的机器上落地,下一步需要的是:**在真机上把 `/dev/app`(或至少 ROMFS 头与目标 inode)读出来**,用 `iconpack.py list` 得到你这一版固件的真实偏移,再生成补丁。这样就不再受"包只适用于某某版本"的限制。 + +--- + +## 7. 风险提示 + +- 对**分区节点**写数据是文档明确警告的危险操作;写错偏移会破坏资源分区,可能导致无法开机。 +- 写之前**务必备份**原始分区(至少备份 ROMFS 头 + inode 表 + 你要改的 `launcher.bin` 原字节,能回滚)。 +- 系统 OTA 会覆盖你的修改;跨版本套用旧补丁 = 往错误位置写数据。 +- 修改系统资源分区可能影响保修;请自行确认设备所有权与当地法律。 +- `voice/alexa/launcher.bin`(6005 字节,`flags=0x0008`)格式尚未确定,不要动它。 + +--- + +## 附:工作目录 + +``` +_x/ 解包后的固件(含 13 个分区镜像) +_tools/romfs.py ROMFS 解析与遍历 +_tools/iconpack.py list / make / apply 图标补丁 +_tools/render_icons.py 把 launcher.bin 解码成 PNG +_tools/patch_demo.py 原地等长替换的概念验证 +_tools/demo_icons.py 编码器自检 + 生成示例图标 +_tools/verify_patch.py 补丁后元数据一致性校验 +_out/icons/ 从原厂固件导出的 38 个应用图标 PNG +_out/launcher_icons_sheet.png 全部图标总览 +_out/before_after.png 原地替换前后对比 +_out/patched_custom.png 自制图标写入后的回读结果 +``` diff --git a/s4e-icons/docs/原表盘说明.md b/s4e-icons/docs/原表盘说明.md new file mode 100644 index 0000000..f4d9a9c --- /dev/null +++ b/s4e-icons/docs/原表盘说明.md @@ -0,0 +1,139 @@ +> 本文是当时的分析 / 交付存档,路径按当时的工作目录写。 +> 本项目里的对应关系: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 那份镜像,或官方固件包里的同名文件)。 diff --git a/s4e-icons/docs/图标命名对照表-历史.md b/s4e-icons/docs/图标命名对照表-历史.md new file mode 100644 index 0000000..44874b4 --- /dev/null +++ b/s4e-icons/docs/图标命名对照表-历史.md @@ -0,0 +1,66 @@ +# 图标命名对照表 —— Xiaomi Watch S4 eSIM (o62lte, 固件 3.202.79) + +把你的 **144×144 PNG** 按下面的「PNG 文件名」命名,放进同一个目录,我就能一次性生成完整补丁包。 + +- 共 **39** 个可替换图标 +- 文件名规则:目录层级用 `_` 连接,去掉结尾的 `launcher` + (`alarm/launcher.bin` → `alarm.png`,`sports/training/launcher.bin` → `sports_training.png`) +- PNG 请带透明通道(RGBA);圆形外圈留透明即可,不要自己画背景 + +| PNG 文件名 | 原厂尺寸 | 槽位格式 | 槽位字节 | 数据偏移 | 注意 | +|---|---|---|---|---|---| +| `alarm.png` | 144x144 | ARGB8888 | 82956 | `0x0f3a7e00` | | +| `barometer.png` | 144x144 | ARGB8888 | 82956 | `0x0e825c00` | | +| `breath.png` | 144x144 | ARGB8888 | 82956 | `0x0bd54c00` | | +| `calendar.png` | 144x144 | ARGB8888 | 82956 | `0x0bc4b200` | | +| `chronograph.png` | 144x144 | ARGB8888 | 82956 | `0x0ba47e00` | | +| `compass.png` | 144x144 | I8 | 21772 | `0x0b8a1400` | ⚠ 需 I8 编码(256 色) | +| `debug.png` | 144x144 | I8 | 21772 | `0x0b81a200` | ⚠ 需 I8 编码(256 色) | +| `esimsms.png` | 144x144 | ARGB8888 | 82956 | `0x0b762200` | | +| `find_phone.png` | 144x144 | ARGB8888 | 82956 | `0x0b6f0000` | | +| `flashlight.png` | 144x144 | ARGB8888 | 82956 | `0x0b6dba00` | | +| `health_today.png` | 144x144 | ARGB8888 | 82956 | `0x0a223a00` | | +| `heartrate.png` | 144x144 | ARGB8888 | 82956 | `0x09fea800` | | +| `innovation_research.png` | 144x144 | ARGB8888 | 82956 | `0x096efa00` | | +| `interconnect.png` | 144x144 | ARGB8888 | 82956 | `0x096d8000` | | +| `lua.png` | 144x144 | I8 | 21772 | `0x096b5200` | ⚠ 需 I8 编码(256 色) | +| `media.png` | 144x144 | ARGB8888 | 82956 | `0x0968e200` | | +| `mijia.png` | 144x144 | ARGB8888 | 82956 | `0x09599200` | | +| `nfccard.png` | 144x144 | ARGB8888 | 82956 | `0x093f2c00` | | +| `oxygen.png` | 144x144 | ARGB8888 | 82956 | `0x08f08c00` | | +| `perpetual_calendar.png` | 144x144 | I8 | 21772 | `0x07b3be00` | ⚠ 需 I8 编码(256 色) | +| `phone.png` | 144x144 | ARGB8888 | 82956 | `0x07a4d000` | | +| `phone_contacts.png` | 144x144 | ARGB8888 | 82956 | `0x07a82200` | | +| `pressure.png` | 144x144 | ARGB8888 | 82956 | `0x07908400` | | +| `recorder.png` | 144x144 | ARGB8888 | 82956 | `0x067a4600` | | +| `remote_camera.png` | 144x144 | I8 | 21772 | `0x06593a00` | ⚠ 需 I8 编码(256 色) | +| `settings.png` | 144x144 | ARGB8888 | 82956 | `0x052e8000` | | +| `sleep.png` | 144x144 | ARGB8888 | 82956 | `0x05156400` | | +| `sports.png` | 144x144 | ARGB8888 | 82956 | `0x04baee00` | | +| `sports_course.png` | 144x144 | ARGB8888 | 82956 | `0x05132400` | | +| `sports_record.png` | 144x144 | ARGB8888 | 82956 | `0x04a63000` | | +| `sports_training.png` | 144x144 | ARGB8888 | 82956 | `0x0466b000` | | +| `timer.png` | 144x144 | ARGB8888 | 82956 | `0x03ad3c00` | | +| `todolist.png` | 144x144 | ARGB8888 | 82956 | `0x03aa9000` | | +| `voice_aivs.png` | 144x144 | ARGB8888 | 82956 | `0x0341c400` | | +| `voice_alexa.png` | 144x144 | I8 | 6005 | `0x032fa000` | ⛔ 格式特殊,暂不支持 | +| `walkie_talkie.png` | 144x144 | ARGB8888 | 82956 | `0x02d45600` | | +| `weather.png` | 144x144 | ARGB8888 | 82956 | `0x017de200` | | +| `womenhealth.png` | 144x144 | ARGB8888 | 82956 | `0x00047a00` | | +| `wxpay.png` | 144x144 | I8 | 21772 | `0x0000bc00` | ⚠ 需 I8 编码(256 色) | + +## 两点必须注意 + +**1. 槽位字节必须完全相等。** ROMFS 是绝对偏移顺序排布,替换文件的字节长度必须和原文件一模一样,否则后面所有文件的偏移整体平移、镜像损坏。正因为你的 PNG 是 144×144、与原厂一致,编码后长度天然相等,可以直接原地覆盖。 + +**2. 标注「需 I8 编码」的那几个,会被自动量化成 256 色。** 它们的槽位只有 21772 字节,装不下 ARGB8888(82956 字节),所以只能用索引色。实测对扁平图标平均误差 0.22/255,肉眼无差别;但如果你的图标有渐变/照片级内容,这几个会有可见色带。 + +`voice_alexa` 是 6005 字节的特例(`flags=0x0008`),格式未确定,建议先不动。 + +## 生成命令 + +``` +python _tools/iconpack.py make _x/vela_app.bin <你的PNG目录> _out/mypack --patched +``` + +产出:`icons.patch`(补丁表)、`icons.rollback`(回滚包)、`payload/`(每个图标的 .bin)、`manifest.json`(含偏移与 SHA256)、`vela_app_patched.bin`(可直接写入的整分区镜像)。 diff --git a/s4e-icons/docs/图标命名对照表.md b/s4e-icons/docs/图标命名对照表.md new file mode 100644 index 0000000..9e6265c --- /dev/null +++ b/s4e-icons/docs/图标命名对照表.md @@ -0,0 +1,70 @@ +> 本文是当时的分析 / 交付存档,路径按当时的工作目录写。 +> 本项目里的对应关系:tools 对应 _tools,inputs/target/vela_app.bin 对应 _x/vela_app.bin, +> inputs/source 对应 _s5,.work 对应 _out(产物),dist 对应 _out/faces。 + +# 图标命名对照表 —— Xiaomi Watch S4 eSIM (o62lte, 固件 3.202.79) + +把你的 **144×144 PNG** 按下面的「PNG 文件名」命名,放进同一个目录,我就能一次性生成完整补丁包。 + +- 共 **39** 个可替换图标 +- 文件名规则:目录层级用 `_` 连接,去掉结尾的 `launcher` + (`alarm/launcher.bin` → `alarm.png`,`sports/training/launcher.bin` → `sports_training.png`) +- PNG 请带透明通道(RGBA);圆形外圈留透明即可,不要自己画背景 + +| PNG 文件名 | 原厂尺寸 | 槽位格式 | 槽位字节 | 数据偏移 | 注意 | +|---|---|---|---|---|---| +| `alarm.png` | 144x144 | ARGB8888 | 82956 | `0x0f3a7e00` | | +| `barometer.png` | 144x144 | ARGB8888 | 82956 | `0x0e825c00` | | +| `breath.png` | 144x144 | ARGB8888 | 82956 | `0x0bd54c00` | | +| `calendar.png` | 144x144 | ARGB8888 | 82956 | `0x0bc4b200` | | +| `chronograph.png` | 144x144 | ARGB8888 | 82956 | `0x0ba47e00` | | +| `compass.png` | 144x144 | I8 | 21772 | `0x0b8a1400` | ⚠ 需 I8 编码(256 色) | +| `debug.png` | 144x144 | I8 | 21772 | `0x0b81a200` | ⚠ 需 I8 编码(256 色) | +| `esimsms.png` | 144x144 | ARGB8888 | 82956 | `0x0b762200` | | +| `find_phone.png` | 144x144 | ARGB8888 | 82956 | `0x0b6f0000` | | +| `flashlight.png` | 144x144 | ARGB8888 | 82956 | `0x0b6dba00` | | +| `health_today.png` | 144x144 | ARGB8888 | 82956 | `0x0a223a00` | | +| `heartrate.png` | 144x144 | ARGB8888 | 82956 | `0x09fea800` | | +| `innovation_research.png` | 144x144 | ARGB8888 | 82956 | `0x096efa00` | | +| `interconnect.png` | 144x144 | ARGB8888 | 82956 | `0x096d8000` | | +| `lua.png` | 144x144 | I8 | 21772 | `0x096b5200` | ⚠ 需 I8 编码(256 色) | +| `media.png` | 144x144 | ARGB8888 | 82956 | `0x0968e200` | | +| `mijia.png` | 144x144 | ARGB8888 | 82956 | `0x09599200` | | +| `nfccard.png` | 144x144 | ARGB8888 | 82956 | `0x093f2c00` | | +| `oxygen.png` | 144x144 | ARGB8888 | 82956 | `0x08f08c00` | | +| `perpetual_calendar.png` | 144x144 | I8 | 21772 | `0x07b3be00` | ⚠ 需 I8 编码(256 色) | +| `phone.png` | 144x144 | ARGB8888 | 82956 | `0x07a4d000` | | +| `phone_contacts.png` | 144x144 | ARGB8888 | 82956 | `0x07a82200` | | +| `pressure.png` | 144x144 | ARGB8888 | 82956 | `0x07908400` | | +| `recorder.png` | 144x144 | ARGB8888 | 82956 | `0x067a4600` | | +| `remote_camera.png` | 144x144 | I8 | 21772 | `0x06593a00` | ⚠ 需 I8 编码(256 色) | +| `settings.png` | 144x144 | ARGB8888 | 82956 | `0x052e8000` | | +| `sleep.png` | 144x144 | ARGB8888 | 82956 | `0x05156400` | | +| `sports.png` | 144x144 | ARGB8888 | 82956 | `0x04baee00` | | +| `sports_course.png` | 144x144 | ARGB8888 | 82956 | `0x05132400` | | +| `sports_record.png` | 144x144 | ARGB8888 | 82956 | `0x04a63000` | | +| `sports_training.png` | 144x144 | ARGB8888 | 82956 | `0x0466b000` | | +| `timer.png` | 144x144 | ARGB8888 | 82956 | `0x03ad3c00` | | +| `todolist.png` | 144x144 | ARGB8888 | 82956 | `0x03aa9000` | | +| `voice_aivs.png` | 144x144 | ARGB8888 | 82956 | `0x0341c400` | | +| `voice_alexa.png` | 144x144 | I8 | 6005 | `0x032fa000` | ⛔ 格式特殊,暂不支持 | +| `walkie_talkie.png` | 144x144 | ARGB8888 | 82956 | `0x02d45600` | | +| `weather.png` | 144x144 | ARGB8888 | 82956 | `0x017de200` | | +| `womenhealth.png` | 144x144 | ARGB8888 | 82956 | `0x00047a00` | | +| `wxpay.png` | 144x144 | I8 | 21772 | `0x0000bc00` | ⚠ 需 I8 编码(256 色) | + +## 两点必须注意 + +**1. 槽位字节必须完全相等。** ROMFS 是绝对偏移顺序排布,替换文件的字节长度必须和原文件一模一样,否则后面所有文件的偏移整体平移、镜像损坏。正因为你的 PNG 是 144×144、与原厂一致,编码后长度天然相等,可以直接原地覆盖。 + +**2. 标注「需 I8 编码」的那几个,会被自动量化成 256 色。** 它们的槽位只有 21772 字节,装不下 ARGB8888(82956 字节),所以只能用索引色。实测对扁平图标平均误差 0.22/255,肉眼无差别;但如果你的图标有渐变/照片级内容,这几个会有可见色带。 + +`voice_alexa` 是 6005 字节的特例(`flags=0x0008`),格式未确定,建议先不动。 + +## 生成命令 + +``` +python tools/iconpack.py make inputs/target/vela_app.bin <你的PNG目录> .work/mypack --patched +``` + +产出:`icons.patch`(补丁表)、`icons.rollback`(回滚包)、`payload/`(每个图标的 .bin)、`manifest.json`(含偏移与 SHA256)、`vela_app_patched.bin`(可直接写入的整分区镜像)。 diff --git a/s4e-icons/flash.sh b/s4e-icons/flash.sh new file mode 100644 index 0000000..329af5a --- /dev/null +++ b/s4e-icons/flash.sh @@ -0,0 +1,769 @@ +#!/bin/sh +# S5 -> S4 icon replacement, run on-device in nsh (NuttX shell). +# 把 flash.sh 与 payload/ 放到同一个目录,例如 /data/s5icons/ +# 然后: sh /data/s5icons/flash.sh 最后: reboot +# nsh 不支持 $( ) / && / || / 通配符,所以这里是线性展开的。 + +DEV=/dev/app +BASE=/data/s5icons/payload + +echo "target check" +ls -l $DEV + +echo "== canary_superblock == +dd if=$BASE/000_canary_superblock.bin of=$DEV bs=4 seek=0 count=8 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=0 count=8 +cmp /tmp/chk.bin $BASE/000_canary_superblock.bin + +echo "== activities == +dd if=$BASE/001_activities.bin of=$DEV bs=4 seek=66401536 count=5443 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=66401536 count=5443 +cmp /tmp/chk.bin $BASE/001_activities.bin + +echo "== alarm == +dd if=$BASE/002_alarm.bin of=$DEV bs=4 seek=63872896 count=20739 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=63872896 count=20739 +cmp /tmp/chk.bin $BASE/002_alarm.bin + +echo "== barometer == +dd if=$BASE/003_barometer.bin of=$DEV bs=4 seek=60856064 count=20739 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=60856064 count=20739 +cmp /tmp/chk.bin $BASE/003_barometer.bin + +echo "== breath == +dd if=$BASE/004_breath.bin of=$DEV bs=4 seek=49632000 count=20739 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=49632000 count=20739 +cmp /tmp/chk.bin $BASE/004_breath.bin + +echo "== calendar == +dd if=$BASE/005_calendar.bin of=$DEV bs=4 seek=49360000 count=20739 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=49360000 count=20739 +cmp /tmp/chk.bin $BASE/005_calendar.bin + +echo "== chronograph == +dd if=$BASE/006_chronograph.bin of=$DEV bs=4 seek=48832384 count=20739 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=48832384 count=20739 +cmp /tmp/chk.bin $BASE/006_chronograph.bin + +echo "== compass == +dd if=$BASE/007_compass.bin of=$DEV bs=4 seek=48399616 count=5443 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=48399616 count=5443 +cmp /tmp/chk.bin $BASE/007_compass.bin + +echo "== debug == +dd if=$BASE/008_debug.bin of=$DEV bs=4 seek=48261248 count=5443 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=48261248 count=5443 +cmp /tmp/chk.bin $BASE/008_debug.bin + +echo "== esimsms == +dd if=$BASE/009_esimsms.bin of=$DEV bs=4 seek=48072832 count=20739 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=48072832 count=20739 +cmp /tmp/chk.bin $BASE/009_esimsms.bin + +echo "== find_phone == +dd if=$BASE/010_find_phone.bin of=$DEV bs=4 seek=47955968 count=20739 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=47955968 count=20739 +cmp /tmp/chk.bin $BASE/010_find_phone.bin + +echo "== flashlight == +dd if=$BASE/011_flashlight.bin of=$DEV bs=4 seek=47935104 count=20739 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=47935104 count=20739 +cmp /tmp/chk.bin $BASE/011_flashlight.bin + +echo "== health_today == +dd if=$BASE/012_health_today.bin of=$DEV bs=4 seek=42503808 count=20739 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=42503808 count=20739 +cmp /tmp/chk.bin $BASE/012_health_today.bin + +echo "== heartrate == +dd if=$BASE/013_heartrate.bin of=$DEV bs=4 seek=41921024 count=20739 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=41921024 count=20739 +cmp /tmp/chk.bin $BASE/013_heartrate.bin + +echo "== innovation_research == +dd if=$BASE/014_innovation_research.bin of=$DEV bs=4 seek=39566976 count=20739 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=39566976 count=20739 +cmp /tmp/chk.bin $BASE/014_innovation_research.bin + +echo "== interconnect == +dd if=$BASE/015_interconnect.bin of=$DEV bs=4 seek=39542784 count=20739 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=39542784 count=20739 +cmp /tmp/chk.bin $BASE/015_interconnect.bin + +echo "== lua == +dd if=$BASE/016_lua.bin of=$DEV bs=4 seek=39507072 count=5443 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=39507072 count=5443 +cmp /tmp/chk.bin $BASE/016_lua.bin + +echo "== media == +dd if=$BASE/017_media.bin of=$DEV bs=4 seek=39467136 count=20739 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=39467136 count=20739 +cmp /tmp/chk.bin $BASE/017_media.bin + +echo "== mijia == +dd if=$BASE/018_mijia.bin of=$DEV bs=4 seek=39216256 count=20739 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=39216256 count=20739 +cmp /tmp/chk.bin $BASE/018_mijia.bin + +echo "== nfccard == +dd if=$BASE/019_nfccard.bin of=$DEV bs=4 seek=38783744 count=20739 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=38783744 count=20739 +cmp /tmp/chk.bin $BASE/019_nfccard.bin + +echo "== oxygen == +dd if=$BASE/020_oxygen.bin of=$DEV bs=4 seek=37495552 count=20739 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=37495552 count=20739 +cmp /tmp/chk.bin $BASE/020_oxygen.bin + +echo "== perpetual_calendar == +dd if=$BASE/021_perpetual_calendar.bin of=$DEV bs=4 seek=32305024 count=5443 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=32305024 count=5443 +cmp /tmp/chk.bin $BASE/021_perpetual_calendar.bin + +echo "== phone == +dd if=$BASE/022_phone.bin of=$DEV bs=4 seek=32060416 count=20739 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=32060416 count=20739 +cmp /tmp/chk.bin $BASE/022_phone.bin + +echo "== phone_contacts == +dd if=$BASE/023_phone_contacts.bin of=$DEV bs=4 seek=32114816 count=20739 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=32114816 count=20739 +cmp /tmp/chk.bin $BASE/023_phone_contacts.bin + +echo "== pressure == +dd if=$BASE/024_pressure.bin of=$DEV bs=4 seek=31727872 count=20739 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=31727872 count=20739 +cmp /tmp/chk.bin $BASE/024_pressure.bin + +echo "== recorder == +dd if=$BASE/025_recorder.bin of=$DEV bs=4 seek=27169152 count=20739 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=27169152 count=20739 +cmp /tmp/chk.bin $BASE/025_recorder.bin + +echo "== remote_camera == +dd if=$BASE/026_remote_camera.bin of=$DEV bs=4 seek=26627712 count=5443 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=26627712 count=5443 +cmp /tmp/chk.bin $BASE/026_remote_camera.bin + +echo "== settings == +dd if=$BASE/027_settings.bin of=$DEV bs=4 seek=21733376 count=20739 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=21733376 count=20739 +cmp /tmp/chk.bin $BASE/027_settings.bin + +echo "== sleep == +dd if=$BASE/028_sleep.bin of=$DEV bs=4 seek=21321984 count=20739 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=21321984 count=20739 +cmp /tmp/chk.bin $BASE/028_sleep.bin + +echo "== sports == +dd if=$BASE/029_sports.bin of=$DEV bs=4 seek=19839872 count=20739 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=19839872 count=20739 +cmp /tmp/chk.bin $BASE/029_sports.bin + +echo "== sports_course == +dd if=$BASE/030_sports_course.bin of=$DEV bs=4 seek=21285120 count=20739 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=21285120 count=20739 +cmp /tmp/chk.bin $BASE/030_sports_course.bin + +echo "== sports_record == +dd if=$BASE/031_sports_record.bin of=$DEV bs=4 seek=19500032 count=20739 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=19500032 count=20739 +cmp /tmp/chk.bin $BASE/031_sports_record.bin + +echo "== sports_training == +dd if=$BASE/032_sports_training.bin of=$DEV bs=4 seek=18459648 count=20739 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=18459648 count=20739 +cmp /tmp/chk.bin $BASE/032_sports_training.bin + +echo "== timer == +dd if=$BASE/033_timer.bin of=$DEV bs=4 seek=15421184 count=20739 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=15421184 count=20739 +cmp /tmp/chk.bin $BASE/033_timer.bin + +echo "== todolist == +dd if=$BASE/034_todolist.bin of=$DEV bs=4 seek=15377408 count=20739 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=15377408 count=20739 +cmp /tmp/chk.bin $BASE/034_todolist.bin + +echo "== voice_aivs == +dd if=$BASE/035_voice_aivs.bin of=$DEV bs=4 seek=13660416 count=20739 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=13660416 count=20739 +cmp /tmp/chk.bin $BASE/035_voice_aivs.bin + +echo "== weather == +dd if=$BASE/036_weather.bin of=$DEV bs=4 seek=6256768 count=20739 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=6256768 count=20739 +cmp /tmp/chk.bin $BASE/036_weather.bin + +echo "== womenhealth == +dd if=$BASE/037_womenhealth.bin of=$DEV bs=4 seek=73344 count=20739 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=73344 count=20739 +cmp /tmp/chk.bin $BASE/037_womenhealth.bin + +echo "== cc_add == +dd if=$BASE/038_cc_add.bin of=$DEV bs=4 seek=48398592 count=835 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=48398592 count=835 +cmp /tmp/chk.bin $BASE/038_cc_add.bin + +echo "== cc_air_mode_close == +dd if=$BASE/039_cc_air_mode_close.bin of=$DEV bs=4 seek=48394112 count=4355 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=48394112 count=4355 +cmp /tmp/chk.bin $BASE/039_cc_air_mode_close.bin + +echo "== cc_air_mode_conn == +dd if=$BASE/040_cc_air_mode_conn.bin of=$DEV bs=4 seek=48393216 count=835 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=48393216 count=835 +cmp /tmp/chk.bin $BASE/040_cc_air_mode_conn.bin + +echo "== cc_air_mode_open == +dd if=$BASE/041_cc_air_mode_open.bin of=$DEV bs=4 seek=48384256 count=4355 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=48384256 count=4355 +cmp /tmp/chk.bin $BASE/041_cc_air_mode_open.bin + +echo "== cc_battery_close == +dd if=$BASE/042_cc_battery_close.bin of=$DEV bs=4 seek=48379776 count=4355 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=48379776 count=4355 +cmp /tmp/chk.bin $BASE/042_cc_battery_close.bin + +echo "== cc_battery_open == +dd if=$BASE/043_cc_battery_open.bin of=$DEV bs=4 seek=48375296 count=4355 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=48375296 count=4355 +cmp /tmp/chk.bin $BASE/043_cc_battery_open.bin + +echo "== cc_bt_connect == +dd if=$BASE/044_cc_bt_connect.bin of=$DEV bs=4 seek=48374400 count=835 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=48374400 count=835 +cmp /tmp/chk.bin $BASE/044_cc_bt_connect.bin + +echo "== cc_bt_disconnect == +dd if=$BASE/045_cc_bt_disconnect.bin of=$DEV bs=4 seek=48373504 count=835 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=48373504 count=835 +cmp /tmp/chk.bin $BASE/045_cc_bt_disconnect.bin + +echo "== cc_cancel == +dd if=$BASE/046_cc_cancel.bin of=$DEV bs=4 seek=48372608 count=835 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=48372608 count=835 +cmp /tmp/chk.bin $BASE/046_cc_cancel.bin + +echo "== cc_drain_close == +dd if=$BASE/047_cc_drain_close.bin of=$DEV bs=4 seek=48368128 count=4355 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=48368128 count=4355 +cmp /tmp/chk.bin $BASE/047_cc_drain_close.bin + +echo "== cc_earphone == +dd if=$BASE/048_cc_earphone.bin of=$DEV bs=4 seek=48353408 count=835 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=48353408 count=835 +cmp /tmp/chk.bin $BASE/048_cc_earphone.bin + +echo "== cc_find_phone_close == +dd if=$BASE/049_cc_find_phone_close.bin of=$DEV bs=4 seek=48348928 count=4355 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=48348928 count=4355 +cmp /tmp/chk.bin $BASE/049_cc_find_phone_close.bin + +echo "== cc_flashlight == +dd if=$BASE/050_cc_flashlight.bin of=$DEV bs=4 seek=48339968 count=4355 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=48339968 count=4355 +cmp /tmp/chk.bin $BASE/050_cc_flashlight.bin + +echo "== cc_mobile_network_close == +dd if=$BASE/051_cc_mobile_network_close.bin of=$DEV bs=4 seek=48334592 count=4355 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=48334592 count=4355 +cmp /tmp/chk.bin $BASE/051_cc_mobile_network_close.bin + +echo "== cc_mobile_network_open == +dd if=$BASE/052_cc_mobile_network_open.bin of=$DEV bs=4 seek=48325632 count=4355 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=48325632 count=4355 +cmp /tmp/chk.bin $BASE/052_cc_mobile_network_open.bin + +echo "== cc_mute_close == +dd if=$BASE/053_cc_mute_close.bin of=$DEV bs=4 seek=48321152 count=4355 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=48321152 count=4355 +cmp /tmp/chk.bin $BASE/053_cc_mute_close.bin + +echo "== cc_mute_open == +dd if=$BASE/054_cc_mute_open.bin of=$DEV bs=4 seek=48316672 count=4355 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=48316672 count=4355 +cmp /tmp/chk.bin $BASE/054_cc_mute_open.bin + +echo "== cc_not_disturb_close == +dd if=$BASE/055_cc_not_disturb_close.bin of=$DEV bs=4 seek=48301440 count=4355 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=48301440 count=4355 +cmp /tmp/chk.bin $BASE/055_cc_not_disturb_close.bin + +echo "== cc_not_disturb_open == +dd if=$BASE/056_cc_not_disturb_open.bin of=$DEV bs=4 seek=48296960 count=4355 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=48296960 count=4355 +cmp /tmp/chk.bin $BASE/056_cc_not_disturb_open.bin + +echo "== cc_raise_bright_close == +dd if=$BASE/057_cc_raise_bright_close.bin of=$DEV bs=4 seek=48288000 count=4355 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=48288000 count=4355 +cmp /tmp/chk.bin $BASE/057_cc_raise_bright_close.bin + +echo "== cc_raise_bright_open == +dd if=$BASE/058_cc_raise_bright_open.bin of=$DEV bs=4 seek=48283520 count=4355 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=48283520 count=4355 +cmp /tmp/chk.bin $BASE/058_cc_raise_bright_open.bin + +echo "== cc_settings == +dd if=$BASE/059_cc_settings.bin of=$DEV bs=4 seek=48279040 count=4355 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=48279040 count=4355 +cmp /tmp/chk.bin $BASE/059_cc_settings.bin + +echo "== sys_lock_icon == +dd if=$BASE/060_sys_lock_icon.bin of=$DEV bs=4 seek=15582848 count=515 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=15582848 count=515 +cmp /tmp/chk.bin $BASE/060_sys_lock_icon.bin + +echo "== sys_system_lock == +dd if=$BASE/061_sys_system_lock.bin of=$DEV bs=4 seek=48587776 count=835 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=48587776 count=835 +cmp /tmp/chk.bin $BASE/061_sys_system_lock.bin + +echo "== sys_quiet_mode == +dd if=$BASE/062_sys_quiet_mode.bin of=$DEV bs=4 seek=48616576 count=835 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=48616576 count=835 +cmp /tmp/chk.bin $BASE/062_sys_quiet_mode.bin + +echo "== sys_no_message == +dd if=$BASE/063_sys_no_message.bin of=$DEV bs=4 seek=37691776 count=1859 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=37691776 count=1859 +cmp /tmp/chk.bin $BASE/063_sys_no_message.bin + +echo "== sys_notify == +dd if=$BASE/064_sys_notify.bin of=$DEV bs=4 seek=21762688 count=1283 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=21762688 count=1283 +cmp /tmp/chk.bin $BASE/064_sys_notify.bin + +echo "== sys_esim_sms_reminder == +dd if=$BASE/065_sys_esim_sms_reminder.bin of=$DEV bs=4 seek=48108800 count=1859 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=48108800 count=1859 +cmp /tmp/chk.bin $BASE/065_sys_esim_sms_reminder.bin + +echo "== sys_enable_donot_disturb == +dd if=$BASE/066_sys_enable_donot_disturb.bin of=$DEV bs=4 seek=39244160 count=1283 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=39244160 count=1283 +cmp /tmp/chk.bin $BASE/066_sys_enable_donot_disturb.bin + +echo "== sys_disable_donot_disturb == +dd if=$BASE/067_sys_disable_donot_disturb.bin of=$DEV bs=4 seek=39247488 count=1283 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=39247488 count=1283 +cmp /tmp/chk.bin $BASE/067_sys_disable_donot_disturb.bin + +echo "== sys_presure_relax == +dd if=$BASE/068_sys_presure_relax.bin of=$DEV bs=4 seek=31843328 count=835 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=31843328 count=835 +cmp /tmp/chk.bin $BASE/068_sys_presure_relax.bin + +echo "== sys_presure_mild == +dd if=$BASE/069_sys_presure_mild.bin of=$DEV bs=4 seek=31847552 count=835 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=31847552 count=835 +cmp /tmp/chk.bin $BASE/069_sys_presure_mild.bin + +echo "== sys_presure_mid == +dd if=$BASE/070_sys_presure_mid.bin of=$DEV bs=4 seek=31851776 count=835 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=31851776 count=835 +cmp /tmp/chk.bin $BASE/070_sys_presure_mid.bin + +echo "== sys_presure_severe == +dd if=$BASE/071_sys_presure_severe.bin of=$DEV bs=4 seek=31839104 count=835 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=31839104 count=835 +cmp /tmp/chk.bin $BASE/071_sys_presure_severe.bin + +echo "== sys_presure_mild32 == +dd if=$BASE/072_sys_presure_mild32.bin of=$DEV bs=4 seek=31846912 count=515 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=31846912 count=515 +cmp /tmp/chk.bin $BASE/072_sys_presure_mild32.bin + +echo "== sys_presure_mid32 == +dd if=$BASE/073_sys_presure_mid32.bin of=$DEV bs=4 seek=31851136 count=515 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=31851136 count=515 +cmp /tmp/chk.bin $BASE/073_sys_presure_mid32.bin + +echo "== sys_presure_severe32 == +dd if=$BASE/074_sys_presure_severe32.bin of=$DEV bs=4 seek=31838464 count=515 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=31838464 count=515 +cmp /tmp/chk.bin $BASE/074_sys_presure_severe32.bin + +echo "== sys_presure_icon == +dd if=$BASE/075_sys_presure_icon.bin of=$DEV bs=4 seek=31852672 count=835 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=31852672 count=835 +cmp /tmp/chk.bin $BASE/075_sys_presure_icon.bin + +echo "== sys_press_grade1 == +dd if=$BASE/076_sys_press_grade1.bin of=$DEV bs=4 seek=32011648 count=515 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=32011648 count=515 +cmp /tmp/chk.bin $BASE/076_sys_press_grade1.bin + +echo "== sys_press_grade2 == +dd if=$BASE/077_sys_press_grade2.bin of=$DEV bs=4 seek=32011008 count=515 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=32011008 count=515 +cmp /tmp/chk.bin $BASE/077_sys_press_grade2.bin + +echo "== sys_press_grade3 == +dd if=$BASE/078_sys_press_grade3.bin of=$DEV bs=4 seek=32010368 count=515 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=32010368 count=515 +cmp /tmp/chk.bin $BASE/078_sys_press_grade3.bin + +echo "== sys_press_grade4 == +dd if=$BASE/079_sys_press_grade4.bin of=$DEV bs=4 seek=32009728 count=515 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=32009728 count=515 +cmp /tmp/chk.bin $BASE/079_sys_press_grade4.bin + +echo "== cc_network_4g == +dd if=$BASE/080_cc_network_4g.bin of=$DEV bs=4 seek=48310400 count=835 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=48310400 count=835 +cmp /tmp/chk.bin $BASE/080_cc_network_4g.bin + +echo "== cc_network_3g == +dd if=$BASE/081_cc_network_3g.bin of=$DEV bs=4 seek=48315776 count=835 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=48315776 count=835 +cmp /tmp/chk.bin $BASE/081_cc_network_3g.bin + +echo "== cc_signal_strength == +dd if=$BASE/082_cc_signal_strength.bin of=$DEV bs=4 seek=48278144 count=835 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=48278144 count=835 +cmp /tmp/chk.bin $BASE/082_cc_signal_strength.bin + +echo "== cc_signal_strength1 == +dd if=$BASE/083_cc_signal_strength1.bin of=$DEV bs=4 seek=48277248 count=835 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=48277248 count=835 +cmp /tmp/chk.bin $BASE/083_cc_signal_strength1.bin + +echo "== cc_signal_strength2 == +dd if=$BASE/084_cc_signal_strength2.bin of=$DEV bs=4 seek=48276352 count=835 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=48276352 count=835 +cmp /tmp/chk.bin $BASE/084_cc_signal_strength2.bin + +echo "== cc_signal_strength3 == +dd if=$BASE/085_cc_signal_strength3.bin of=$DEV bs=4 seek=48275456 count=835 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=48275456 count=835 +cmp /tmp/chk.bin $BASE/085_cc_signal_strength3.bin + +echo "== cc_signal_strength4 == +dd if=$BASE/086_cc_signal_strength4.bin of=$DEV bs=4 seek=48274560 count=835 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=48274560 count=835 +cmp /tmp/chk.bin $BASE/086_cc_signal_strength4.bin + +echo "== cc_location == +dd if=$BASE/087_cc_location.bin of=$DEV bs=4 seek=48339072 count=835 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=48339072 count=835 +cmp /tmp/chk.bin $BASE/087_cc_location.bin + +echo "== cc_network_3g_signal_1 == +dd if=$BASE/088_cc_network_3g_signal_1.bin of=$DEV bs=4 seek=48313984 count=835 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=48313984 count=835 +cmp /tmp/chk.bin $BASE/088_cc_network_3g_signal_1.bin + +echo "== cc_network_3g_signal_2 == +dd if=$BASE/089_cc_network_3g_signal_2.bin of=$DEV bs=4 seek=48313088 count=835 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=48313088 count=835 +cmp /tmp/chk.bin $BASE/089_cc_network_3g_signal_2.bin + +echo "== cc_network_3g_signal_3 == +dd if=$BASE/090_cc_network_3g_signal_3.bin of=$DEV bs=4 seek=48312192 count=835 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=48312192 count=835 +cmp /tmp/chk.bin $BASE/090_cc_network_3g_signal_3.bin + +echo "== cc_network_3g_signal_4 == +dd if=$BASE/091_cc_network_3g_signal_4.bin of=$DEV bs=4 seek=48311296 count=835 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=48311296 count=835 +cmp /tmp/chk.bin $BASE/091_cc_network_3g_signal_4.bin + +echo "== cc_network_3g_no_signal == +dd if=$BASE/092_cc_network_3g_no_signal.bin of=$DEV bs=4 seek=48314880 count=835 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=48314880 count=835 +cmp /tmp/chk.bin $BASE/092_cc_network_3g_no_signal.bin + +echo "== cc_network_4g_signal_1 == +dd if=$BASE/093_cc_network_4g_signal_1.bin of=$DEV bs=4 seek=48308608 count=835 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=48308608 count=835 +cmp /tmp/chk.bin $BASE/093_cc_network_4g_signal_1.bin + +echo "== cc_network_4g_signal_2 == +dd if=$BASE/094_cc_network_4g_signal_2.bin of=$DEV bs=4 seek=48307712 count=835 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=48307712 count=835 +cmp /tmp/chk.bin $BASE/094_cc_network_4g_signal_2.bin + +echo "== cc_network_4g_signal_3 == +dd if=$BASE/095_cc_network_4g_signal_3.bin of=$DEV bs=4 seek=48306816 count=835 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=48306816 count=835 +cmp /tmp/chk.bin $BASE/095_cc_network_4g_signal_3.bin + +echo "== cc_network_4g_signal_4 == +dd if=$BASE/096_cc_network_4g_signal_4.bin of=$DEV bs=4 seek=48305920 count=835 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=48305920 count=835 +cmp /tmp/chk.bin $BASE/096_cc_network_4g_signal_4.bin + +echo "== cc_network_4g_no_signal == +dd if=$BASE/097_cc_network_4g_no_signal.bin of=$DEV bs=4 seek=48309504 count=835 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=48309504 count=835 +cmp /tmp/chk.bin $BASE/097_cc_network_4g_no_signal.bin + +echo "== sys_notifications_car == +dd if=$BASE/098_sys_notifications_car.bin of=$DEV bs=4 seek=37707392 count=1315 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=37707392 count=1315 +cmp /tmp/chk.bin $BASE/098_sys_notifications_car.bin + +echo "== sys_notifications_default == +dd if=$BASE/099_sys_notifications_default.bin of=$DEV bs=4 seek=37698688 count=8003 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=37698688 count=8003 +cmp /tmp/chk.bin $BASE/099_sys_notifications_default.bin + +echo "== sys_notifications_default_small == +dd if=$BASE/100_sys_notifications_default_small.bin of=$DEV bs=4 seek=37697792 count=835 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=37697792 count=835 +cmp /tmp/chk.bin $BASE/100_sys_notifications_default_small.bin + +echo "== sys_notifications_half_going == +dd if=$BASE/101_sys_notifications_half_going.bin of=$DEV bs=4 seek=37694080 count=763 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=37694080 count=763 +cmp /tmp/chk.bin $BASE/101_sys_notifications_half_going.bin + +echo "== sys_notifications_half_point == +dd if=$BASE/102_sys_notifications_half_point.bin of=$DEV bs=4 seek=37693696 count=323 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=37693696 count=323 +cmp /tmp/chk.bin $BASE/102_sys_notifications_half_point.bin + +echo "== sys_notifications_no_message == +dd if=$BASE/103_sys_notifications_no_message.bin of=$DEV bs=4 seek=37691776 count=1859 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=37691776 count=1859 +cmp /tmp/chk.bin $BASE/103_sys_notifications_no_message.bin + +echo "== sys_phone_icon_app_h_w_48 == +dd if=$BASE/104_sys_phone_icon_app_h_w_48.bin of=$DEV bs=4 seek=32112512 count=835 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=32112512 count=835 +cmp /tmp/chk.bin $BASE/104_sys_phone_icon_app_h_w_48.bin + +echo "== sys_phone_icon_dial == +dd if=$BASE/105_sys_phone_icon_dial.bin of=$DEV bs=4 seek=32111616 count=835 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=32111616 count=835 +cmp /tmp/chk.bin $BASE/105_sys_phone_icon_dial.bin + +echo "== sys_phone_icon_hash == +dd if=$BASE/106_sys_phone_icon_hash.bin of=$DEV bs=4 seek=32110720 count=835 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=32110720 count=835 +cmp /tmp/chk.bin $BASE/106_sys_phone_icon_hash.bin + +echo "== sys_phone_icon_headphone_icon == +dd if=$BASE/107_sys_phone_icon_headphone_icon.bin of=$DEV bs=4 seek=32109312 count=1283 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=32109312 count=1283 +cmp /tmp/chk.bin $BASE/107_sys_phone_icon_headphone_icon.bin + +echo "== sys_phone_icon_keypad_icon == +dd if=$BASE/108_sys_phone_icon_keypad_icon.bin of=$DEV bs=4 seek=32106496 count=1283 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=32106496 count=1283 +cmp /tmp/chk.bin $BASE/108_sys_phone_icon_keypad_icon.bin + +echo "== sys_phone_icon_mic_off == +dd if=$BASE/109_sys_phone_icon_mic_off.bin of=$DEV bs=4 seek=32097408 count=835 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=32097408 count=835 +cmp /tmp/chk.bin $BASE/109_sys_phone_icon_mic_off.bin + +echo "== sys_phone_icon_mic_on == +dd if=$BASE/110_sys_phone_icon_mic_on.bin of=$DEV bs=4 seek=32096512 count=835 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=32096512 count=835 +cmp /tmp/chk.bin $BASE/110_sys_phone_icon_mic_on.bin + +echo "== sys_phone_icon_no_calllog == +dd if=$BASE/111_sys_phone_icon_no_calllog.bin of=$DEV bs=4 seek=32094592 count=1859 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=32094592 count=1859 +cmp /tmp/chk.bin $BASE/111_sys_phone_icon_no_calllog.bin + +echo "== sys_phone_icon_no_contacts == +dd if=$BASE/112_sys_phone_icon_no_contacts.bin of=$DEV bs=4 seek=32092672 count=1859 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=32092672 count=1859 +cmp /tmp/chk.bin $BASE/112_sys_phone_icon_no_contacts.bin + +echo "== sys_phone_icon_phone_icon == +dd if=$BASE/113_sys_phone_icon_phone_icon.bin of=$DEV bs=4 seek=32090368 count=1283 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=32090368 count=1283 +cmp /tmp/chk.bin $BASE/113_sys_phone_icon_phone_icon.bin + +echo "== sys_phone_icon_phone_icon_small == +dd if=$BASE/114_sys_phone_icon_phone_icon_small.bin of=$DEV bs=4 seek=32089728 count=515 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=32089728 count=515 +cmp /tmp/chk.bin $BASE/114_sys_phone_icon_phone_icon_small.bin + +echo "== sys_phone_icon_plus == +dd if=$BASE/115_sys_phone_icon_plus.bin of=$DEV bs=4 seek=32088832 count=835 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=32088832 count=835 +cmp /tmp/chk.bin $BASE/115_sys_phone_icon_plus.bin + +echo "== sys_phone_icon_spk_icon_64 == +dd if=$BASE/116_sys_phone_icon_spk_icon_64.bin of=$DEV bs=4 seek=32084224 count=1283 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=32084224 count=1283 +cmp /tmp/chk.bin $BASE/116_sys_phone_icon_spk_icon_64.bin + +echo "== sys_phone_icon_star == +dd if=$BASE/117_sys_phone_icon_star.bin of=$DEV bs=4 seek=32083328 count=835 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=32083328 count=835 +cmp /tmp/chk.bin $BASE/117_sys_phone_icon_star.bin + +echo "== sys_phone_icon_watch_icon == +dd if=$BASE/118_sys_phone_icon_watch_icon.bin of=$DEV bs=4 seek=32081920 count=1283 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=32081920 count=1283 +cmp /tmp/chk.bin $BASE/118_sys_phone_icon_watch_icon.bin + +echo "== sys_phone_icon_watch_icon_small == +dd if=$BASE/119_sys_phone_icon_watch_icon_small.bin of=$DEV bs=4 seek=32081280 count=515 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=32081280 count=515 +cmp /tmp/chk.bin $BASE/119_sys_phone_icon_watch_icon_small.bin + +echo "== sys_phone_widget_widget_contact == +dd if=$BASE/120_sys_phone_widget_widget_contact.bin of=$DEV bs=4 seek=32026624 count=3395 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=32026624 count=3395 +cmp /tmp/chk.bin $BASE/120_sys_phone_widget_widget_contact.bin + +echo "== sys_phone_widget_widget_dialpad == +dd if=$BASE/121_sys_phone_widget_widget_dialpad.bin of=$DEV bs=4 seek=32023168 count=3395 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=32023168 count=3395 +cmp /tmp/chk.bin $BASE/121_sys_phone_widget_widget_dialpad.bin + +echo "== sys_phone_widget_widget_phone == +dd if=$BASE/122_sys_phone_widget_widget_phone.bin of=$DEV bs=4 seek=32019712 count=3395 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=32019712 count=3395 +cmp /tmp/chk.bin $BASE/122_sys_phone_widget_widget_phone.bin + +echo "== sys_recorder_ahead3 == +dd if=$BASE/123_sys_recorder_ahead3.bin of=$DEV bs=4 seek=27191936 count=1859 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=27191936 count=1859 +cmp /tmp/chk.bin $BASE/123_sys_recorder_ahead3.bin + +echo "== sys_recorder_back3 == +dd if=$BASE/124_sys_recorder_back3.bin of=$DEV bs=4 seek=27190016 count=1859 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=27190016 count=1859 +cmp /tmp/chk.bin $BASE/124_sys_recorder_back3.bin + +echo "== sys_recorder_main == +dd if=$BASE/125_sys_recorder_main.bin of=$DEV bs=4 seek=27168256 count=835 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=27168256 count=835 +cmp /tmp/chk.bin $BASE/125_sys_recorder_main.bin + +echo "== sys_recorder_mark == +dd if=$BASE/126_sys_recorder_mark.bin of=$DEV bs=4 seek=27167360 count=835 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=27167360 count=835 +cmp /tmp/chk.bin $BASE/126_sys_recorder_mark.bin + +echo "== sys_recorder_mute == +dd if=$BASE/127_sys_recorder_mute.bin of=$DEV bs=4 seek=27165568 count=835 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=27165568 count=835 +cmp /tmp/chk.bin $BASE/127_sys_recorder_mute.bin + +echo "== sys_recorder_sound == +dd if=$BASE/128_sys_recorder_sound.bin of=$DEV bs=4 seek=27163776 count=835 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=27163776 count=835 +cmp /tmp/chk.bin $BASE/128_sys_recorder_sound.bin + +echo "== sys_recorder_trash == +dd if=$BASE/129_sys_recorder_trash.bin of=$DEV bs=4 seek=27152128 count=835 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=27152128 count=835 +cmp /tmp/chk.bin $BASE/129_sys_recorder_trash.bin + +echo "== sys_recorder_warning == +dd if=$BASE/130_sys_recorder_warning.bin of=$DEV bs=4 seek=27150208 count=1859 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=27150208 count=1859 +cmp /tmp/chk.bin $BASE/130_sys_recorder_warning.bin + +echo "== sys_timer_icon_close == +dd if=$BASE/131_sys_timer_icon_close.bin of=$DEV bs=4 seek=15576448 count=835 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=15576448 count=835 +cmp /tmp/chk.bin $BASE/131_sys_timer_icon_close.bin + +echo "== sys_timer_icon_pause == +dd if=$BASE/132_sys_timer_icon_pause.bin of=$DEV bs=4 seek=15574656 count=835 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=15574656 count=835 +cmp /tmp/chk.bin $BASE/132_sys_timer_icon_pause.bin + +echo "== sys_timer_icon_restart == +dd if=$BASE/133_sys_timer_icon_restart.bin of=$DEV bs=4 seek=15573760 count=835 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=15573760 count=835 +cmp /tmp/chk.bin $BASE/133_sys_timer_icon_restart.bin + +echo "== sys_timer_icon_start == +dd if=$BASE/134_sys_timer_icon_start.bin of=$DEV bs=4 seek=15506304 count=835 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=15506304 count=835 +cmp /tmp/chk.bin $BASE/134_sys_timer_icon_start.bin + +echo "== sys_timer_icon_stop == +dd if=$BASE/135_sys_timer_icon_stop.bin of=$DEV bs=4 seek=15505408 count=835 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=15505408 count=835 +cmp /tmp/chk.bin $BASE/135_sys_timer_icon_stop.bin + +echo "== sys_timer_icon_widget_light12 == +dd if=$BASE/136_sys_timer_icon_widget_light12.bin of=$DEV bs=4 seek=15474176 count=30211 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=15474176 count=30211 +cmp /tmp/chk.bin $BASE/136_sys_timer_icon_widget_light12.bin + +echo "== sys_timer_icon_widget_light12_five == +dd if=$BASE/137_sys_timer_icon_widget_light12_five.bin of=$DEV bs=4 seek=15443840 count=30211 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=15443840 count=30211 +cmp /tmp/chk.bin $BASE/137_sys_timer_icon_widget_light12_five.bin + +echo "== sys_timer_icon_widget_pause == +dd if=$BASE/138_sys_timer_icon_widget_pause.bin of=$DEV bs=4 seek=15442944 count=835 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=15442944 count=835 +cmp /tmp/chk.bin $BASE/138_sys_timer_icon_widget_pause.bin + +echo "== sys_timer_icon_widget_start == +dd if=$BASE/139_sys_timer_icon_widget_start.bin of=$DEV bs=4 seek=15442048 count=835 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=15442048 count=835 +cmp /tmp/chk.bin $BASE/139_sys_timer_icon_widget_start.bin + +echo "== sys_todolist_checked == +dd if=$BASE/140_sys_todolist_checked.bin of=$DEV bs=4 seek=15420288 count=835 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=15420288 count=835 +cmp /tmp/chk.bin $BASE/140_sys_todolist_checked.bin + +echo "== sys_todolist_create_todo_btn == +dd if=$BASE/141_sys_todolist_create_todo_btn.bin of=$DEV bs=4 seek=15416064 count=4099 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=15416064 count=4099 +cmp /tmp/chk.bin $BASE/141_sys_todolist_create_todo_btn.bin + +echo "== sys_todolist_dark12_bg == +dd if=$BASE/142_sys_todolist_dark12_bg.bin of=$DEV bs=4 seek=15400192 count=15747 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=15400192 count=15747 +cmp /tmp/chk.bin $BASE/142_sys_todolist_dark12_bg.bin + +echo "== sys_todolist_empty == +dd if=$BASE/143_sys_todolist_empty.bin of=$DEV bs=4 seek=15398272 count=1859 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=15398272 count=1859 +cmp /tmp/chk.bin $BASE/143_sys_todolist_empty.bin + +echo "== sys_todolist_reminder_big == +dd if=$BASE/144_sys_todolist_reminder_big.bin of=$DEV bs=4 seek=15372928 count=4355 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=15372928 count=4355 +cmp /tmp/chk.bin $BASE/144_sys_todolist_reminder_big.bin + +echo "== sys_todolist_reminder_small == +dd if=$BASE/145_sys_todolist_reminder_small.bin of=$DEV bs=4 seek=15371008 count=1859 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=15371008 count=1859 +cmp /tmp/chk.bin $BASE/145_sys_todolist_reminder_small.bin + +echo "== sys_todolist_tdl_dark11_bg == +dd if=$BASE/146_sys_todolist_tdl_dark11_bg.bin of=$DEV bs=4 seek=15362944 count=8003 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=15362944 count=8003 +cmp /tmp/chk.bin $BASE/146_sys_todolist_tdl_dark11_bg.bin + +echo "== sys_todolist_tdl_light11_bg == +dd if=$BASE/147_sys_todolist_tdl_light11_bg.bin of=$DEV bs=4 seek=15354880 count=8003 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=15354880 count=8003 +cmp /tmp/chk.bin $BASE/147_sys_todolist_tdl_light11_bg.bin + +echo "== sys_todolist_unchecked == +dd if=$BASE/148_sys_todolist_unchecked.bin of=$DEV bs=4 seek=15353984 count=835 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=15353984 count=835 +cmp /tmp/chk.bin $BASE/148_sys_todolist_unchecked.bin + +echo "== sys_todolist_widget_dark_icon == +dd if=$BASE/149_sys_todolist_widget_dark_icon.bin of=$DEV bs=4 seek=15352064 count=1859 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=15352064 count=1859 +cmp /tmp/chk.bin $BASE/149_sys_todolist_widget_dark_icon.bin + +echo "== sys_todolist_widget_light_icon == +dd if=$BASE/150_sys_todolist_widget_light_icon.bin of=$DEV bs=4 seek=15350144 count=1859 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=15350144 count=1859 +cmp /tmp/chk.bin $BASE/150_sys_todolist_widget_light_icon.bin + +rm /tmp/chk.bin +echo "all done - run reboot to apply" diff --git a/s4e-icons/main.lua b/s4e-icons/main.lua new file mode 100644 index 0000000..91b801e --- /dev/null +++ b/s4e-icons/main.lua @@ -0,0 +1,639 @@ +--[[---------------------------------------------------------------------------- + OS4 Icons —— S5 图标包 -> S4 eSIM + + 界面按 CSS 设计稿 1:1 落地(本机用真 MiSans 字体渲染预览验证过): + * 页面底色纯黑(取自封面图四角) + * rgba(255,255,255,0.6) -> #999999(白 60% 叠黑) + * font-weight 600 -> MiSans-Semibold ; 500 -> MiSans-Medium + * vertical-align:middle -> 手工算 y(LVGL 标签没有垂直居中样式) + * 多色文本用 LVGL recolor:"灰#ffffff 白#灰" + * 圆屏安全区:实测所有页面 0 像素越界(大按钮全靠 40px 圆角收进圆内) + + 真机已验证的底层能力: + * 宿主不调用 ui.init -> 加载期建界面 + * SCRIPT_PATH 为空 -> 用 debug.getinfo 反推目录 + * .face 不解包,资源仍在 resource.bin 里 -> 按偏移读出来用 + * /dev/app 可写,dd 读写校验通过 + * 没有 utf8 标准库 -> 自带按位 UTF-8 解码(本文件未用到,留作提醒) +------------------------------------------------------------------------------]] + +local lvgl = require("lvgl") + +local W, H = 466, 466 +local DEV, BLK = "/dev/app", 4 +local TMPH = "/tmp/os4_head.bin" +local TMPR = "/tmp/os4_back.bin" + +-- 等长覆盖只保证「偏移」512 对齐;RLE 素材的长度常常不是 4 的倍数(如 1635 / 3047), +-- 那种记录必须退化成 bs=1(偏移本身对齐,所以 bs=1 也精确),否则会多写相邻文件的字节。 +local function ddBlk(off, len) + if off % BLK == 0 and len % BLK == 0 then return BLK end + return 1 +end +local TMPB = "/tmp/os4_blob.bin" + +--------------------------------------------------------------------- 自身目录 +local SRC = "" +if debug and debug.getinfo then + local info = debug.getinfo(1, "S") + SRC = (info and info.source) or "" +end +if type(SRC) ~= "string" then SRC = "" end +if SRC:sub(1, 1) == "@" then SRC = SRC:sub(2) end +SRC = SRC:gsub("\\", "/") +local THIS_DIR = SRC:match("^(.*)/[^/]+$") or "" +local SP = THIS_DIR ~= "" and (THIS_DIR .. "/") or "" +local PARENT = SP:gsub("[^/]+/$", "") + +local RES = nil +do + local c = { PARENT .. "resource.bin", SP .. "../resource.bin", SP .. "resource.bin" } + for i = 1, #c do + local f = io.open(c[i], "rb") + if f then f:close(); RES = c[i]; break end + end +end + +--------------------------------------------------------------------- 资源偏移 +-- UI 图片 { resource.bin 内偏移, 长度, 名字 } —— 由 _tools/gen_ui_lua.py 回填 +local UIIMG = { +__UIIMG__ +} + +-- 负载表 { resource 内偏移, ROMFS 目标偏移, 长度 } —— 同样由脚本回填 +local PAYTBL = { +__PAY__ +__RB__ +} + +local function prepImages() + local paths = {} + if not RES then return paths end + local f = io.open(RES, "rb") + if not f then return paths end + for i = 1, #UIIMG do + local e = UIIMG[i] + f:seek("set", e[1]) + local d = f:read(e[2]) + if d and #d == e[2] then + -- 直接写到 /tmp 根下:不能写子目录(目录不存在,且这台机器的 Lua 没有 mkdir) + local p = "/tmp/ui_" .. e[3] .. ".bin" + local o = io.open(p, "wb") + if o then o:write(d); o:close(); paths[e[3]] = p end + end + end + f:close() + return paths +end + +local IMG = prepImages() + +--------------------------------------------------------------------- 颜色与字体 +local C_GRAY, C_WHITE = '#999999', '#ffffff' +local C_OK, C_ERR = '#43cf7c', '#d43030' +local C_PLATE, C_CIRC = 0x002927, 0x4f4f4f + +local function getFont(name, size) + local f = nil + pcall(function() f = lvgl.Font(name, size) end) + return f +end + +local F_CLOCK = getFont('MiSans-Semibold', 28) +local F_TITLE = getFont('MiSans-Medium', 30) +local F_ITEM = getFont('MiSans-Medium', 28) +local F_BODY = getFont('MiSans-Medium', 24) +local F_VER = getFont('MiSans-Medium', 20) + +--------------------------------------------------------------------- 按下高亮 +-- onPressed / onClicked 是真实的 LVGL 对象方法(就在 onevent 旁边,属性表里可查) +local HL = 0x005149 -- 按下高亮色(比底板 0x002927 明显亮) +local pressed = nil -- { obj = , t = } +local tickCount = 0 + +local function setBg(obj, c) + pcall(function() obj:set { bg_color = c } end) +end + +local function pressRelease() + if pressed then + setBg(pressed.obj, C_PLATE) + pressed = nil + end +end + +--------------------------------------------------------------------- 基础 +local ui = {} +local built = false +local pages = {} +local clocks = {} +local job = { run = false, list = nil, i = 0, errs = 0, variant = 'replace' } +-- 从哪个入口进"开始替换"确认页,确认按钮就执行对应操作: +-- 首页「开始替换」 -> 'replace' +-- 帮助页「回滚修改」 -> 'rollback'(也要先过一遍警告确认,不直接开跑) +local pendingVariant = 'replace' +-- 确认页是从哪进来的,返回就回哪: +-- 首页「开始替换」 -> 'home' ; 帮助页「回滚修改」 -> 'help' +local confirmFrom = 'home' + +local function newPage() + local p = nil + pcall(function() + p = lvgl.Object(nil, { x = 0, y = 0, w = W, h = H, bg_color = 0x000000, + bg_opa = 255, border_width = 0, pad_all = 0 }) + p:clear_flag(lvgl.FLAG.SCROLLABLE) + p:add_flag(lvgl.FLAG.EVENT_BUBBLE) + end) + return p +end + +local function show(p) + if not p then return end + pressRelease() -- 切页时清掉可能残留的高亮 + for k, v in pairs(pages) do + pcall(function() v:add_flag(lvgl.FLAG.HIDDEN) end) + end + pcall(function() p:clear_flag(lvgl.FLAG.HIDDEN) end) +end + +local function bind(obj, fn) + if not obj then return end + -- 成品样本用的是 onevent(lvgl.EVENT.SHORT_CLICKED, fn)(CLICKED 这个常量 + -- 在这台机器上不存在),所以按 SHORT_CLICKED -> onClicked -> CLICKED 依次试 + local ev = lvgl.EVENT + if ev and ev.SHORT_CLICKED then + if pcall(function() obj:onevent(ev.SHORT_CLICKED, function() fn() end) end) then return end + end + if pcall(function() obj:onClicked(function() fn() end) end) then return end + pcall(function() obj:onevent(ev and ev.CLICKED, fn) end) +end + +--------------------------------------------------------------------- 点按 +-- 点按 = 按下高亮 + 抬起恢复 + 执行动作;带超时兜底(手指滑出时 click 不触发) +local function bindTap(obj, onTap) + if not obj then return end + local ok = pcall(function() + obj:onPressed(function() + setBg(obj, HL) + pressed = { obj = obj, t = tickCount } + end) + obj:onClicked(function() + pressRelease() + if onTap then onTap() end + end) + end) + if not ok then bind(obj, onTap) end +end + +local function setClickable(o) + if not o then return end + pcall(function() o:add_flag(lvgl.FLAG.CLICKABLE) end) +end + +local function lab(parent, o) + o.text_font = o.text_font or F_BODY + local l = nil + pcall(function() l = parent:Label(o) end) + return l +end + +local function clockLabel(parent) + -- 居中不用 text_align(它要的是 lv_text_align_t:LEFT=0/CENTER=1/RIGHT=2, + -- 传 ALIGN.CENTER=9 越界会被忽略 -> 实测变成左对齐)。 + -- 改为按真字体量出的文本宽度手工算 x,并把 w 收紧,这样对齐方式就不影响结果。 + local l = lab(parent, { x = 195, y = 3, w = 86, h = 45, text = '--:--', + text_color = C_GRAY, font_size = 28, text_font = F_CLOCK }) + clocks[#clocks + 1] = l + return l +end + +local function pageTitle(parent, text, x, w) + -- 标题 y:框 y=34 h=45,字号 30 行高约 41 -> 34 + (45-41)/2 = 36 + return lab(parent, { x = x, y = 36, w = w, h = 45, text = text, + text_color = C_WHITE, font_size = 30, text_font = F_TITLE }) +end + +local function backArrow(parent, x, onTap) + local im = nil + if IMG.back then + pcall(function() im = parent:Image { src = IMG.back, x = x or 160, y = 43 } end) + setClickable(im) + bind(im, onTap) + end + return im +end + +-- 列表项:底板(整块可点) + 圆形遮罩 + 图标 + 标题 +-- pad_all=0 是关键:LVGL 主题会给 Object 加内边距,不清零的话子元素会整体右下偏移 +local function listItem(parent, y, iconkey, title, onTap) + local plate = nil + pcall(function() + plate = parent:Object { x = 49, y = y, w = 368, h = 88, pad_all = 0, + bg_color = C_PLATE, border_width = 0, radius = 34 } + end) + if plate then + if onTap then + setClickable(plate) + bindTap(plate, onTap) + end + pcall(function() + plate:Object { x = 20, y = 19, w = 50, h = 50, + bg_color = C_CIRC, border_width = 0, radius = 25 } + end) + if IMG[iconkey] then + pcall(function() plate:Image { src = IMG[iconkey], x = 30, y = 29 } end) + end + -- 标题框 h=54,字号 28 行高约 38 -> 垂直居中 y = 17 + 8 = 25(左对齐,不用 text_align) + lab(plate, { x = 80, y = 25, w = 208, h = 54, text = title, + text_color = C_WHITE, font_size = 28, text_font = F_ITEM }) + end + return plate +end + +local function bigButton(parent, text) + local b = nil + pcall(function() + b = parent:Object { x = 108, y = 365, w = 250, h = 80, pad_all = 0, + bg_color = C_PLATE, border_width = 0, radius = 40 } + end) + if b then setClickable(b) end + -- 按钮文字:框 (65,13,120,54) 相对底板;字号 28 行高约 38 -> y = 13 + 8 = 21 + -- "确认替换"/"重启手表" 量得 112px -> 居中 x = 65 + (120-112)/2 = 69 + lab(b, { x = 69, y = 21, w = 122, h = 54, text = text, + text_color = C_WHITE, font_size = 28, text_font = F_ITEM }) + return b +end + +local function clockTick() + local t = '--:--' + if os and type(os.date) == "function" then + local ok, s = pcall(os.date, "%H:%M") + if ok and type(s) == "string" then t = s end + end + for i = 1, #clocks do + pcall(function() clocks[i]:set { text = t } end) + end +end + +--------------------------------------------------------------------- 外壳 +local function slurp(p, n) + local f = io.open(p, "rb") + if not f then return nil end + local d = f:read(n or "*a") + f:close() + return d +end + +local function sh(cmd) + if type(os.execute) ~= "function" then return -1 end + local a, b, c = os.execute(cmd) + if type(a) == "number" then return a end + if a == true then return 0 end + if type(c) == "number" then return c end + if type(b) == "number" then return b end + return -1 +end + +--------------------------------------------------------------------- 预检查 +local desc1 = nil + +local function setDesc1(txt, color) + if not desc1 then return end + pcall(function() desc1:set { text = txt, text_color = color } end) +end + +local function doCheck() + if not RES then + setDesc1('找不到 resource.bin,无法操作。', C_ERR) + return false + end + if sh("dd if=" .. DEV .. " of=/tmp/os4_hdr.bin bs=1 count=48") ~= 0 then + setDesc1('读取分区失败(dd 不可用或无权限)。', C_ERR) + return false + end + local h = slurp("/tmp/os4_hdr.bin", 48) + if not h or #h < 48 then + setDesc1('读不到分区头。', C_ERR) + return false + end + if h:sub(1, 8) ~= "-rom1fs-" then + setDesc1('预检查失败:分区头不是 ROMFS。', C_ERR) + return false + end + local z = h:find("\0", 17, true) + local vol = h:sub(17, (z or 33) - 1) + if vol ~= "app" then + setDesc1('预检查失败:卷名是 ' .. vol .. '。', C_ERR) + return false + end + local size = string.unpack(" size then bad = bad + 1 end + total = total + r[3] + end + if bad > 0 then + setDesc1(string.format('预检查失败:%d 项越界或未对齐。', bad), C_ERR) + return false + end + -- 顺带确认 resource.bin 第一条能读出来 + local f = io.open(RES, "rb") + if f then + f:seek("set", list[1][1]) + local probe = f:read(list[1][3]) + f:close() + if not probe or #probe ~= list[1][3] then + setDesc1('预检查失败:resource.bin 读取异常。', C_ERR) + return false + end + end + setDesc1(string.format('预检查通过 | %d 条 | %.2fMB\n您可以返回主页开始修改。', + #list, total / 1048576), C_OK) + return true +end + +--------------------------------------------------------------------- 替换 +local logLabel = nil +local rebootBtn = nil +local logLines = {} + +local function logOut(s) + logLines[#logLines + 1] = s + while #logLines > 8 do table.remove(logLines, 1) end + if logLabel then + pcall(function() logLabel:set { text = table.concat(logLines, "\n") } end) + end +end + +local function showReboot() + if rebootBtn then pcall(function() rebootBtn:clear_flag(lvgl.FLAG.HIDDEN) end) end +end + +local function readChunk(off, len, path, bsz) + if os.remove then pcall(os.remove, path) end + sh(string.format("dd if=%s of=%s bs=%d skip=%d count=%d", + DEV, path, bsz, off // bsz, len // bsz)) + return slurp(path, len) +end + +-- 每条三步:① 版本校验(设备上那段必须等于「原厂」或「替换后」)② 写入 ③ 整段回读比对 +local function writeOne(rec, want) + local f = io.open(RES, "rb") + if not f then return 'err' end + f:seek("set", rec[1]) + local data = f:read(rec[3]) + f:close() + if not data or #data ~= rec[3] then return 'err' end + if want then + local n = math.min(64, rec[3], want[3]) + local wf = io.open(RES, "rb") + wf:seek("set", want[1]) + local wdata = wf:read(n) + wf:close() + local cur = readChunk(rec[2], n, TMPH, 1) + if not cur or #cur ~= n or not wdata or #wdata ~= n then return 'err' end + if cur ~= data:sub(1, n) and cur ~= wdata then return 'ver' end + end + local o = io.open(TMPB, "wb") + if not o then return 'err' end + o:write(data) + o:close() + local b = ddBlk(rec[2], rec[3]) + sh(string.format("dd if=%s of=%s bs=%d seek=%d count=%d", + TMPB, DEV, b, rec[2] // b, rec[3] // b)) + local back = readChunk(rec[2], rec[3], TMPR, b) + if not back or #back ~= rec[3] or back ~= data then return 'err' end + return 'ok' +end + +local function startJob(variant) + if job.run then return end + local list = (variant == 'rollback') and PAYTBL.rb or PAYTBL.pay + if not list or #list == 0 then + setDesc1('负载表为空,已中止。', C_ERR) + return + end + job.run, job.list, job.i, job.errs, job.variant = true, list, 0, 0, variant + logLines = {} + logOut(variant == 'rollback' and '> 开始回滚' or '> 开始替换') + logOut('> 读取 resource.bin') + if rebootBtn then pcall(function() rebootBtn:add_flag(lvgl.FLAG.HIDDEN) end) end + show(pages.running) +end + +local function tick() + tickCount = tickCount + 1 + -- 兜底:按下超过 1.6s 没收到 click(手指滑出等)就恢复高亮 + if pressed and (tickCount - pressed.t) > 40 then pressRelease() end + if not job.run or not job.list then return end + job.i = job.i + 1 + local n = #job.list + if job.i > n then + job.run = false + local s = (job.variant == 'rollback') + and '> 回滚完毕,请重启手表。' + or '> 替换完毕,请重启手表。若发现问题,请回滚修改。' + logOut(s) + logOut(string.format('> 共 %d 条,失败 %d 条%s', n, job.errs, + (job.vers or 0) > 0 and string.format(',版本不符跳过 %d 条', job.vers) or '')) + showReboot() + return + end + local rb = (job.variant == 'rollback') and PAYTBL.pay or PAYTBL.rb + local r = writeOne(job.list[job.i], rb and rb[job.i]) + if r == 'ver' then + job.vers = (job.vers or 0) + 1 + elseif r ~= 'ok' then + job.errs = job.errs + 1 + end + if job.i % 4 == 0 or job.i == n then + logOut(string.format('> 写入 %d/%d 失败 %d', job.i, n, job.errs)) + end +end + +--------------------------------------------------------------------- 页面 +local function buildHome() + local p = newPage() + if not p then return nil end + clockLabel(p) + pageTitle(p, 'OS4 Icons', 159, 158) + listItem(p, 85, 'ic_help', '快速帮助', function() show(pages.help) end) + listItem(p, 181, 'ic_replace', '开始替换', function() + pendingVariant = 'replace' + confirmFrom = 'home' + show(pages.confirm) + end) + listItem(p, 277, 'ic_about', '关于', function() show(pages.about) end) + return p +end + +local function buildHelp() + local p = newPage() + if not p then return nil end + clockLabel(p) + backArrow(p, 160, function() show(pages.home) end) + local t = pageTitle(p, '快速帮助', 183, 130) + setClickable(t) + bind(t, function() show(pages.home) end) + + listItem(p, 85, 'ic_check', '环境预检查', function() doCheck() end) + desc1 = lab(p, { x = 65, y = 176, w = 338, h = 72, + text = '检查原始资源状态、分区可写情况等。', + text_color = C_GRAY, font_size = 24, text_font = F_BODY }) + + -- 回滚修改:不直接开跑,同样先跳到"开始替换"确认页,确认后执行的是回滚 + listItem(p, 256, 'ic_rollback', '回滚修改', function() + pendingVariant = 'rollback' + confirmFrom = 'help' + show(pages.confirm) + end) + lab(p, { x = 65, y = 344, w = 334, h = 72, + text = '将图标恢复到系统原始样式。', + text_color = C_GRAY, font_size = 24, text_font = F_BODY }) + return p +end + +local function buildConfirm() + local p = newPage() + if not p then return nil end + clockLabel(p) + -- 确认页的返回:回到进来时的那个页面(首页 或 快速帮助) + local function goBack() show(pages[confirmFrom] or pages.home) end + backArrow(p, 160, goBack) + local t = pageTitle(p, '开始替换', 183, 130) + setClickable(t) + bind(t, goBack) + + -- 多色文本**不能**用 recolor:AP 里那几处 recolor 是图片重着色(bg_img_recolor) + -- 和表盘编辑器的主题重着色,不是文本属性;实测标记会被原样显示出来。 + -- 改为拆成多个标签 + 手工算 x(宽度用真 MiSans 24px 量出)。 + -- 折行也显式写死:因为"替换图标包之前,请先确认自己"量得 336px,几乎等于 338px 框宽, + -- 且型号那行 500px 必然折行,只有写死才能让多色片段位置确定。 + lab(p, { x = 64, y = 90, w = 338, h = 70, + text = '替换图标包之前,请先确认自己\n的系统版本号为 ', + text_color = C_GRAY, font_size = 24, text_font = F_BODY }) + lab(p, { x = 241, y = 123, w = 104, h = 33, text = '3.202.79', + text_color = C_WHITE, font_size = 24, text_font = F_BODY }) + lab(p, { x = 340, y = 123, w = 30, h = 33, text = '!', + text_color = C_GRAY, font_size = 24, text_font = F_BODY }) + lab(p, { x = 64, y = 189, w = 338, h = 33, text = '请确认自己的手表型号为', + text_color = C_GRAY, font_size = 24, text_font = F_BODY }) + lab(p, { x = 64, y = 222, w = 338, h = 70, + text = 'Xiaomi Watch S4 eSIM(非\n15 周年纪念版)', + text_color = C_WHITE, font_size = 24, text_font = F_BODY }) + lab(p, { x = 242, y = 255, w = 30, h = 33, text = '!', + text_color = C_GRAY, font_size = 24, text_font = F_BODY }) + lab(p, { x = 64, y = 321, w = 338, h = 33, text = '搞机有风险,变砖后果自负。', + text_color = C_GRAY, font_size = 24, text_font = F_BODY }) + + local b = bigButton(p, '确认替换') + bindTap(b, function() startJob(pendingVariant) end) + return p +end + +local function buildAbout() + local p = newPage() + if not p then return nil end + clockLabel(p) + backArrow(p, 191, function() show(pages.home) end) + local t = pageTitle(p, '关于', 214, 64) + setClickable(t) + bind(t, function() show(pages.home) end) + + -- 信息卡:底板 + 圆形 logo + 大标题 + 版本号 + pcall(function() + p:Object { x = 52, y = 79, w = 362, h = 204, pad_all = 0, + bg_color = C_PLATE, border_width = 0, radius = 30 } + end) + if IMG.logo then + pcall(function() p:Image { src = IMG.logo, x = 194, y = 108 } end) + end + -- "OS4 Icons" 28px 量得 138px -> 居中 x = 66 + (334-138)/2 = 164 + lab(p, { x = 164, y = 192, w = 138, h = 38, text = 'OS4 Icons', + text_color = C_WHITE, font_size = 28, text_font = F_ITEM }) + -- "1.0.0" 20px 量得 45px -> 居中 x = 66 + (334-45)/2 = 210 + lab(p, { x = 210, y = 227, w = 45, h = 27, text = '1.0.0', + text_color = C_GRAY, font_size = 20, text_font = F_VER }) + + -- QQ 联系方式:结构与列表项一致(无动作,所以不给它绑点击) + listItem(p, 291, 'ic_qq', '732681995', nil) + return p +end + +local function buildRunning() + local p = newPage() + if not p then return nil end + clockLabel(p) + -- 本页没有返回图标(规格:顶栏同首页),所以标题要屏幕居中: + -- "正在替换" 30px 量得 120px -> x = 233 - 120/2 = 173(不是帮助页的 183) + pageTitle(p, '正在替换', 173, 130) + logLabel = lab(p, { x = 64, y = 90, w = 338, h = 269, text = '', + text_color = C_WHITE, font_size = 24, text_font = F_BODY }) + rebootBtn = bigButton(p, '重启手表') + -- 替换过程中不显示,替换完再显示 + if rebootBtn then pcall(function() rebootBtn:add_flag(lvgl.FLAG.HIDDEN) end) end + bindTap(rebootBtn, function() + logOut('> 正在重启…') + sh("sync") + sh("reboot") + end) + return p +end + +--------------------------------------------------------------------- 组装 +local function build() + if built then return end + built = true + pages.home = buildHome() + pages.help = buildHelp() + pages.confirm = buildConfirm() + pages.about = buildAbout() + pages.running = buildRunning() + if pages.home then show(pages.home) end + clockTick() + pcall(function() + ui.timer = lvgl.Timer { period = 40, repeat_count = -1, cb = function() tick() end } + if ui.timer and ui.timer.resume then ui.timer:resume() end + end) + pcall(function() + ui.clockTimer = lvgl.Timer { period = 1000, repeat_count = -1, + cb = function() clockTick() end } + if ui.clockTimer and ui.clockTimer.resume then ui.clockTimer:resume() end + end) +end + +local okbuild, errbuild = pcall(build) +if not okbuild then + pcall(function() + local r = lvgl.Object(nil, { x = 0, y = 0, w = W, h = H, bg_color = 0x000000, + bg_opa = 255, border_width = 0 }) + r:Label { x = 20, y = 20, w = W - 40, h = H - 40, + text = 'BUILD FAILED:\n' .. tostring(errbuild), + text_color = '#ffffff', font_size = 13, text_font = F_BODY } + end) +end + +ui.init = function() + pcall(build) + return ui.root +end + +function pageOnPause() + if ui.timer then pcall(function() ui.timer:pause() end) end + if ui.clockTimer then pcall(function() ui.clockTimer:pause() end) end +end + +function pageOnResume() + if ui.timer then pcall(function() ui.timer:resume() end) end + if ui.clockTimer then pcall(function() ui.clockTimer:resume() end) end +end + +return ui diff --git a/s4e-icons/manifest.json b/s4e-icons/manifest.json new file mode 100644 index 0000000..f0a2ff7 --- /dev/null +++ b/s4e-icons/manifest.json @@ -0,0 +1,404 @@ +{ + "target": "Xiaomi Watch S4 eSIM (o62lte) vela_app.bin", + "source": "Xiaomi Watch S5 (q63) v4.101.020 icons", + "image_sha256": "caa66693f49477dde7dc4d22d66c728d905dc5d1238065b39c022846293680af", + "replace_count": 39, + "records": [ + { + "name": "activities", + "offset": 265606144, + "length": 21772, + "s4_format": "I8", + "s4_flags": 0, + "s5_bytes": 15944, + "s5_from": "health", + "method": "re-encode" + }, + { + "name": "alarm", + "offset": 255491584, + "length": 82956, + "s4_format": "ARGB8888", + "s4_flags": 0, + "s5_bytes": 21772, + "s5_from": "app", + "method": "re-encode" + }, + { + "name": "barometer", + "offset": 243424256, + "length": 82956, + "s4_format": "ARGB8888", + "s4_flags": 0, + "s5_bytes": 21772, + "s5_from": "health", + "method": "re-encode" + }, + { + "name": "breath", + "offset": 198528000, + "length": 82956, + "s4_format": "ARGB8888", + "s4_flags": 0, + "s5_bytes": 21772, + "s5_from": "health", + "method": "re-encode" + }, + { + "name": "calendar", + "offset": 197440000, + "length": 82956, + "s4_format": "ARGB8888", + "s4_flags": 0, + "s5_bytes": 21772, + "s5_from": "app", + "method": "re-encode" + }, + { + "name": "chronograph", + "offset": 195329536, + "length": 82956, + "s4_format": "ARGB8888", + "s4_flags": 0, + "s5_bytes": 21772, + "s5_from": "app", + "method": "re-encode" + }, + { + "name": "compass", + "offset": 193598464, + "length": 21772, + "s4_format": "I8", + "s4_flags": 0, + "s5_bytes": 21772, + "s5_from": "app", + "method": "copy" + }, + { + "name": "debug", + "offset": 193044992, + "length": 21772, + "s4_format": "I8", + "s4_flags": 0, + "s5_bytes": 82956, + "s5_from": "app", + "method": "re-encode" + }, + { + "name": "esimsms", + "offset": 192291328, + "length": 82956, + "s4_format": "ARGB8888", + "s4_flags": 0, + "s5_bytes": 82956, + "s5_from": "app", + "method": "copy" + }, + { + "name": "find_phone", + "offset": 191823872, + "length": 82956, + "s4_format": "ARGB8888", + "s4_flags": 0, + "s5_bytes": 21772, + "s5_from": "app", + "method": "re-encode" + }, + { + "name": "flashlight", + "offset": 191740416, + "length": 82956, + "s4_format": "ARGB8888", + "s4_flags": 0, + "s5_bytes": 21772, + "s5_from": "app", + "method": "re-encode" + }, + { + "name": "health_today", + "offset": 170015232, + "length": 82956, + "s4_format": "ARGB8888", + "s4_flags": 0, + "s5_bytes": 21772, + "s5_from": "health", + "method": "re-encode" + }, + { + "name": "heartrate", + "offset": 167684096, + "length": 82956, + "s4_format": "ARGB8888", + "s4_flags": 0, + "s5_bytes": 21772, + "s5_from": "health", + "method": "re-encode" + }, + { + "name": "innovation_research", + "offset": 158267904, + "length": 82956, + "s4_format": "ARGB8888", + "s4_flags": 0, + "s5_bytes": 21772, + "s5_from": "health", + "method": "re-encode" + }, + { + "name": "interconnect", + "offset": 158171136, + "length": 82956, + "s4_format": "ARGB8888", + "s4_flags": 0, + "s5_bytes": 82956, + "s5_from": "app", + "method": "copy" + }, + { + "name": "lua", + "offset": 158028288, + "length": 21772, + "s4_format": "I8", + "s4_flags": 0, + "s5_bytes": 82956, + "s5_from": "app", + "method": "re-encode" + }, + { + "name": "media", + "offset": 157868544, + "length": 82956, + "s4_format": "ARGB8888", + "s4_flags": 0, + "s5_bytes": 21772, + "s5_from": "app", + "method": "re-encode" + }, + { + "name": "mijia", + "offset": 156865024, + "length": 82956, + "s4_format": "ARGB8888", + "s4_flags": 0, + "s5_bytes": 21772, + "s5_from": "app", + "method": "re-encode" + }, + { + "name": "nfccard", + "offset": 155134976, + "length": 82956, + "s4_format": "ARGB8888", + "s4_flags": 0, + "s5_bytes": 21772, + "s5_from": "app", + "method": "re-encode" + }, + { + "name": "oxygen", + "offset": 149982208, + "length": 82956, + "s4_format": "ARGB8888", + "s4_flags": 0, + "s5_bytes": 21772, + "s5_from": "health", + "method": "re-encode" + }, + { + "name": "perpetual_calendar", + "offset": 129220096, + "length": 21772, + "s4_format": "I8", + "s4_flags": 0, + "s5_bytes": 21772, + "s5_from": "app", + "method": "copy" + }, + { + "name": "phone", + "offset": 128241664, + "length": 82956, + "s4_format": "ARGB8888", + "s4_flags": 0, + "s5_bytes": 21772, + "s5_from": "app", + "method": "re-encode" + }, + { + "name": "phone_contacts", + "offset": 128459264, + "length": 82956, + "s4_format": "ARGB8888", + "s4_flags": 0, + "s5_bytes": 21772, + "s5_from": "app", + "method": "re-encode" + }, + { + "name": "pressure", + "offset": 126911488, + "length": 82956, + "s4_format": "ARGB8888", + "s4_flags": 0, + "s5_bytes": 21772, + "s5_from": "health", + "method": "re-encode" + }, + { + "name": "recorder", + "offset": 108676608, + "length": 82956, + "s4_format": "ARGB8888", + "s4_flags": 0, + "s5_bytes": 21772, + "s5_from": "app", + "method": "re-encode" + }, + { + "name": "remote_camera", + "offset": 106510848, + "length": 21772, + "s4_format": "I8", + "s4_flags": 0, + "s5_bytes": 21772, + "s5_from": "app", + "method": "copy" + }, + { + "name": "settings", + "offset": 86933504, + "length": 82956, + "s4_format": "ARGB8888", + "s4_flags": 0, + "s5_bytes": 21772, + "s5_from": "app", + "method": "re-encode" + }, + { + "name": "sleep", + "offset": 85287936, + "length": 82956, + "s4_format": "ARGB8888", + "s4_flags": 0, + "s5_bytes": 21772, + "s5_from": "health", + "method": "re-encode" + }, + { + "name": "sports", + "offset": 79359488, + "length": 82956, + "s4_format": "ARGB8888", + "s4_flags": 0, + "s5_bytes": 82956, + "s5_from": "health", + "method": "copy" + }, + { + "name": "sports_course", + "offset": 85140480, + "length": 82956, + "s4_format": "ARGB8888", + "s4_flags": 0, + "s5_bytes": 82956, + "s5_from": "health", + "method": "copy" + }, + { + "name": "sports_record", + "offset": 78000128, + "length": 82956, + "s4_format": "ARGB8888", + "s4_flags": 0, + "s5_bytes": 82956, + "s5_from": "health", + "method": "copy" + }, + { + "name": "sports_training", + "offset": 73838592, + "length": 82956, + "s4_format": "ARGB8888", + "s4_flags": 0, + "s5_bytes": 82956, + "s5_from": "health", + "method": "copy" + }, + { + "name": "timer", + "offset": 61684736, + "length": 82956, + "s4_format": "ARGB8888", + "s4_flags": 0, + "s5_bytes": 21772, + "s5_from": "app", + "method": "re-encode" + }, + { + "name": "todolist", + "offset": 61509632, + "length": 82956, + "s4_format": "ARGB8888", + "s4_flags": 0, + "s5_bytes": 21772, + "s5_from": "app", + "method": "re-encode" + }, + { + "name": "voice_aivs", + "offset": 54641664, + "length": 82956, + "s4_format": "ARGB8888", + "s4_flags": 0, + "s5_bytes": 17493, + "s5_from": "app", + "method": "re-encode" + }, + { + "name": "voice_alexa", + "offset": 53452800, + "length": 6005, + "s4_format": "I8", + "s4_flags": 8, + "s5_bytes": 6005, + "s5_from": "app", + "method": "copy" + }, + { + "name": "weather", + "offset": 25027072, + "length": 82956, + "s4_format": "ARGB8888", + "s4_flags": 0, + "s5_bytes": 21772, + "s5_from": "app", + "method": "re-encode" + }, + { + "name": "womenhealth", + "offset": 293376, + "length": 82956, + "s4_format": "ARGB8888", + "s4_flags": 0, + "s5_bytes": 21772, + "s5_from": "health", + "method": "re-encode" + }, + { + "name": "wxpay", + "offset": 48128, + "length": 21772, + "s4_format": "I8", + "s4_flags": 0, + "s5_bytes": 21772, + "s5_from": "app", + "method": "copy" + } + ], + "skipped": [ + { + "name": "walkie_talkie", + "reason": "no S5 counterpart" + } + ] +} \ No newline at end of file diff --git a/s4e-icons/payload/payload.lua b/s4e-icons/payload/payload.lua new file mode 100644 index 0000000..52f03ff --- /dev/null +++ b/s4e-icons/payload/payload.lua @@ -0,0 +1,155 @@ +-- generated by _tools/make_dd_payload.py -- do not hand-edit +-- dd: -if / -of /dev/app bs=4 seek=offset/4 count=length/4 +return { + { name = "canary_superblock", file = "000_canary_superblock.bin", offset = 0, length = 32 }, + { name = "activities", file = "001_activities.bin", offset = 265606144, length = 21772 }, + { name = "alarm", file = "002_alarm.bin", offset = 255491584, length = 82956 }, + { name = "barometer", file = "003_barometer.bin", offset = 243424256, length = 82956 }, + { name = "breath", file = "004_breath.bin", offset = 198528000, length = 82956 }, + { name = "calendar", file = "005_calendar.bin", offset = 197440000, length = 82956 }, + { name = "chronograph", file = "006_chronograph.bin", offset = 195329536, length = 82956 }, + { name = "compass", file = "007_compass.bin", offset = 193598464, length = 21772 }, + { name = "debug", file = "008_debug.bin", offset = 193044992, length = 21772 }, + { name = "esimsms", file = "009_esimsms.bin", offset = 192291328, length = 82956 }, + { name = "find_phone", file = "010_find_phone.bin", offset = 191823872, length = 82956 }, + { name = "flashlight", file = "011_flashlight.bin", offset = 191740416, length = 82956 }, + { name = "health_today", file = "012_health_today.bin", offset = 170015232, length = 82956 }, + { name = "heartrate", file = "013_heartrate.bin", offset = 167684096, length = 82956 }, + { name = "innovation_research", file = "014_innovation_research.bin", offset = 158267904, length = 82956 }, + { name = "interconnect", file = "015_interconnect.bin", offset = 158171136, length = 82956 }, + { name = "lua", file = "016_lua.bin", offset = 158028288, length = 21772 }, + { name = "media", file = "017_media.bin", offset = 157868544, length = 82956 }, + { name = "mijia", file = "018_mijia.bin", offset = 156865024, length = 82956 }, + { name = "nfccard", file = "019_nfccard.bin", offset = 155134976, length = 82956 }, + { name = "oxygen", file = "020_oxygen.bin", offset = 149982208, length = 82956 }, + { name = "perpetual_calendar", file = "021_perpetual_calendar.bin", offset = 129220096, length = 21772 }, + { name = "phone", file = "022_phone.bin", offset = 128241664, length = 82956 }, + { name = "phone_contacts", file = "023_phone_contacts.bin", offset = 128459264, length = 82956 }, + { name = "pressure", file = "024_pressure.bin", offset = 126911488, length = 82956 }, + { name = "recorder", file = "025_recorder.bin", offset = 108676608, length = 82956 }, + { name = "remote_camera", file = "026_remote_camera.bin", offset = 106510848, length = 21772 }, + { name = "settings", file = "027_settings.bin", offset = 86933504, length = 82956 }, + { name = "sleep", file = "028_sleep.bin", offset = 85287936, length = 82956 }, + { name = "sports", file = "029_sports.bin", offset = 79359488, length = 82956 }, + { name = "sports_course", file = "030_sports_course.bin", offset = 85140480, length = 82956 }, + { name = "sports_record", file = "031_sports_record.bin", offset = 78000128, length = 82956 }, + { name = "sports_training", file = "032_sports_training.bin", offset = 73838592, length = 82956 }, + { name = "timer", file = "033_timer.bin", offset = 61684736, length = 82956 }, + { name = "todolist", file = "034_todolist.bin", offset = 61509632, length = 82956 }, + { name = "voice_aivs", file = "035_voice_aivs.bin", offset = 54641664, length = 82956 }, + { name = "weather", file = "036_weather.bin", offset = 25027072, length = 82956 }, + { name = "womenhealth", file = "037_womenhealth.bin", offset = 293376, length = 82956 }, + { name = "cc_add", file = "038_cc_add.bin", offset = 193594368, length = 3340 }, + { name = "cc_air_mode_close", file = "039_cc_air_mode_close.bin", offset = 193576448, length = 17420 }, + { name = "cc_air_mode_conn", file = "040_cc_air_mode_conn.bin", offset = 193572864, length = 3340 }, + { name = "cc_air_mode_open", file = "041_cc_air_mode_open.bin", offset = 193537024, length = 17420 }, + { name = "cc_battery_close", file = "042_cc_battery_close.bin", offset = 193519104, length = 17420 }, + { name = "cc_battery_open", file = "043_cc_battery_open.bin", offset = 193501184, length = 17420 }, + { name = "cc_bt_connect", file = "044_cc_bt_connect.bin", offset = 193497600, length = 3340 }, + { name = "cc_bt_disconnect", file = "045_cc_bt_disconnect.bin", offset = 193494016, length = 3340 }, + { name = "cc_cancel", file = "046_cc_cancel.bin", offset = 193490432, length = 3340 }, + { name = "cc_drain_close", file = "047_cc_drain_close.bin", offset = 193472512, length = 17420 }, + { name = "cc_earphone", file = "048_cc_earphone.bin", offset = 193413632, length = 3340 }, + { name = "cc_find_phone_close", file = "049_cc_find_phone_close.bin", offset = 193395712, length = 17420 }, + { name = "cc_flashlight", file = "050_cc_flashlight.bin", offset = 193359872, length = 17420 }, + { name = "cc_mobile_network_close", file = "051_cc_mobile_network_close.bin", offset = 193338368, length = 17420 }, + { name = "cc_mobile_network_open", file = "052_cc_mobile_network_open.bin", offset = 193302528, length = 17420 }, + { name = "cc_mute_close", file = "053_cc_mute_close.bin", offset = 193284608, length = 17420 }, + { name = "cc_mute_open", file = "054_cc_mute_open.bin", offset = 193266688, length = 17420 }, + { name = "cc_not_disturb_close", file = "055_cc_not_disturb_close.bin", offset = 193205760, length = 17420 }, + { name = "cc_not_disturb_open", file = "056_cc_not_disturb_open.bin", offset = 193187840, length = 17420 }, + { name = "cc_raise_bright_close", file = "057_cc_raise_bright_close.bin", offset = 193152000, length = 17420 }, + { name = "cc_raise_bright_open", file = "058_cc_raise_bright_open.bin", offset = 193134080, length = 17420 }, + { name = "cc_settings", file = "059_cc_settings.bin", offset = 193116160, length = 17420 }, + { name = "sys_lock_icon", file = "060_sys_lock_icon.bin", offset = 62331392, length = 2060 }, + { name = "sys_system_lock", file = "061_sys_system_lock.bin", offset = 194351104, length = 3340 }, + { name = "sys_quiet_mode", file = "062_sys_quiet_mode.bin", offset = 194466304, length = 3340 }, + { name = "sys_no_message", file = "063_sys_no_message.bin", offset = 150767104, length = 7436 }, + { name = "sys_notify", file = "064_sys_notify.bin", offset = 87050752, length = 5132 }, + { name = "sys_esim_sms_reminder", file = "065_sys_esim_sms_reminder.bin", offset = 192435200, length = 7436 }, + { name = "sys_enable_donot_disturb", file = "066_sys_enable_donot_disturb.bin", offset = 156976640, length = 5132 }, + { name = "sys_disable_donot_disturb", file = "067_sys_disable_donot_disturb.bin", offset = 156989952, length = 5132 }, + { name = "sys_presure_relax", file = "068_sys_presure_relax.bin", offset = 127373312, length = 3340 }, + { name = "sys_presure_mild", file = "069_sys_presure_mild.bin", offset = 127390208, length = 3340 }, + { name = "sys_presure_mid", file = "070_sys_presure_mid.bin", offset = 127407104, length = 3340 }, + { name = "sys_presure_severe", file = "071_sys_presure_severe.bin", offset = 127356416, length = 3340 }, + { name = "sys_presure_mild32", file = "072_sys_presure_mild32.bin", offset = 127387648, length = 2060 }, + { name = "sys_presure_mid32", file = "073_sys_presure_mid32.bin", offset = 127404544, length = 2060 }, + { name = "sys_presure_severe32", file = "074_sys_presure_severe32.bin", offset = 127353856, length = 2060 }, + { name = "sys_presure_icon", file = "075_sys_presure_icon.bin", offset = 127410688, length = 3340 }, + { name = "sys_press_grade1", file = "076_sys_press_grade1.bin", offset = 128046592, length = 2060 }, + { name = "sys_press_grade2", file = "077_sys_press_grade2.bin", offset = 128044032, length = 2060 }, + { name = "sys_press_grade3", file = "078_sys_press_grade3.bin", offset = 128041472, length = 2060 }, + { name = "sys_press_grade4", file = "079_sys_press_grade4.bin", offset = 128038912, length = 2060 }, + { name = "cc_network_4g", file = "080_cc_network_4g.bin", offset = 193241600, length = 3340 }, + { name = "cc_network_3g", file = "081_cc_network_3g.bin", offset = 193263104, length = 3340 }, + { name = "cc_signal_strength", file = "082_cc_signal_strength.bin", offset = 193112576, length = 3340 }, + { name = "cc_signal_strength1", file = "083_cc_signal_strength1.bin", offset = 193108992, length = 3340 }, + { name = "cc_signal_strength2", file = "084_cc_signal_strength2.bin", offset = 193105408, length = 3340 }, + { name = "cc_signal_strength3", file = "085_cc_signal_strength3.bin", offset = 193101824, length = 3340 }, + { name = "cc_signal_strength4", file = "086_cc_signal_strength4.bin", offset = 193098240, length = 3340 }, + { name = "cc_location", file = "087_cc_location.bin", offset = 193356288, length = 3340 }, + { name = "cc_network_3g_signal_1", file = "088_cc_network_3g_signal_1.bin", offset = 193255936, length = 3340 }, + { name = "cc_network_3g_signal_2", file = "089_cc_network_3g_signal_2.bin", offset = 193252352, length = 3340 }, + { name = "cc_network_3g_signal_3", file = "090_cc_network_3g_signal_3.bin", offset = 193248768, length = 3340 }, + { name = "cc_network_3g_signal_4", file = "091_cc_network_3g_signal_4.bin", offset = 193245184, length = 3340 }, + { name = "cc_network_3g_no_signal", file = "092_cc_network_3g_no_signal.bin", offset = 193259520, length = 3340 }, + { name = "cc_network_4g_signal_1", file = "093_cc_network_4g_signal_1.bin", offset = 193234432, length = 3340 }, + { name = "cc_network_4g_signal_2", file = "094_cc_network_4g_signal_2.bin", offset = 193230848, length = 3340 }, + { name = "cc_network_4g_signal_3", file = "095_cc_network_4g_signal_3.bin", offset = 193227264, length = 3340 }, + { name = "cc_network_4g_signal_4", file = "096_cc_network_4g_signal_4.bin", offset = 193223680, length = 3340 }, + { name = "cc_network_4g_no_signal", file = "097_cc_network_4g_no_signal.bin", offset = 193238016, length = 3340 }, + { name = "sys_notifications_car", file = "098_sys_notifications_car.bin", offset = 150829568, length = 5260 }, + { name = "sys_notifications_default", file = "099_sys_notifications_default.bin", offset = 150794752, length = 32012 }, + { name = "sys_notifications_default_small", file = "100_sys_notifications_default_small.bin", offset = 150791168, length = 3340 }, + { name = "sys_notifications_half_going", file = "101_sys_notifications_half_going.bin", offset = 150776320, length = 3052 }, + { name = "sys_notifications_half_point", file = "102_sys_notifications_half_point.bin", offset = 150774784, length = 1292 }, + { name = "sys_notifications_no_message", file = "103_sys_notifications_no_message.bin", offset = 150767104, length = 7436 }, + { name = "sys_phone_icon_app_h_w_48", file = "104_sys_phone_icon_app_h_w_48.bin", offset = 128450048, length = 3340 }, + { name = "sys_phone_icon_dial", file = "105_sys_phone_icon_dial.bin", offset = 128446464, length = 3340 }, + { name = "sys_phone_icon_hash", file = "106_sys_phone_icon_hash.bin", offset = 128442880, length = 3340 }, + { name = "sys_phone_icon_headphone_icon", file = "107_sys_phone_icon_headphone_icon.bin", offset = 128437248, length = 5132 }, + { name = "sys_phone_icon_keypad_icon", file = "108_sys_phone_icon_keypad_icon.bin", offset = 128425984, length = 5132 }, + { name = "sys_phone_icon_mic_off", file = "109_sys_phone_icon_mic_off.bin", offset = 128389632, length = 3340 }, + { name = "sys_phone_icon_mic_on", file = "110_sys_phone_icon_mic_on.bin", offset = 128386048, length = 3340 }, + { name = "sys_phone_icon_no_calllog", file = "111_sys_phone_icon_no_calllog.bin", offset = 128378368, length = 7436 }, + { name = "sys_phone_icon_no_contacts", file = "112_sys_phone_icon_no_contacts.bin", offset = 128370688, length = 7436 }, + { name = "sys_phone_icon_phone_icon", file = "113_sys_phone_icon_phone_icon.bin", offset = 128361472, length = 5132 }, + { name = "sys_phone_icon_phone_icon_small", file = "114_sys_phone_icon_phone_icon_small.bin", offset = 128358912, length = 2060 }, + { name = "sys_phone_icon_plus", file = "115_sys_phone_icon_plus.bin", offset = 128355328, length = 3340 }, + { name = "sys_phone_icon_spk_icon_64", file = "116_sys_phone_icon_spk_icon_64.bin", offset = 128336896, length = 5132 }, + { name = "sys_phone_icon_star", file = "117_sys_phone_icon_star.bin", offset = 128333312, length = 3340 }, + { name = "sys_phone_icon_watch_icon", file = "118_sys_phone_icon_watch_icon.bin", offset = 128327680, length = 5132 }, + { name = "sys_phone_icon_watch_icon_small", file = "119_sys_phone_icon_watch_icon_small.bin", offset = 128325120, length = 2060 }, + { name = "sys_phone_widget_widget_contact", file = "120_sys_phone_widget_widget_contact.bin", offset = 128106496, length = 13580 }, + { name = "sys_phone_widget_widget_dialpad", file = "121_sys_phone_widget_widget_dialpad.bin", offset = 128092672, length = 13580 }, + { name = "sys_phone_widget_widget_phone", file = "122_sys_phone_widget_widget_phone.bin", offset = 128078848, length = 13580 }, + { name = "sys_recorder_ahead3", file = "123_sys_recorder_ahead3.bin", offset = 108767744, length = 7436 }, + { name = "sys_recorder_back3", file = "124_sys_recorder_back3.bin", offset = 108760064, length = 7436 }, + { name = "sys_recorder_main", file = "125_sys_recorder_main.bin", offset = 108673024, length = 3340 }, + { name = "sys_recorder_mark", file = "126_sys_recorder_mark.bin", offset = 108669440, length = 3340 }, + { name = "sys_recorder_mute", file = "127_sys_recorder_mute.bin", offset = 108662272, length = 3340 }, + { name = "sys_recorder_sound", file = "128_sys_recorder_sound.bin", offset = 108655104, length = 3340 }, + { name = "sys_recorder_trash", file = "129_sys_recorder_trash.bin", offset = 108608512, length = 3340 }, + { name = "sys_recorder_warning", file = "130_sys_recorder_warning.bin", offset = 108600832, length = 7436 }, + { name = "sys_timer_icon_close", file = "131_sys_timer_icon_close.bin", offset = 62305792, length = 3340 }, + { name = "sys_timer_icon_pause", file = "132_sys_timer_icon_pause.bin", offset = 62298624, length = 3340 }, + { name = "sys_timer_icon_restart", file = "133_sys_timer_icon_restart.bin", offset = 62295040, length = 3340 }, + { name = "sys_timer_icon_start", file = "134_sys_timer_icon_start.bin", offset = 62025216, length = 3340 }, + { name = "sys_timer_icon_stop", file = "135_sys_timer_icon_stop.bin", offset = 62021632, length = 3340 }, + { name = "sys_timer_icon_widget_light12", file = "136_sys_timer_icon_widget_light12.bin", offset = 61896704, length = 120844 }, + { name = "sys_timer_icon_widget_light12_five", file = "137_sys_timer_icon_widget_light12_five.bin", offset = 61775360, length = 120844 }, + { name = "sys_timer_icon_widget_pause", file = "138_sys_timer_icon_widget_pause.bin", offset = 61771776, length = 3340 }, + { name = "sys_timer_icon_widget_start", file = "139_sys_timer_icon_widget_start.bin", offset = 61768192, length = 3340 }, + { name = "sys_todolist_checked", file = "140_sys_todolist_checked.bin", offset = 61681152, length = 3340 }, + { name = "sys_todolist_create_todo_btn", file = "141_sys_todolist_create_todo_btn.bin", offset = 61664256, length = 16396 }, + { name = "sys_todolist_dark12_bg", file = "142_sys_todolist_dark12_bg.bin", offset = 61600768, length = 62988 }, + { name = "sys_todolist_empty", file = "143_sys_todolist_empty.bin", offset = 61593088, length = 7436 }, + { name = "sys_todolist_reminder_big", file = "144_sys_todolist_reminder_big.bin", offset = 61491712, length = 17420 }, + { name = "sys_todolist_reminder_small", file = "145_sys_todolist_reminder_small.bin", offset = 61484032, length = 7436 }, + { name = "sys_todolist_tdl_dark11_bg", file = "146_sys_todolist_tdl_dark11_bg.bin", offset = 61451776, length = 32012 }, + { name = "sys_todolist_tdl_light11_bg", file = "147_sys_todolist_tdl_light11_bg.bin", offset = 61419520, length = 32012 }, + { name = "sys_todolist_unchecked", file = "148_sys_todolist_unchecked.bin", offset = 61415936, length = 3340 }, + { name = "sys_todolist_widget_dark_icon", file = "149_sys_todolist_widget_dark_icon.bin", offset = 61408256, length = 7436 }, + { name = "sys_todolist_widget_light_icon", file = "150_sys_todolist_widget_light_icon.bin", offset = 61400576, length = 7436 }, +} diff --git a/s4e-icons/payload_rollback/payload.lua b/s4e-icons/payload_rollback/payload.lua new file mode 100644 index 0000000..52f03ff --- /dev/null +++ b/s4e-icons/payload_rollback/payload.lua @@ -0,0 +1,155 @@ +-- generated by _tools/make_dd_payload.py -- do not hand-edit +-- dd: -if / -of /dev/app bs=4 seek=offset/4 count=length/4 +return { + { name = "canary_superblock", file = "000_canary_superblock.bin", offset = 0, length = 32 }, + { name = "activities", file = "001_activities.bin", offset = 265606144, length = 21772 }, + { name = "alarm", file = "002_alarm.bin", offset = 255491584, length = 82956 }, + { name = "barometer", file = "003_barometer.bin", offset = 243424256, length = 82956 }, + { name = "breath", file = "004_breath.bin", offset = 198528000, length = 82956 }, + { name = "calendar", file = "005_calendar.bin", offset = 197440000, length = 82956 }, + { name = "chronograph", file = "006_chronograph.bin", offset = 195329536, length = 82956 }, + { name = "compass", file = "007_compass.bin", offset = 193598464, length = 21772 }, + { name = "debug", file = "008_debug.bin", offset = 193044992, length = 21772 }, + { name = "esimsms", file = "009_esimsms.bin", offset = 192291328, length = 82956 }, + { name = "find_phone", file = "010_find_phone.bin", offset = 191823872, length = 82956 }, + { name = "flashlight", file = "011_flashlight.bin", offset = 191740416, length = 82956 }, + { name = "health_today", file = "012_health_today.bin", offset = 170015232, length = 82956 }, + { name = "heartrate", file = "013_heartrate.bin", offset = 167684096, length = 82956 }, + { name = "innovation_research", file = "014_innovation_research.bin", offset = 158267904, length = 82956 }, + { name = "interconnect", file = "015_interconnect.bin", offset = 158171136, length = 82956 }, + { name = "lua", file = "016_lua.bin", offset = 158028288, length = 21772 }, + { name = "media", file = "017_media.bin", offset = 157868544, length = 82956 }, + { name = "mijia", file = "018_mijia.bin", offset = 156865024, length = 82956 }, + { name = "nfccard", file = "019_nfccard.bin", offset = 155134976, length = 82956 }, + { name = "oxygen", file = "020_oxygen.bin", offset = 149982208, length = 82956 }, + { name = "perpetual_calendar", file = "021_perpetual_calendar.bin", offset = 129220096, length = 21772 }, + { name = "phone", file = "022_phone.bin", offset = 128241664, length = 82956 }, + { name = "phone_contacts", file = "023_phone_contacts.bin", offset = 128459264, length = 82956 }, + { name = "pressure", file = "024_pressure.bin", offset = 126911488, length = 82956 }, + { name = "recorder", file = "025_recorder.bin", offset = 108676608, length = 82956 }, + { name = "remote_camera", file = "026_remote_camera.bin", offset = 106510848, length = 21772 }, + { name = "settings", file = "027_settings.bin", offset = 86933504, length = 82956 }, + { name = "sleep", file = "028_sleep.bin", offset = 85287936, length = 82956 }, + { name = "sports", file = "029_sports.bin", offset = 79359488, length = 82956 }, + { name = "sports_course", file = "030_sports_course.bin", offset = 85140480, length = 82956 }, + { name = "sports_record", file = "031_sports_record.bin", offset = 78000128, length = 82956 }, + { name = "sports_training", file = "032_sports_training.bin", offset = 73838592, length = 82956 }, + { name = "timer", file = "033_timer.bin", offset = 61684736, length = 82956 }, + { name = "todolist", file = "034_todolist.bin", offset = 61509632, length = 82956 }, + { name = "voice_aivs", file = "035_voice_aivs.bin", offset = 54641664, length = 82956 }, + { name = "weather", file = "036_weather.bin", offset = 25027072, length = 82956 }, + { name = "womenhealth", file = "037_womenhealth.bin", offset = 293376, length = 82956 }, + { name = "cc_add", file = "038_cc_add.bin", offset = 193594368, length = 3340 }, + { name = "cc_air_mode_close", file = "039_cc_air_mode_close.bin", offset = 193576448, length = 17420 }, + { name = "cc_air_mode_conn", file = "040_cc_air_mode_conn.bin", offset = 193572864, length = 3340 }, + { name = "cc_air_mode_open", file = "041_cc_air_mode_open.bin", offset = 193537024, length = 17420 }, + { name = "cc_battery_close", file = "042_cc_battery_close.bin", offset = 193519104, length = 17420 }, + { name = "cc_battery_open", file = "043_cc_battery_open.bin", offset = 193501184, length = 17420 }, + { name = "cc_bt_connect", file = "044_cc_bt_connect.bin", offset = 193497600, length = 3340 }, + { name = "cc_bt_disconnect", file = "045_cc_bt_disconnect.bin", offset = 193494016, length = 3340 }, + { name = "cc_cancel", file = "046_cc_cancel.bin", offset = 193490432, length = 3340 }, + { name = "cc_drain_close", file = "047_cc_drain_close.bin", offset = 193472512, length = 17420 }, + { name = "cc_earphone", file = "048_cc_earphone.bin", offset = 193413632, length = 3340 }, + { name = "cc_find_phone_close", file = "049_cc_find_phone_close.bin", offset = 193395712, length = 17420 }, + { name = "cc_flashlight", file = "050_cc_flashlight.bin", offset = 193359872, length = 17420 }, + { name = "cc_mobile_network_close", file = "051_cc_mobile_network_close.bin", offset = 193338368, length = 17420 }, + { name = "cc_mobile_network_open", file = "052_cc_mobile_network_open.bin", offset = 193302528, length = 17420 }, + { name = "cc_mute_close", file = "053_cc_mute_close.bin", offset = 193284608, length = 17420 }, + { name = "cc_mute_open", file = "054_cc_mute_open.bin", offset = 193266688, length = 17420 }, + { name = "cc_not_disturb_close", file = "055_cc_not_disturb_close.bin", offset = 193205760, length = 17420 }, + { name = "cc_not_disturb_open", file = "056_cc_not_disturb_open.bin", offset = 193187840, length = 17420 }, + { name = "cc_raise_bright_close", file = "057_cc_raise_bright_close.bin", offset = 193152000, length = 17420 }, + { name = "cc_raise_bright_open", file = "058_cc_raise_bright_open.bin", offset = 193134080, length = 17420 }, + { name = "cc_settings", file = "059_cc_settings.bin", offset = 193116160, length = 17420 }, + { name = "sys_lock_icon", file = "060_sys_lock_icon.bin", offset = 62331392, length = 2060 }, + { name = "sys_system_lock", file = "061_sys_system_lock.bin", offset = 194351104, length = 3340 }, + { name = "sys_quiet_mode", file = "062_sys_quiet_mode.bin", offset = 194466304, length = 3340 }, + { name = "sys_no_message", file = "063_sys_no_message.bin", offset = 150767104, length = 7436 }, + { name = "sys_notify", file = "064_sys_notify.bin", offset = 87050752, length = 5132 }, + { name = "sys_esim_sms_reminder", file = "065_sys_esim_sms_reminder.bin", offset = 192435200, length = 7436 }, + { name = "sys_enable_donot_disturb", file = "066_sys_enable_donot_disturb.bin", offset = 156976640, length = 5132 }, + { name = "sys_disable_donot_disturb", file = "067_sys_disable_donot_disturb.bin", offset = 156989952, length = 5132 }, + { name = "sys_presure_relax", file = "068_sys_presure_relax.bin", offset = 127373312, length = 3340 }, + { name = "sys_presure_mild", file = "069_sys_presure_mild.bin", offset = 127390208, length = 3340 }, + { name = "sys_presure_mid", file = "070_sys_presure_mid.bin", offset = 127407104, length = 3340 }, + { name = "sys_presure_severe", file = "071_sys_presure_severe.bin", offset = 127356416, length = 3340 }, + { name = "sys_presure_mild32", file = "072_sys_presure_mild32.bin", offset = 127387648, length = 2060 }, + { name = "sys_presure_mid32", file = "073_sys_presure_mid32.bin", offset = 127404544, length = 2060 }, + { name = "sys_presure_severe32", file = "074_sys_presure_severe32.bin", offset = 127353856, length = 2060 }, + { name = "sys_presure_icon", file = "075_sys_presure_icon.bin", offset = 127410688, length = 3340 }, + { name = "sys_press_grade1", file = "076_sys_press_grade1.bin", offset = 128046592, length = 2060 }, + { name = "sys_press_grade2", file = "077_sys_press_grade2.bin", offset = 128044032, length = 2060 }, + { name = "sys_press_grade3", file = "078_sys_press_grade3.bin", offset = 128041472, length = 2060 }, + { name = "sys_press_grade4", file = "079_sys_press_grade4.bin", offset = 128038912, length = 2060 }, + { name = "cc_network_4g", file = "080_cc_network_4g.bin", offset = 193241600, length = 3340 }, + { name = "cc_network_3g", file = "081_cc_network_3g.bin", offset = 193263104, length = 3340 }, + { name = "cc_signal_strength", file = "082_cc_signal_strength.bin", offset = 193112576, length = 3340 }, + { name = "cc_signal_strength1", file = "083_cc_signal_strength1.bin", offset = 193108992, length = 3340 }, + { name = "cc_signal_strength2", file = "084_cc_signal_strength2.bin", offset = 193105408, length = 3340 }, + { name = "cc_signal_strength3", file = "085_cc_signal_strength3.bin", offset = 193101824, length = 3340 }, + { name = "cc_signal_strength4", file = "086_cc_signal_strength4.bin", offset = 193098240, length = 3340 }, + { name = "cc_location", file = "087_cc_location.bin", offset = 193356288, length = 3340 }, + { name = "cc_network_3g_signal_1", file = "088_cc_network_3g_signal_1.bin", offset = 193255936, length = 3340 }, + { name = "cc_network_3g_signal_2", file = "089_cc_network_3g_signal_2.bin", offset = 193252352, length = 3340 }, + { name = "cc_network_3g_signal_3", file = "090_cc_network_3g_signal_3.bin", offset = 193248768, length = 3340 }, + { name = "cc_network_3g_signal_4", file = "091_cc_network_3g_signal_4.bin", offset = 193245184, length = 3340 }, + { name = "cc_network_3g_no_signal", file = "092_cc_network_3g_no_signal.bin", offset = 193259520, length = 3340 }, + { name = "cc_network_4g_signal_1", file = "093_cc_network_4g_signal_1.bin", offset = 193234432, length = 3340 }, + { name = "cc_network_4g_signal_2", file = "094_cc_network_4g_signal_2.bin", offset = 193230848, length = 3340 }, + { name = "cc_network_4g_signal_3", file = "095_cc_network_4g_signal_3.bin", offset = 193227264, length = 3340 }, + { name = "cc_network_4g_signal_4", file = "096_cc_network_4g_signal_4.bin", offset = 193223680, length = 3340 }, + { name = "cc_network_4g_no_signal", file = "097_cc_network_4g_no_signal.bin", offset = 193238016, length = 3340 }, + { name = "sys_notifications_car", file = "098_sys_notifications_car.bin", offset = 150829568, length = 5260 }, + { name = "sys_notifications_default", file = "099_sys_notifications_default.bin", offset = 150794752, length = 32012 }, + { name = "sys_notifications_default_small", file = "100_sys_notifications_default_small.bin", offset = 150791168, length = 3340 }, + { name = "sys_notifications_half_going", file = "101_sys_notifications_half_going.bin", offset = 150776320, length = 3052 }, + { name = "sys_notifications_half_point", file = "102_sys_notifications_half_point.bin", offset = 150774784, length = 1292 }, + { name = "sys_notifications_no_message", file = "103_sys_notifications_no_message.bin", offset = 150767104, length = 7436 }, + { name = "sys_phone_icon_app_h_w_48", file = "104_sys_phone_icon_app_h_w_48.bin", offset = 128450048, length = 3340 }, + { name = "sys_phone_icon_dial", file = "105_sys_phone_icon_dial.bin", offset = 128446464, length = 3340 }, + { name = "sys_phone_icon_hash", file = "106_sys_phone_icon_hash.bin", offset = 128442880, length = 3340 }, + { name = "sys_phone_icon_headphone_icon", file = "107_sys_phone_icon_headphone_icon.bin", offset = 128437248, length = 5132 }, + { name = "sys_phone_icon_keypad_icon", file = "108_sys_phone_icon_keypad_icon.bin", offset = 128425984, length = 5132 }, + { name = "sys_phone_icon_mic_off", file = "109_sys_phone_icon_mic_off.bin", offset = 128389632, length = 3340 }, + { name = "sys_phone_icon_mic_on", file = "110_sys_phone_icon_mic_on.bin", offset = 128386048, length = 3340 }, + { name = "sys_phone_icon_no_calllog", file = "111_sys_phone_icon_no_calllog.bin", offset = 128378368, length = 7436 }, + { name = "sys_phone_icon_no_contacts", file = "112_sys_phone_icon_no_contacts.bin", offset = 128370688, length = 7436 }, + { name = "sys_phone_icon_phone_icon", file = "113_sys_phone_icon_phone_icon.bin", offset = 128361472, length = 5132 }, + { name = "sys_phone_icon_phone_icon_small", file = "114_sys_phone_icon_phone_icon_small.bin", offset = 128358912, length = 2060 }, + { name = "sys_phone_icon_plus", file = "115_sys_phone_icon_plus.bin", offset = 128355328, length = 3340 }, + { name = "sys_phone_icon_spk_icon_64", file = "116_sys_phone_icon_spk_icon_64.bin", offset = 128336896, length = 5132 }, + { name = "sys_phone_icon_star", file = "117_sys_phone_icon_star.bin", offset = 128333312, length = 3340 }, + { name = "sys_phone_icon_watch_icon", file = "118_sys_phone_icon_watch_icon.bin", offset = 128327680, length = 5132 }, + { name = "sys_phone_icon_watch_icon_small", file = "119_sys_phone_icon_watch_icon_small.bin", offset = 128325120, length = 2060 }, + { name = "sys_phone_widget_widget_contact", file = "120_sys_phone_widget_widget_contact.bin", offset = 128106496, length = 13580 }, + { name = "sys_phone_widget_widget_dialpad", file = "121_sys_phone_widget_widget_dialpad.bin", offset = 128092672, length = 13580 }, + { name = "sys_phone_widget_widget_phone", file = "122_sys_phone_widget_widget_phone.bin", offset = 128078848, length = 13580 }, + { name = "sys_recorder_ahead3", file = "123_sys_recorder_ahead3.bin", offset = 108767744, length = 7436 }, + { name = "sys_recorder_back3", file = "124_sys_recorder_back3.bin", offset = 108760064, length = 7436 }, + { name = "sys_recorder_main", file = "125_sys_recorder_main.bin", offset = 108673024, length = 3340 }, + { name = "sys_recorder_mark", file = "126_sys_recorder_mark.bin", offset = 108669440, length = 3340 }, + { name = "sys_recorder_mute", file = "127_sys_recorder_mute.bin", offset = 108662272, length = 3340 }, + { name = "sys_recorder_sound", file = "128_sys_recorder_sound.bin", offset = 108655104, length = 3340 }, + { name = "sys_recorder_trash", file = "129_sys_recorder_trash.bin", offset = 108608512, length = 3340 }, + { name = "sys_recorder_warning", file = "130_sys_recorder_warning.bin", offset = 108600832, length = 7436 }, + { name = "sys_timer_icon_close", file = "131_sys_timer_icon_close.bin", offset = 62305792, length = 3340 }, + { name = "sys_timer_icon_pause", file = "132_sys_timer_icon_pause.bin", offset = 62298624, length = 3340 }, + { name = "sys_timer_icon_restart", file = "133_sys_timer_icon_restart.bin", offset = 62295040, length = 3340 }, + { name = "sys_timer_icon_start", file = "134_sys_timer_icon_start.bin", offset = 62025216, length = 3340 }, + { name = "sys_timer_icon_stop", file = "135_sys_timer_icon_stop.bin", offset = 62021632, length = 3340 }, + { name = "sys_timer_icon_widget_light12", file = "136_sys_timer_icon_widget_light12.bin", offset = 61896704, length = 120844 }, + { name = "sys_timer_icon_widget_light12_five", file = "137_sys_timer_icon_widget_light12_five.bin", offset = 61775360, length = 120844 }, + { name = "sys_timer_icon_widget_pause", file = "138_sys_timer_icon_widget_pause.bin", offset = 61771776, length = 3340 }, + { name = "sys_timer_icon_widget_start", file = "139_sys_timer_icon_widget_start.bin", offset = 61768192, length = 3340 }, + { name = "sys_todolist_checked", file = "140_sys_todolist_checked.bin", offset = 61681152, length = 3340 }, + { name = "sys_todolist_create_todo_btn", file = "141_sys_todolist_create_todo_btn.bin", offset = 61664256, length = 16396 }, + { name = "sys_todolist_dark12_bg", file = "142_sys_todolist_dark12_bg.bin", offset = 61600768, length = 62988 }, + { name = "sys_todolist_empty", file = "143_sys_todolist_empty.bin", offset = 61593088, length = 7436 }, + { name = "sys_todolist_reminder_big", file = "144_sys_todolist_reminder_big.bin", offset = 61491712, length = 17420 }, + { name = "sys_todolist_reminder_small", file = "145_sys_todolist_reminder_small.bin", offset = 61484032, length = 7436 }, + { name = "sys_todolist_tdl_dark11_bg", file = "146_sys_todolist_tdl_dark11_bg.bin", offset = 61451776, length = 32012 }, + { name = "sys_todolist_tdl_light11_bg", file = "147_sys_todolist_tdl_light11_bg.bin", offset = 61419520, length = 32012 }, + { name = "sys_todolist_unchecked", file = "148_sys_todolist_unchecked.bin", offset = 61415936, length = 3340 }, + { name = "sys_todolist_widget_dark_icon", file = "149_sys_todolist_widget_dark_icon.bin", offset = 61408256, length = 7436 }, + { name = "sys_todolist_widget_light_icon", file = "150_sys_todolist_widget_light_icon.bin", offset = 61400576, length = 7436 }, +} diff --git a/s4e-icons/rollback.sh b/s4e-icons/rollback.sh new file mode 100644 index 0000000..1d126a9 --- /dev/null +++ b/s4e-icons/rollback.sh @@ -0,0 +1,312 @@ +#!/bin/sh +# 写回 S4 原厂图标 + +DEV=/dev/app +BASE=/data/s5icons/payload_rollback + +echo "target check" +ls -l $DEV + +echo "== canary_superblock == +dd if=$BASE/000_canary_superblock.bin of=$DEV bs=4 seek=0 count=8 +echo "== activities == +dd if=$BASE/001_activities.bin of=$DEV bs=4 seek=66401536 count=5443 +echo "== alarm == +dd if=$BASE/002_alarm.bin of=$DEV bs=4 seek=63872896 count=20739 +echo "== barometer == +dd if=$BASE/003_barometer.bin of=$DEV bs=4 seek=60856064 count=20739 +echo "== breath == +dd if=$BASE/004_breath.bin of=$DEV bs=4 seek=49632000 count=20739 +echo "== calendar == +dd if=$BASE/005_calendar.bin of=$DEV bs=4 seek=49360000 count=20739 +echo "== chronograph == +dd if=$BASE/006_chronograph.bin of=$DEV bs=4 seek=48832384 count=20739 +echo "== compass == +dd if=$BASE/007_compass.bin of=$DEV bs=4 seek=48399616 count=5443 +echo "== debug == +dd if=$BASE/008_debug.bin of=$DEV bs=4 seek=48261248 count=5443 +echo "== esimsms == +dd if=$BASE/009_esimsms.bin of=$DEV bs=4 seek=48072832 count=20739 +echo "== find_phone == +dd if=$BASE/010_find_phone.bin of=$DEV bs=4 seek=47955968 count=20739 +echo "== flashlight == +dd if=$BASE/011_flashlight.bin of=$DEV bs=4 seek=47935104 count=20739 +echo "== health_today == +dd if=$BASE/012_health_today.bin of=$DEV bs=4 seek=42503808 count=20739 +echo "== heartrate == +dd if=$BASE/013_heartrate.bin of=$DEV bs=4 seek=41921024 count=20739 +echo "== innovation_research == +dd if=$BASE/014_innovation_research.bin of=$DEV bs=4 seek=39566976 count=20739 +echo "== interconnect == +dd if=$BASE/015_interconnect.bin of=$DEV bs=4 seek=39542784 count=20739 +echo "== lua == +dd if=$BASE/016_lua.bin of=$DEV bs=4 seek=39507072 count=5443 +echo "== media == +dd if=$BASE/017_media.bin of=$DEV bs=4 seek=39467136 count=20739 +echo "== mijia == +dd if=$BASE/018_mijia.bin of=$DEV bs=4 seek=39216256 count=20739 +echo "== nfccard == +dd if=$BASE/019_nfccard.bin of=$DEV bs=4 seek=38783744 count=20739 +echo "== oxygen == +dd if=$BASE/020_oxygen.bin of=$DEV bs=4 seek=37495552 count=20739 +echo "== perpetual_calendar == +dd if=$BASE/021_perpetual_calendar.bin of=$DEV bs=4 seek=32305024 count=5443 +echo "== phone == +dd if=$BASE/022_phone.bin of=$DEV bs=4 seek=32060416 count=20739 +echo "== phone_contacts == +dd if=$BASE/023_phone_contacts.bin of=$DEV bs=4 seek=32114816 count=20739 +echo "== pressure == +dd if=$BASE/024_pressure.bin of=$DEV bs=4 seek=31727872 count=20739 +echo "== recorder == +dd if=$BASE/025_recorder.bin of=$DEV bs=4 seek=27169152 count=20739 +echo "== remote_camera == +dd if=$BASE/026_remote_camera.bin of=$DEV bs=4 seek=26627712 count=5443 +echo "== settings == +dd if=$BASE/027_settings.bin of=$DEV bs=4 seek=21733376 count=20739 +echo "== sleep == +dd if=$BASE/028_sleep.bin of=$DEV bs=4 seek=21321984 count=20739 +echo "== sports == +dd if=$BASE/029_sports.bin of=$DEV bs=4 seek=19839872 count=20739 +echo "== sports_course == +dd if=$BASE/030_sports_course.bin of=$DEV bs=4 seek=21285120 count=20739 +echo "== sports_record == +dd if=$BASE/031_sports_record.bin of=$DEV bs=4 seek=19500032 count=20739 +echo "== sports_training == +dd if=$BASE/032_sports_training.bin of=$DEV bs=4 seek=18459648 count=20739 +echo "== timer == +dd if=$BASE/033_timer.bin of=$DEV bs=4 seek=15421184 count=20739 +echo "== todolist == +dd if=$BASE/034_todolist.bin of=$DEV bs=4 seek=15377408 count=20739 +echo "== voice_aivs == +dd if=$BASE/035_voice_aivs.bin of=$DEV bs=4 seek=13660416 count=20739 +echo "== weather == +dd if=$BASE/036_weather.bin of=$DEV bs=4 seek=6256768 count=20739 +echo "== womenhealth == +dd if=$BASE/037_womenhealth.bin of=$DEV bs=4 seek=73344 count=20739 +echo "== cc_add == +dd if=$BASE/038_cc_add.bin of=$DEV bs=4 seek=48398592 count=835 +echo "== cc_air_mode_close == +dd if=$BASE/039_cc_air_mode_close.bin of=$DEV bs=4 seek=48394112 count=4355 +echo "== cc_air_mode_conn == +dd if=$BASE/040_cc_air_mode_conn.bin of=$DEV bs=4 seek=48393216 count=835 +echo "== cc_air_mode_open == +dd if=$BASE/041_cc_air_mode_open.bin of=$DEV bs=4 seek=48384256 count=4355 +echo "== cc_battery_close == +dd if=$BASE/042_cc_battery_close.bin of=$DEV bs=4 seek=48379776 count=4355 +echo "== cc_battery_open == +dd if=$BASE/043_cc_battery_open.bin of=$DEV bs=4 seek=48375296 count=4355 +echo "== cc_bt_connect == +dd if=$BASE/044_cc_bt_connect.bin of=$DEV bs=4 seek=48374400 count=835 +echo "== cc_bt_disconnect == +dd if=$BASE/045_cc_bt_disconnect.bin of=$DEV bs=4 seek=48373504 count=835 +echo "== cc_cancel == +dd if=$BASE/046_cc_cancel.bin of=$DEV bs=4 seek=48372608 count=835 +echo "== cc_drain_close == +dd if=$BASE/047_cc_drain_close.bin of=$DEV bs=4 seek=48368128 count=4355 +echo "== cc_earphone == +dd if=$BASE/048_cc_earphone.bin of=$DEV bs=4 seek=48353408 count=835 +echo "== cc_find_phone_close == +dd if=$BASE/049_cc_find_phone_close.bin of=$DEV bs=4 seek=48348928 count=4355 +echo "== cc_flashlight == +dd if=$BASE/050_cc_flashlight.bin of=$DEV bs=4 seek=48339968 count=4355 +echo "== cc_mobile_network_close == +dd if=$BASE/051_cc_mobile_network_close.bin of=$DEV bs=4 seek=48334592 count=4355 +echo "== cc_mobile_network_open == +dd if=$BASE/052_cc_mobile_network_open.bin of=$DEV bs=4 seek=48325632 count=4355 +echo "== cc_mute_close == +dd if=$BASE/053_cc_mute_close.bin of=$DEV bs=4 seek=48321152 count=4355 +echo "== cc_mute_open == +dd if=$BASE/054_cc_mute_open.bin of=$DEV bs=4 seek=48316672 count=4355 +echo "== cc_not_disturb_close == +dd if=$BASE/055_cc_not_disturb_close.bin of=$DEV bs=4 seek=48301440 count=4355 +echo "== cc_not_disturb_open == +dd if=$BASE/056_cc_not_disturb_open.bin of=$DEV bs=4 seek=48296960 count=4355 +echo "== cc_raise_bright_close == +dd if=$BASE/057_cc_raise_bright_close.bin of=$DEV bs=4 seek=48288000 count=4355 +echo "== cc_raise_bright_open == +dd if=$BASE/058_cc_raise_bright_open.bin of=$DEV bs=4 seek=48283520 count=4355 +echo "== cc_settings == +dd if=$BASE/059_cc_settings.bin of=$DEV bs=4 seek=48279040 count=4355 +echo "== sys_lock_icon == +dd if=$BASE/060_sys_lock_icon.bin of=$DEV bs=4 seek=15582848 count=515 +echo "== sys_system_lock == +dd if=$BASE/061_sys_system_lock.bin of=$DEV bs=4 seek=48587776 count=835 +echo "== sys_quiet_mode == +dd if=$BASE/062_sys_quiet_mode.bin of=$DEV bs=4 seek=48616576 count=835 +echo "== sys_no_message == +dd if=$BASE/063_sys_no_message.bin of=$DEV bs=4 seek=37691776 count=1859 +echo "== sys_notify == +dd if=$BASE/064_sys_notify.bin of=$DEV bs=4 seek=21762688 count=1283 +echo "== sys_esim_sms_reminder == +dd if=$BASE/065_sys_esim_sms_reminder.bin of=$DEV bs=4 seek=48108800 count=1859 +echo "== sys_enable_donot_disturb == +dd if=$BASE/066_sys_enable_donot_disturb.bin of=$DEV bs=4 seek=39244160 count=1283 +echo "== sys_disable_donot_disturb == +dd if=$BASE/067_sys_disable_donot_disturb.bin of=$DEV bs=4 seek=39247488 count=1283 +echo "== sys_presure_relax == +dd if=$BASE/068_sys_presure_relax.bin of=$DEV bs=4 seek=31843328 count=835 +echo "== sys_presure_mild == +dd if=$BASE/069_sys_presure_mild.bin of=$DEV bs=4 seek=31847552 count=835 +echo "== sys_presure_mid == +dd if=$BASE/070_sys_presure_mid.bin of=$DEV bs=4 seek=31851776 count=835 +echo "== sys_presure_severe == +dd if=$BASE/071_sys_presure_severe.bin of=$DEV bs=4 seek=31839104 count=835 +echo "== sys_presure_mild32 == +dd if=$BASE/072_sys_presure_mild32.bin of=$DEV bs=4 seek=31846912 count=515 +echo "== sys_presure_mid32 == +dd if=$BASE/073_sys_presure_mid32.bin of=$DEV bs=4 seek=31851136 count=515 +echo "== sys_presure_severe32 == +dd if=$BASE/074_sys_presure_severe32.bin of=$DEV bs=4 seek=31838464 count=515 +echo "== sys_presure_icon == +dd if=$BASE/075_sys_presure_icon.bin of=$DEV bs=4 seek=31852672 count=835 +echo "== sys_press_grade1 == +dd if=$BASE/076_sys_press_grade1.bin of=$DEV bs=4 seek=32011648 count=515 +echo "== sys_press_grade2 == +dd if=$BASE/077_sys_press_grade2.bin of=$DEV bs=4 seek=32011008 count=515 +echo "== sys_press_grade3 == +dd if=$BASE/078_sys_press_grade3.bin of=$DEV bs=4 seek=32010368 count=515 +echo "== sys_press_grade4 == +dd if=$BASE/079_sys_press_grade4.bin of=$DEV bs=4 seek=32009728 count=515 +echo "== cc_network_4g == +dd if=$BASE/080_cc_network_4g.bin of=$DEV bs=4 seek=48310400 count=835 +echo "== cc_network_3g == +dd if=$BASE/081_cc_network_3g.bin of=$DEV bs=4 seek=48315776 count=835 +echo "== cc_signal_strength == +dd if=$BASE/082_cc_signal_strength.bin of=$DEV bs=4 seek=48278144 count=835 +echo "== cc_signal_strength1 == +dd if=$BASE/083_cc_signal_strength1.bin of=$DEV bs=4 seek=48277248 count=835 +echo "== cc_signal_strength2 == +dd if=$BASE/084_cc_signal_strength2.bin of=$DEV bs=4 seek=48276352 count=835 +echo "== cc_signal_strength3 == +dd if=$BASE/085_cc_signal_strength3.bin of=$DEV bs=4 seek=48275456 count=835 +echo "== cc_signal_strength4 == +dd if=$BASE/086_cc_signal_strength4.bin of=$DEV bs=4 seek=48274560 count=835 +echo "== cc_location == +dd if=$BASE/087_cc_location.bin of=$DEV bs=4 seek=48339072 count=835 +echo "== cc_network_3g_signal_1 == +dd if=$BASE/088_cc_network_3g_signal_1.bin of=$DEV bs=4 seek=48313984 count=835 +echo "== cc_network_3g_signal_2 == +dd if=$BASE/089_cc_network_3g_signal_2.bin of=$DEV bs=4 seek=48313088 count=835 +echo "== cc_network_3g_signal_3 == +dd if=$BASE/090_cc_network_3g_signal_3.bin of=$DEV bs=4 seek=48312192 count=835 +echo "== cc_network_3g_signal_4 == +dd if=$BASE/091_cc_network_3g_signal_4.bin of=$DEV bs=4 seek=48311296 count=835 +echo "== cc_network_3g_no_signal == +dd if=$BASE/092_cc_network_3g_no_signal.bin of=$DEV bs=4 seek=48314880 count=835 +echo "== cc_network_4g_signal_1 == +dd if=$BASE/093_cc_network_4g_signal_1.bin of=$DEV bs=4 seek=48308608 count=835 +echo "== cc_network_4g_signal_2 == +dd if=$BASE/094_cc_network_4g_signal_2.bin of=$DEV bs=4 seek=48307712 count=835 +echo "== cc_network_4g_signal_3 == +dd if=$BASE/095_cc_network_4g_signal_3.bin of=$DEV bs=4 seek=48306816 count=835 +echo "== cc_network_4g_signal_4 == +dd if=$BASE/096_cc_network_4g_signal_4.bin of=$DEV bs=4 seek=48305920 count=835 +echo "== cc_network_4g_no_signal == +dd if=$BASE/097_cc_network_4g_no_signal.bin of=$DEV bs=4 seek=48309504 count=835 +echo "== sys_notifications_car == +dd if=$BASE/098_sys_notifications_car.bin of=$DEV bs=4 seek=37707392 count=1315 +echo "== sys_notifications_default == +dd if=$BASE/099_sys_notifications_default.bin of=$DEV bs=4 seek=37698688 count=8003 +echo "== sys_notifications_default_small == +dd if=$BASE/100_sys_notifications_default_small.bin of=$DEV bs=4 seek=37697792 count=835 +echo "== sys_notifications_half_going == +dd if=$BASE/101_sys_notifications_half_going.bin of=$DEV bs=4 seek=37694080 count=763 +echo "== sys_notifications_half_point == +dd if=$BASE/102_sys_notifications_half_point.bin of=$DEV bs=4 seek=37693696 count=323 +echo "== sys_notifications_no_message == +dd if=$BASE/103_sys_notifications_no_message.bin of=$DEV bs=4 seek=37691776 count=1859 +echo "== sys_phone_icon_app_h_w_48 == +dd if=$BASE/104_sys_phone_icon_app_h_w_48.bin of=$DEV bs=4 seek=32112512 count=835 +echo "== sys_phone_icon_dial == +dd if=$BASE/105_sys_phone_icon_dial.bin of=$DEV bs=4 seek=32111616 count=835 +echo "== sys_phone_icon_hash == +dd if=$BASE/106_sys_phone_icon_hash.bin of=$DEV bs=4 seek=32110720 count=835 +echo "== sys_phone_icon_headphone_icon == +dd if=$BASE/107_sys_phone_icon_headphone_icon.bin of=$DEV bs=4 seek=32109312 count=1283 +echo "== sys_phone_icon_keypad_icon == +dd if=$BASE/108_sys_phone_icon_keypad_icon.bin of=$DEV bs=4 seek=32106496 count=1283 +echo "== sys_phone_icon_mic_off == +dd if=$BASE/109_sys_phone_icon_mic_off.bin of=$DEV bs=4 seek=32097408 count=835 +echo "== sys_phone_icon_mic_on == +dd if=$BASE/110_sys_phone_icon_mic_on.bin of=$DEV bs=4 seek=32096512 count=835 +echo "== sys_phone_icon_no_calllog == +dd if=$BASE/111_sys_phone_icon_no_calllog.bin of=$DEV bs=4 seek=32094592 count=1859 +echo "== sys_phone_icon_no_contacts == +dd if=$BASE/112_sys_phone_icon_no_contacts.bin of=$DEV bs=4 seek=32092672 count=1859 +echo "== sys_phone_icon_phone_icon == +dd if=$BASE/113_sys_phone_icon_phone_icon.bin of=$DEV bs=4 seek=32090368 count=1283 +echo "== sys_phone_icon_phone_icon_small == +dd if=$BASE/114_sys_phone_icon_phone_icon_small.bin of=$DEV bs=4 seek=32089728 count=515 +echo "== sys_phone_icon_plus == +dd if=$BASE/115_sys_phone_icon_plus.bin of=$DEV bs=4 seek=32088832 count=835 +echo "== sys_phone_icon_spk_icon_64 == +dd if=$BASE/116_sys_phone_icon_spk_icon_64.bin of=$DEV bs=4 seek=32084224 count=1283 +echo "== sys_phone_icon_star == +dd if=$BASE/117_sys_phone_icon_star.bin of=$DEV bs=4 seek=32083328 count=835 +echo "== sys_phone_icon_watch_icon == +dd if=$BASE/118_sys_phone_icon_watch_icon.bin of=$DEV bs=4 seek=32081920 count=1283 +echo "== sys_phone_icon_watch_icon_small == +dd if=$BASE/119_sys_phone_icon_watch_icon_small.bin of=$DEV bs=4 seek=32081280 count=515 +echo "== sys_phone_widget_widget_contact == +dd if=$BASE/120_sys_phone_widget_widget_contact.bin of=$DEV bs=4 seek=32026624 count=3395 +echo "== sys_phone_widget_widget_dialpad == +dd if=$BASE/121_sys_phone_widget_widget_dialpad.bin of=$DEV bs=4 seek=32023168 count=3395 +echo "== sys_phone_widget_widget_phone == +dd if=$BASE/122_sys_phone_widget_widget_phone.bin of=$DEV bs=4 seek=32019712 count=3395 +echo "== sys_recorder_ahead3 == +dd if=$BASE/123_sys_recorder_ahead3.bin of=$DEV bs=4 seek=27191936 count=1859 +echo "== sys_recorder_back3 == +dd if=$BASE/124_sys_recorder_back3.bin of=$DEV bs=4 seek=27190016 count=1859 +echo "== sys_recorder_main == +dd if=$BASE/125_sys_recorder_main.bin of=$DEV bs=4 seek=27168256 count=835 +echo "== sys_recorder_mark == +dd if=$BASE/126_sys_recorder_mark.bin of=$DEV bs=4 seek=27167360 count=835 +echo "== sys_recorder_mute == +dd if=$BASE/127_sys_recorder_mute.bin of=$DEV bs=4 seek=27165568 count=835 +echo "== sys_recorder_sound == +dd if=$BASE/128_sys_recorder_sound.bin of=$DEV bs=4 seek=27163776 count=835 +echo "== sys_recorder_trash == +dd if=$BASE/129_sys_recorder_trash.bin of=$DEV bs=4 seek=27152128 count=835 +echo "== sys_recorder_warning == +dd if=$BASE/130_sys_recorder_warning.bin of=$DEV bs=4 seek=27150208 count=1859 +echo "== sys_timer_icon_close == +dd if=$BASE/131_sys_timer_icon_close.bin of=$DEV bs=4 seek=15576448 count=835 +echo "== sys_timer_icon_pause == +dd if=$BASE/132_sys_timer_icon_pause.bin of=$DEV bs=4 seek=15574656 count=835 +echo "== sys_timer_icon_restart == +dd if=$BASE/133_sys_timer_icon_restart.bin of=$DEV bs=4 seek=15573760 count=835 +echo "== sys_timer_icon_start == +dd if=$BASE/134_sys_timer_icon_start.bin of=$DEV bs=4 seek=15506304 count=835 +echo "== sys_timer_icon_stop == +dd if=$BASE/135_sys_timer_icon_stop.bin of=$DEV bs=4 seek=15505408 count=835 +echo "== sys_timer_icon_widget_light12 == +dd if=$BASE/136_sys_timer_icon_widget_light12.bin of=$DEV bs=4 seek=15474176 count=30211 +echo "== sys_timer_icon_widget_light12_five == +dd if=$BASE/137_sys_timer_icon_widget_light12_five.bin of=$DEV bs=4 seek=15443840 count=30211 +echo "== sys_timer_icon_widget_pause == +dd if=$BASE/138_sys_timer_icon_widget_pause.bin of=$DEV bs=4 seek=15442944 count=835 +echo "== sys_timer_icon_widget_start == +dd if=$BASE/139_sys_timer_icon_widget_start.bin of=$DEV bs=4 seek=15442048 count=835 +echo "== sys_todolist_checked == +dd if=$BASE/140_sys_todolist_checked.bin of=$DEV bs=4 seek=15420288 count=835 +echo "== sys_todolist_create_todo_btn == +dd if=$BASE/141_sys_todolist_create_todo_btn.bin of=$DEV bs=4 seek=15416064 count=4099 +echo "== sys_todolist_dark12_bg == +dd if=$BASE/142_sys_todolist_dark12_bg.bin of=$DEV bs=4 seek=15400192 count=15747 +echo "== sys_todolist_empty == +dd if=$BASE/143_sys_todolist_empty.bin of=$DEV bs=4 seek=15398272 count=1859 +echo "== sys_todolist_reminder_big == +dd if=$BASE/144_sys_todolist_reminder_big.bin of=$DEV bs=4 seek=15372928 count=4355 +echo "== sys_todolist_reminder_small == +dd if=$BASE/145_sys_todolist_reminder_small.bin of=$DEV bs=4 seek=15371008 count=1859 +echo "== sys_todolist_tdl_dark11_bg == +dd if=$BASE/146_sys_todolist_tdl_dark11_bg.bin of=$DEV bs=4 seek=15362944 count=8003 +echo "== sys_todolist_tdl_light11_bg == +dd if=$BASE/147_sys_todolist_tdl_light11_bg.bin of=$DEV bs=4 seek=15354880 count=8003 +echo "== sys_todolist_unchecked == +dd if=$BASE/148_sys_todolist_unchecked.bin of=$DEV bs=4 seek=15353984 count=835 +echo "== sys_todolist_widget_dark_icon == +dd if=$BASE/149_sys_todolist_widget_dark_icon.bin of=$DEV bs=4 seek=15352064 count=1859 +echo "== sys_todolist_widget_light_icon == +dd if=$BASE/150_sys_todolist_widget_light_icon.bin of=$DEV bs=4 seek=15350144 count=1859 +echo "rollback done - run reboot to apply" diff --git a/s4e-icons/tools/build_faces.py b/s4e-icons/tools/build_faces.py new file mode 100644 index 0000000..6ec566c --- /dev/null +++ b/s4e-icons/tools/build_faces.py @@ -0,0 +1,80 @@ +"""overlay + 官方模板 -> Compiler.exe -> .face + +要点(都是踩过的坑): + * preview.png 必须是 326x326,Compiler.exe 会直接拒绝别的尺寸 + * watchfaceId 要带机型家族前缀:S4 家族 = 362150xxx(固件自带表盘是 362150004..7) + * 用 Python 的 copytree 拷目录,别用 PowerShell 的 Copy-Item -Recurse(会拍平) +""" +import os, json, shutil, subprocess, sys +from paths import WORK, TEMPLATE + +TPL = TEMPLATE +OVER = os.path.join(WORK, 'overlay') +BUILD = os.path.join(WORK, 'build') + +# name -> (watchfaceId, description) +TARGETS = {'S5Icons': ('362150101', 'S5 41mm OS4 icons -> S4 eSIM')} + + +def prepare(name): + oid, _ = TARGETS[name] + ov = os.path.join(OVER, name) + dst = os.path.join(BUILD, name) + if not os.path.exists(TPL): + raise SystemExit('找不到官方模板:%s\n请把 FangAiden/LuaDevTemplate 放到 vendor/ 下(见 README)' % TPL) + if os.path.exists(dst): + shutil.rmtree(dst) + shutil.copytree(TPL, dst) + + old = os.path.join(dst, 'watchface', 'fprj', 'LuaDevTemplate.fprj') + if os.path.exists(old): + os.remove(old) + + cfg = json.load(open(os.path.join(ov, 'watchface.config.json'), encoding='utf-8')) + cfg['watchfaceId'] = oid + json.dump(cfg, open(os.path.join(dst, 'watchface.config.json'), 'w', encoding='utf-8'), + indent=4, ensure_ascii=False) + + shutil.copyfile(os.path.join(ov, 'watchface', 'fprj', f'{name}.fprj'), + os.path.join(dst, 'watchface', 'fprj', f'{name}.fprj')) + shutil.copyfile(os.path.join(ov, 'watchface', 'fprj', 'images', 'preview.png'), + os.path.join(dst, 'watchface', 'fprj', 'images', 'preview.png')) + shutil.copyfile(os.path.join(ov, 'watchface', 'fprj', 'app', 'lua', 'main.lua'), + os.path.join(dst, 'watchface', 'fprj', 'app', 'lua', 'main.lua')) + + img = os.path.join(dst, 'watchface', 'fprj', 'app', 'images') + os.makedirs(img, exist_ok=True) + ovimg = os.path.join(ov, 'watchface', 'fprj', 'app', 'images') + for d in sorted(x for x in os.listdir(ovimg) if os.path.isdir(os.path.join(ovimg, x))): + tgt = os.path.join(img, d) + if os.path.exists(tgt): + shutil.rmtree(tgt) + shutil.copytree(os.path.join(ovimg, d), tgt) + print(f' {name}: app/images/{d}/ -> {len(os.listdir(tgt))} files') + return dst + + +def build(dst): + ps = os.path.join(dst, 'scripts', 'build_face.ps1') + r = subprocess.run(['powershell', '-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', ps], + capture_output=True, text=True, errors='replace') + for l in [x for x in (r.stdout or '').splitlines() if x.strip()][-6:]: + print(' |', l) + if r.returncode != 0: + print(' !! build failed, rc=', r.returncode) + return r.returncode == 0 + + +if __name__ == '__main__': + only = sys.argv[1] if len(sys.argv) > 1 else None + for name in TARGETS: + if only and name != only: + continue + print(f'== {name} (id={TARGETS[name][0]}) ==') + dst = prepare(name) + ok = build(dst) + face = os.path.join(dst, 'bin', f'{name}.face') + if os.path.exists(face): + print(f' OK {face} {os.path.getsize(face)} bytes') + else: + print(' missing output') diff --git a/s4e-icons/tools/check_clip.py b/s4e-icons/tools/check_clip.py new file mode 100644 index 0000000..fc8ad4c --- /dev/null +++ b/s4e-icons/tools/check_clip.py @@ -0,0 +1,58 @@ +"""用真实渲染结果判"会不会被圆屏切掉"。 + +做法:渲染一张**不套圆形遮罩**的 RGBA,然后量所有非透明像素到圆心的距离。 +这样自动考虑字体实际字宽、折行、圆角、图片缩放 —— 比按元素外框估算准得多。 +""" +import os, sys, math +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from PIL import Image, ImageDraw +from render_ui import draw_label, W, H +from pages import PAGES + +CX = CY = 233.0 +R = 233.0 + + +def render_raw(items): + img = Image.new('RGBA', (W, None) if False else (W, H), (0, 0, 0, 0)) + d = ImageDraw.Draw(img) + from render_ui import _rgb, font + for it in items: + k = it.get('kind') + if k == 'rect': + box = [it['x'], it['y'], it['x'] + it['w'] - 1, it['y'] + it['h'] - 1] + r = it.get('radius', 0) + if r: + d.rounded_rectangle(box, radius=r, fill=_rgb(it['bg']) + (255,)) + else: + d.rectangle(box, fill=_rgb(it['bg']) + (255,)) + elif k == 'circle': + d.ellipse([it['x'], it['y'], it['x'] + it['w'] - 1, it['y'] + it['h'] - 1], + fill=_rgb(it['bg']) + (255,)) + elif k == 'image': + if os.path.exists(it['src']): + im = Image.open(it['src']).convert('RGBA').resize((it['w'], it['h']), Image.LANCZOS) + img.alpha_composite(im, (it['x'], it['y'])) + elif k == 'label': + draw_label(img, d, it) + return img + + +print(f'{"page":<10} {"max r":>7} {"outside px":>11} worst point / note') +print('-' * 78) +for name, items in PAGES.items(): + im = render_raw(items) + px = im.load() + worst, wpt, outside = 0.0, None, 0 + for y in range(H): + for x in range(W): + if px[x, y][3] > 8: + d = math.hypot(x - CX, y - CY) + if d > worst: + worst, wpt = d, (x, y) + if d > R: + outside += 1 + note = '全部在圆内' if worst <= R else f'会被切 {outside} px' + print(f'{name:<10} {worst:>7.1f} {outside:>11} {wpt} {note}') +print('-' * 78) +print(f'圆心 ({CX:.0f},{CY:.0f}) 半径 {R:.0f} 画布 {W}x{H}') diff --git a/s4e-icons/tools/finalize_faces.py b/s4e-icons/tools/finalize_faces.py new file mode 100644 index 0000000..98bad33 --- /dev/null +++ b/s4e-icons/tools/finalize_faces.py @@ -0,0 +1,68 @@ +"""产出最终 .face。 + + * Compiler.exe 写 byte5 = 0x00 + 自己的随机 id 到 40..49 + * 官方 set_face_id.ps1 会把 byte5 改成 10 并写 id + * 固件里 25 个真表盘 byte5 ∈ {0,1},从不为 10 + => 忠实产物 = Compiler.exe 原始输出,只替换 40..49(我们称 b5=0 那份) + 同时保留官方工具链那份(b5=10),装不上时可以换。 +""" +import os, shutil, subprocess, hashlib +from paths import WORK, DIST + +BUILD = os.path.join(WORK, 'build') +TARGETS = {'S5Icons': '362150101'} + + +def compile_raw(name): + w = os.path.join(BUILD, name) + exe = os.path.join(w, 'watchface', 'tools', 'Compiler.exe') + if not os.path.exists(exe): + print(f' {name}: 跳过(没有构建树,先跑 build_faces.py {name})') + return None + fprj = os.path.join(w, 'watchface', 'fprj', f'{name}.fprj') + out = os.path.join(WORK, 'raw_' + name) + if os.path.exists(out): + shutil.rmtree(out) + os.makedirs(out) + subprocess.run([exe, '-b', fprj, out, f'{name}.face', TARGETS[name]], + capture_output=True, text=True, errors='replace') + p = os.path.join(out, f'{name}.face') + return p if os.path.exists(p) else None + + +def patch_id(src, dst, wid): + d = bytearray(open(src, 'rb').read()) + if d[0:4] != b'\x5a\xa5\x34\x12': + raise SystemExit('bad magic') + if len(wid) > 10: + raise SystemExit('id too long') + d[40:50] = wid.encode() + b'\x00' * (10 - len(wid)) + open(dst, 'wb').write(bytes(d)) + return dst + + +def main(): + os.makedirs(DIST, exist_ok=True) + report = [] + for name, wid in TARGETS.items(): + raw = compile_raw(name) + if not raw: + continue + faithful = patch_id(raw, os.path.join(DIST, f'{name}.face'), wid) + tc_src = os.path.join(BUILD, name, 'bin', f'{name}.face') + tc = os.path.join(DIST, f'{name}_toolchain.face') + if os.path.exists(tc_src): + shutil.copyfile(tc_src, tc) + for p in (faithful, tc): + if os.path.exists(p): + d = open(p, 'rb').read() + report.append((os.path.basename(p), len(d), d[4], d[5], d[40:50], + hashlib.sha256(d).hexdigest()[:16])) + print(f'{"file":<32}{"bytes":>10} b4 b5 id sha256[:16]') + for f, n, b4, b5, sid, h in report: + print(f'{f:<32}{n:>10} {b4:<3} {b5:<3} {sid!r:<20} {h}') + print('->', DIST) + + +if __name__ == '__main__': + main() diff --git a/s4e-icons/tools/gen_ui_lua.py b/s4e-icons/tools/gen_ui_lua.py new file mode 100644 index 0000000..3e07b76 --- /dev/null +++ b/s4e-icons/tools/gen_ui_lua.py @@ -0,0 +1,142 @@ +"""规范化构建:表盘源码 + 资源 -> .face(两遍,自动回填 resource.bin 内的偏移) + + main.lua <- **源码**,含 __UIIMG__ / __PAY__ / __RB__ 占位符 + .work/overlay/S5Icons <- 中间产物(回填后的 Lua 落在这里) + .work/build/S5Icons <- 编译工作区(Compiler.exe 的实际输入) + dist/S5Icons.face <- 交付物 + +为什么要两遍:.face 里各资源的偏移取决于打包顺序,我们靠在 .face 里搜文件字节来定位。 +Lua 源码排在所有资源**之后**,所以回填(让 Lua 变长)不会移动前面资源的位置 —— 第一遍量出来的 +偏移在第二遍依然成立,脚本最后会逐条校验。 +""" +import os, re, sys, json, subprocess +from paths import PROJ, WORK + +TOOLS = os.path.dirname(os.path.abspath(__file__)) +SRC = os.path.join(PROJ, 'main.lua') +OVERLAY_APP = os.path.join(WORK, 'overlay', 'S5Icons', 'watchface', 'fprj', 'app') +RESOLVED = os.path.join(OVERLAY_APP, 'lua', 'main.lua') +FACE = os.path.join(WORK, 'build', 'S5Icons', 'bin', 'S5Icons.face') +OFFS = os.path.join(WORK, 'face_payload_offsets.json') +PY = sys.executable + +UINAMES = ['back', 'ic_help', 'ic_replace', 'ic_about', 'ic_check', 'ic_rollback', + 'ic_qq', 'logo'] + + +def run(script, *args): + r = subprocess.run([PY, os.path.join(TOOLS, script)] + list(args), + capture_output=True, text=True, encoding='utf-8', errors='replace') + if r.returncode != 0: + print(r.stdout[-2000:]); print(r.stderr[-2000:]) + raise SystemExit(f'{script} 执行失败') + return r.stdout + + +def find_blob(face, blob): + hits, start = [], 0 + while True: + i = face.find(blob, start) + if i < 0: + break + hits.append(i) + start = i + 1 + if len(hits) > 4: + break + return hits + + +def payload_rows(tag, lua_payload, offs): + src = open(lua_payload, encoding='utf-8').read() + tgt = {} + for m in re.finditer(r'file = "([^"]+)", offset = (\d+), length = (\d+)', src): + tgt[m.group(1)] = (int(m.group(2)), int(m.group(3))) + rows = [] + for name, ln, hits, sig in offs[tag]: + if name not in tgt: + continue + blob = min(hits) if tag == 'payload' else max(hits) + to, tl = tgt[name] + rows.append((blob, to, tl)) + rows.sort(key=lambda r: r[1]) + return rows + + +def main(): + tpl = open(SRC, encoding='utf-8').read() + if '__UIIMG__' not in tpl: + raise SystemExit('main.lua 里没有占位符 —— 它应该是源码模板,不要把回填后的版本存回去') + print('第 1 遍:用占位符编一次,量出资源在 resource.bin 里的位置') + run('make_overlays.py') + run('build_faces.py', 'S5Icons') + face = open(FACE, 'rb').read() + print(f' face {len(face)} bytes') + + ui_rows, bad = [], 0 + for name in UINAMES: + blob = open(os.path.join(PROJ, 'ui_images', name + '.bin'), 'rb').read() + hits = find_blob(face, blob) + if len(hits) != 1: + print(f' !! {name} 命中 {len(hits)} 次(应为 1)') + bad += 1 + if hits: + ui_rows.append((hits[0], len(blob), name)) + print(f' {name:<12} off=0x{hits[0]:x} len={len(blob)}') + if bad: + raise SystemExit('有资源无法唯一定位,检查是否有重复内容') + + # 负载是「魔数 + 顺序拼接」的**单个条目**(blob/),偏移 = 魔数位置 + 魔数长度 + 相对偏移 + blobs = json.load(open(os.path.join(PROJ, 'blob', 'offsets.json'), encoding='utf-8')) + pay_blob = open(os.path.join(PROJ, 'blob', 'payload.bin'), 'rb').read() + rb_blob = open(os.path.join(PROJ, 'blob', 'rollback.bin'), 'rb').read() + ml = blobs['magic_len'] + pay_base = face.find(pay_blob[:15]) + rb_base = face.find(rb_blob[:15]) + if face.count(pay_blob[:15]) != 1 or face.count(rb_blob[:15]) != 1: + raise SystemExit('魔数命中次数 != 1(payload %d / rollback %d)' + % (face.count(pay_blob[:15]), face.count(rb_blob[:15]))) + # 单条目必须**原样连续**存放,否则「base + 魔数长度 + 相对偏移」不成立 + if face.find(pay_blob) != pay_base or face.find(rb_blob) != rb_base: + raise SystemExit('负载不是原样连续存放') + pay = [(pay_base + ml + r['rel'], r['target'], r['length']) for r in blobs['records']] + rb = [(rb_base + ml + r['rel'], r['target'], r['length']) for r in blobs['records']] + print(f' 单条目:payload @0x{pay_base:x}({len(pay_blob)} B)/ rollback @0x{rb_base:x}' + f'({len(rb_blob)} B),共 {len(pay)} 条记录') + + ui_txt = '\n'.join(f' {{{o},{l},"{n}"}},' for o, l, n in ui_rows) + pay_txt = ' pay = {\n' + '\n'.join(f' {{{o},{t},{l}}},' for o, t, l in pay) + '\n },' + rb_txt = ' rb = {\n' + '\n'.join(f' {{{o},{t},{l}}},' for o, t, l in rb) + '\n },' + resolved = (tpl.replace('__UIIMG__', ui_txt) + .replace('__PAY__', pay_txt) + .replace('__RB__', rb_txt)) + assert '__UIIMG__' not in resolved and 'pay = {}' not in resolved + os.makedirs(os.path.dirname(RESOLVED), exist_ok=True) + open(RESOLVED, 'w', encoding='utf-8', newline='\n').write(resolved) + print(f'第 2 遍:回填后的 Lua 写入 overlay({len(resolved)} bytes),重新编译') + + run('build_faces.py', 'S5Icons') + print(run('finalize_faces.py').strip()) + + face2 = open(FACE, 'rb').read() + drift = 0 + for name in UINAMES: + blob = open(os.path.join(PROJ, 'ui_images', name + '.bin'), 'rb').read() + hits = find_blob(face2, blob) + row = [r for r in ui_rows if r[2] == name] + if not row or not hits or hits[0] != row[0][0]: + print(f' !! 偏移漂移 {name}') + drift += 1 + for tag, rows, d in (('pay', pay, os.path.join(PROJ, 'payload')), + ('rb', rb, os.path.join(PROJ, 'payload_rollback'))): + files = [(f, open(os.path.join(d, f), 'rb').read()) + for f in sorted(os.listdir(d)) if f.endswith('.bin')] + ok = sum(1 for off, tgt, ln in rows + if any(len(x) == ln and face2[off:off + ln] == x for _, x in files)) + if ok != len(rows): + print(f' !! {tag} 只有 {ok}/{len(rows)} 条吻合'); drift += 1 + print(f'偏移校验:{"全部吻合,0 漂移" if drift == 0 else str(drift) + " 处异常"}') + print('交付物:', os.path.join(PROJ, 'dist', 'S5Icons.face')) + + +if __name__ == '__main__': + main() diff --git a/s4e-icons/tools/iconpack.py b/s4e-icons/tools/iconpack.py new file mode 100644 index 0000000..4012c6e --- /dev/null +++ b/s4e-icons/tools/iconpack.py @@ -0,0 +1,447 @@ +"""Build / apply a system-app launcher-icon replacement for a Vela `vela_app.bin`. + +Why "same length" is the whole ball game +---------------------------------------- +ROMFS packs superblock -> inode -> name -> file data strictly back to back, and +every `next` / `spec` pointer is an ABSOLUTE offset. If a replacement file +changes length by even one byte, every later offset shifts and the image is +corrupt. So a replacement must encode to EXACTLY the original byte length: + + * source PNG == original dimensions -> encode, lengths match, done. + * source PNG != original dimensions -> two options: + (default) resize to the original dimensions (icon renders identically) + --pad keep the source dimensions, zero-pad the container to the + original length (renders smaller unless the launcher scales) + +Usage: + iconpack.py list + iconpack.py make [--pad] [--patched] + iconpack.py apply +""" +import os, sys, json, struct, hashlib, shutil +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from romfs import Romfs, img_header +from lvgl_bin import rle_compress, pad_rle +from PIL import Image + +CF_ARGB8888, CF_I8, MAGIC = 0x10, 0x0a, 0x19 +FMT = {CF_ARGB8888: 'ARGB8888', CF_I8: 'I8'} +RESAMPLE = Image.LANCZOS + + +# ----------------------------------------------------------------- encoders +def encode_argb8888(img): + rgba = img.convert('RGBA') + w, h = rgba.size + px = rgba.load() + out = bytearray() + for y in range(h): + row = bytearray() + for x in range(w): + r, g, b, a = px[x, y] + row += bytes((b, g, r, a)) + out += row + return bytes(out) + + +def encode_i8(img, alpha_levels=16, sharpen_up=0.0): + """I8 encoder with an ALPHA-AWARE palette. + + The naive version quantised RGB only and then stored one *averaged* alpha + per palette entry. Any colour shared by an opaque body pixel and a + semi-transparent edge pixel collapsed to a mid alpha, and fully transparent + pixels inherited that same entry -- so the transparent margin became faintly + visible (measured: perpetual_calendar haze 0.000 -> 1.000, i.e. a grey veil + around the whole icon). + + Here the palette is built over (quantised RGB, quantised alpha) PAIRS, so a + colour can own several entries at different alphas and alpha 0 stays alpha 0. + """ + rgba = img.convert('RGBA') + w, h = rgba.size + src = rgba.load() + + # average colour of the visible pixels: transparent pixels must not invent + # their own colour clusters + rs = gs = bs = n = 0 + for y in range(h): + for x in range(w): + r, g, b, a = src[x, y] + if a > 0: + rs += r; gs += g; bs += b; n += 1 + avg = (rs // n, gs // n, bs // n) if n else (0, 0, 0) + + step = max(1, 256 // max(1, alpha_levels)) + flat = Image.new('RGB', (w, h)) + fp = flat.load() + qa = [[0] * w for _ in range(h)] + for y in range(h): + for x in range(w): + r, g, b, a = src[x, y] + fp[x, y] = (r, g, b) if a > 0 else avg + q = int(round(a / float(step))) * step + qa[y][x] = 255 if q > 255 else q + + p = flat.quantize(colors=256, method=Image.MEDIANCUT) + pal_rgb = p.getpalette()[:768] + idx = p.tobytes() + + cnt = {} + for y in range(h): + row = y * w + for x in range(w): + k = (idx[row + x], qa[y][x]) + cnt[k] = cnt.get(k, 0) + 1 + keep = sorted(cnt.items(), key=lambda kv: -kv[1])[:256] + + pal = bytearray() + remap = {} + by_rgb = {} + for slot, ((pi, al), _c) in enumerate(keep): + r = pal_rgb[pi * 3] if pi * 3 + 2 < len(pal_rgb) else 0 + g = pal_rgb[pi * 3 + 1] if pi * 3 + 2 < len(pal_rgb) else 0 + b = pal_rgb[pi * 3 + 2] if pi * 3 + 2 < len(pal_rgb) else 0 + pal += bytes((b, g, r, al)) + remap[(pi, al)] = slot + by_rgb.setdefault(pi, []).append((al, slot)) + removed = 256 - len(keep) + if removed > 0: + pal += b'\x00\x00\x00\x00' * removed + for pi in by_rgb: + by_rgb[pi].sort() + + out = bytearray(w * h) + for y in range(h): + row = y * w + for x in range(w): + pi = idx[row + x] + al = qa[y][x] + s = remap.get((pi, al)) + if s is None: + cand = by_rgb.get(pi) + if cand: + s = min(cand, key=lambda t: abs(t[0] - al))[1] + else: + s = 0 + out[row + x] = s + return bytes(pal) + bytes(out) + + +def encode_i8_palette(img, pal_bgra, alpha_tol=12): + """Quantise to an EXISTING 256-entry BGRA palette (as stored in an I8 blob). + + Reusing the source icon's own palette preserves the artwork's gradient shades + and its anti-aliased edge levels. A freshly clustered palette cannot: the + plate gradient eats the 256 entries, the edge collapses to a few alpha steps + and dropped pixels land on wrong entries -- measured on remote_camera as a + jagged plate rim plus stray coloured speckles. + + The search is constrained by alpha so that + * fully transparent source pixels can only pick transparent entries + (no veil, no speckles), and + * fully opaque source pixels can only pick opaque entries + (the body never goes translucent). + """ + rgba = img.convert('RGBA') + w, h = rgba.size + px = rgba.load() + + ent = [(pal_bgra[i * 4 + 2], pal_bgra[i * 4 + 1], + pal_bgra[i * 4], pal_bgra[i * 4 + 3]) for i in range(256)] + clear = [i for i, e in enumerate(ent) if e[3] == 0] + if not clear: # guarantee a transparent entry exists + worst = min(range(256), key=lambda i: ent[i][3]) + ent[worst] = (0, 0, 0, 0) + clear = [worst] + solid = [i for i, e in enumerate(ent) if e[3] >= 248] + by_alpha = {} + for i, e in enumerate(ent): + by_alpha.setdefault(e[3], []).append(i) + + def premul(e): + r, g, b, a = e + return (r * a // 255, g * a // 255, b * a // 255, a) + + P = [premul(e) for e in ent] + cache = {} + out = bytearray(w * h) + for y in range(h): + row = y * w + for x in range(w): + r, g, b, a = px[x, y] + key = (r >> 2, g >> 2, b >> 2, a >> 3) + s = cache.get(key) + if s is None: + if a == 0: + cand = clear + elif a >= 248 and solid: + cand = solid + else: + cand = [] + for da in range(0, alpha_tol + 1): + cand += by_alpha.get(a + da, ()) if a + da <= 255 else () + if da: + cand += by_alpha.get(a - da, ()) if a - da >= 0 else () + if not cand: + cand = range(256) + pr, pg, pb, pa = premul((r, g, b, a)) + best, bd = cand[0], None + for i in cand: + q = P[i] + d = ((pr - q[0]) ** 2 + (pg - q[1]) ** 2 + (pb - q[2]) ** 2 + + 2 * (pa - q[3]) ** 2) + if bd is None or d < bd: + bd, best = d, i + s = best + cache[key] = s + out[row + x] = s + + pal = bytearray() + for (r, g, b, a) in ent: + pal += bytes((b, g, r, a)) + return bytes(pal) + bytes(out) + + +def encode(img, cf, w, h, flags=0, pad_to=None, palette=None): + if cf == CF_ARGB8888: + body = encode_argb8888(img) + elif palette is not None: + body = encode_i8_palette(img, palette) + else: + body = encode_i8(img) + stride = w * (4 if cf == CF_ARGB8888 else 1) + blob = struct.pack(' pad_to: + raise ValueError(f'encoded {len(blob)} > slot {pad_to}') + blob += b'\x00' * (pad_to - len(blob)) + return blob + + +# ----------------------------------------------------------------- helpers +def raw_i8_strided(img, stride, colors=256): + """I8 像素流:1024 B 调色板(BGRA)+ stride*h 个索引(行尾按 header 的 stride 补 0)。 + + RLE 槽位专用:用 FASTOCTREE 自适应减色 + 关闭抖动 —— 颜色越少、相邻像素越容易出现 + 长游程,RLE 流就越短(这是能不能塞进原 csize 的关键)。 + """ + q = img.convert('RGBA').quantize(colors=max(2, min(256, colors)), + method=Image.FASTOCTREE, dither=Image.NONE) + if q.palette.mode == 'RGBA': + pal = list(q.getpalette('RGBA')) + else: + rgb = q.getpalette('RGB') + pal = [v for i in range(256) for v in (rgb[i * 3], rgb[i * 3 + 1], rgb[i * 3 + 2], 255)] + pal = (pal + [0] * 1024)[:1024] + pal_b = bytearray() + for i in range(256): # 存储序:B, G, R, A + r, g, b, a = pal[i * 4:i * 4 + 4] + pal_b += bytes((b, g, r, a)) + idx = q.tobytes() + w, h = img.size + rows = bytearray() + for y in range(h): + rows += idx[y * w:(y + 1) * w] + if stride > w: + rows += b'\x00' * (stride - w) + return bytes(pal_b) + bytes(rows) + + +# 从大到小试:优先保真,装不下再减色 +RLE_COLOR_STEPS = (256, 192, 160, 128, 96, 80, 64, 48, 40, 32, 24, 16, 12, 8, 6, 4) + + +def encode_into_slot(img, stock_blob, slot_len=None, colors=None): + """把 img 编成与 stock_blob **逐字节等长** 的 LVGL v9 blob(沿用原 header)。 + + 真实固件里这些 UI 素材有两种槽位形态,都必须支持,否则 ROMFS 后面所有偏移全平移: + * ARGB8888 未压缩(144x144 图标那种):像素流 + 零填充到原长度 + * I8 + RLE 压缩(walkie_talkie 里几乎全是):量化 -> RLE -> 用 ctrl=0 空指令补齐到原 csize + (颜色数从 256 逐级往下试,取第一个能塞进原 csize 的) + 返回 (blob, how);不支持 / 压缩后仍超长时返回 (None, 原因)。 + """ + h = img_header(stock_blob) + if h is None: + return None, '不是 LVGL bin' + w, ht, cf, flags = h['w'], h['h'], h['cf'], h['flags'] + if slot_len is None: + slot_len = len(stock_blob) + out_img = img.convert('RGBA') + if out_img.size != (w, ht): + # 约定:**长宽比不一致的素材直接不替换**(保持原厂图),而不是拉伸或裁切 —— + # 拉伸会把方块变长方形;裁切会切掉按钮两端。宁可保持原厂,也不给设备塞变形图。 + pw, ph = out_img.size + if abs(pw / ph - w / ht) / (w / ht) > 0.005: + return None, ('长宽比不一致(源 %dx%d = %.3f,槽位 %dx%d = %.3f)-> 按约定跳过,保持原厂' + % (pw, ph, pw / ph, w, ht, w / ht)) + out_img = out_img.resize((w, ht), RESAMPLE) + if cf == CF_ARGB8888 and not (flags & 0x08): + blob = stock_blob[:12] + encode_argb8888(out_img) + if len(blob) > slot_len: + return None, 'ARGB 编码 %d > 槽位 %d' % (len(blob), slot_len) + return blob + b'\x00' * (slot_len - len(blob)), 'ARGB(补 %d)' % (slot_len - len(blob)) + if cf == CF_I8 and (flags & 0x08): + method, csize, usize = struct.unpack(' csize %d(已减到 %d 色)' % (len(comp), csize, n_last) + n, raw, comp = best + comp = comp + bytes((1, raw[-1])) # 把流做到正好 usize 字节,和原厂一致 + padded = pad_rle(comp, csize) + if padded is None: + return None, '补齐失败' + blob = stock_blob[:12] + struct.pack(' inode for every launcher icon. + + alarm/launcher.bin -> alarm + sports/training/launcher.bin -> sports_training + phone/contacts_launcher.bin -> phone_contacts + """ + out = {} + for p, i in r.entries(): + if not p.rsplit('/', 1)[-1].endswith('launcher.bin'): + continue + stem = p[:-4] # drop '.bin' + if stem.endswith('_launcher'): + stem = stem[:-len('_launcher')] + elif stem.endswith('/launcher'): + stem = stem[:-len('/launcher')] + out[stem.replace('/', '_')] = i + return out + + +def describe(r, i): + h = img_header(r.read(i)) + if not h: + return '?', '?', '?' + return f'{h["w"]}x{h["h"]}', FMT.get(h['cf'], f'0x{h["cf"]:02x}'), h + + +def cmd_list(image): + r = Romfs(image) + print(f'{os.path.basename(image)} volume={r.volume} romfs_size={r.sb_size}') + for app, i in sorted(targets(r).items()): + dim, fmt, _ = describe(r, i) + print(f' {app:<26} data@0x{i.data_off:08x} len={i.size:<7} {dim} {fmt}') + + +def cmd_make(image, pngdir, outdir, pad=False, write_patched=False): + r = Romfs(image) + tg = targets(r) + os.makedirs(outdir, exist_ok=True) + payload_dir = os.path.join(outdir, 'payload') + os.makedirs(payload_dir, exist_ok=True) + patch = bytearray() + rollback = bytearray() + records, skipped = [], [] + for fn in sorted(os.listdir(pngdir)): + if not fn.lower().endswith('.png'): + continue + app = fn[:-4] + if app not in tg: + skipped.append((app, 'no such app')) + continue + ino = tg[app] + orig = r.read(ino) + _, fmt, h = describe(r, ino) + if h is None: + skipped.append((app, f'not LVGL image (len={ino.size})')) + continue + src = Image.open(os.path.join(pngdir, fn)).convert('RGBA') + src_dim = f'{src.size[0]}x{src.size[1]}' + resized = False + if src.size != (h['w'], h['h']): + if pad: + tw, th = src.size + blob = encode(src, h['cf'], tw, th, h['flags'], pad_to=len(orig)) + else: + src = src.resize((h['w'], h['h']), RESAMPLE) + tw, th = src.size + resized = True + blob = encode(src, h['cf'], tw, th, h['flags']) + else: + tw, th = src.size + blob = encode(src, h['cf'], tw, th, h['flags']) + if len(blob) != len(orig): + skipped.append((app, f'length {len(blob)} != {len(orig)}')) + continue + patch += struct.pack('12}{"bytes":>8} {"fmt":<10}{"stock":>9}{"source":>9} note') + for m in records: + note = 'resized' if m['resized'] else ('padded' if m['padded'] else '') + print(f'{m["app"]:<26}0x{m["offset"]:08x}{m["length"]:>8} {m["format"]:<10}' + f'{m["stock_dim"]:>9}{m["source_dim"]:>9} {note}') + for a, why in skipped: + print(f' SKIP {a}: {why}') + print(f'\n{len(records)} replacement(s)') + print(f' payload -> {payload_dir}') + print(f' patch -> {os.path.join(outdir, "icons.patch")} ({len(patch)} bytes)') + print(f' rollback -> {os.path.join(outdir, "icons.rollback")} ({len(rollback)} bytes)') + print(f' manifest -> {os.path.join(outdir, "manifest.json")}') + if write_patched: + out = os.path.join(outdir, 'vela_app_patched.bin') + cmd_apply(image, os.path.join(outdir, 'icons.patch'), out) + + +def cmd_apply(image, patch, out): + data = bytearray(open(image, 'rb').read()) + blob = open(patch, 'rb').read() + off = n = 0 + while off < len(blob): + addr, ln = struct.unpack(' {out}') + + +if __name__ == '__main__': + a = sys.argv[1:] + if not a: + print(__doc__) + elif a[0] == 'list': + cmd_list(a[1]) + elif a[0] == 'make': + cmd_make(a[1], a[2], a[3], '--pad' in a, '--patched' in a) + elif a[0] == 'apply': + cmd_apply(a[1], a[2], a[3]) + else: + print(__doc__) diff --git a/s4e-icons/tools/locate_payload_in_face.py b/s4e-icons/tools/locate_payload_in_face.py new file mode 100644 index 0000000..e0fc6d2 --- /dev/null +++ b/s4e-icons/tools/locate_payload_in_face.py @@ -0,0 +1,49 @@ +"""在编好的 .face 里定位每一份负载,好让 Lua 运行时按偏移从 resource.bin 里读出来 +(设备不会把 .face 里的 images/ 解包到文件系统,图片仍然躺在 resource.bin 里)。 +""" +import os, sys, json +from paths import PROJ, WORK + +FACE = os.path.join(WORK, 'build', 'S5Icons', 'bin', 'S5Icons.face') +PDIR = os.path.join(PROJ, 'payload') +RDIR = os.path.join(PROJ, 'payload_rollback') +OUT = os.path.join(WORK, 'face_payload_offsets.json') + +face = open(FACE, 'rb').read() +print(f'face {os.path.basename(FACE)} {len(face)} bytes') + +anchor = face.find(b'local lvgl = require("lvgl")') +print(f'lua source starts at 0x{anchor:x} ({anchor})') + +out = {} +for tag, d in (('payload', PDIR), ('rollback', RDIR)): + recs = [] + dup = 0 + for name in sorted(os.listdir(d)): + if not name.endswith('.bin') or name.endswith('payload.lua'): + continue + data = open(os.path.join(d, name), 'rb').read() + if not data: + continue + hits, start = [], 0 + while True: + i = face.find(data, start) + if i < 0: + break + hits.append(i) + start = i + 1 + if len(hits) > 8: + break + recs.append((name, len(data), hits, data[:16].hex())) + if len(hits) != 1: + dup += 1 + out[tag] = recs + print(f'\n{tag}: {len(recs)} files, {dup} with !=1 occurrence') + for name, ln, hits, sig in recs[:6]: + print(f' {name:<34} {ln:>7} hits={[hex(h) for h in hits]}') + if recs: + print(' ...') + +json.dump({k: [(n, l, h, s) for n, l, h, s in v] for k, v in out.items()}, + open(OUT, 'w', encoding='utf-8'), indent=1) +print('\nsaved', OUT) diff --git a/s4e-icons/tools/lua_balance.py b/s4e-icons/tools/lua_balance.py new file mode 100644 index 0000000..a33ebba --- /dev/null +++ b/s4e-icons/tools/lua_balance.py @@ -0,0 +1,72 @@ +"""Crude but effective Lua block-balance checker: strips comments and strings, +then counts block openers vs `end`.""" +import sys, re + +def strip(src): + out = [] + i, n = 0, len(src) + while i < n: + c = src[i] + # long bracket comment / string --[[ ]] , --[==[ ]==] + m = re.match(r'--\[(=*)\[', src[i:]) + if m: + close = ']' + m.group(1) + ']' + j = src.find(close, i) + i = n if j < 0 else j + len(close) + out.append(' ') + continue + if src.startswith('--', i): + j = src.find('\n', i) + i = n if j < 0 else j + out.append(' ') + continue + m = re.match(r'\[(=*)\[', src[i:]) + if m: + close = ']' + m.group(1) + ']' + j = src.find(close, i) + i = n if j < 0 else j + len(close) + out.append('""') + continue + if c in '"\'': + j = i + 1 + while j < n: + if src[j] == '\\': + j += 2 + continue + if src[j] == c: + break + j += 1 + i = j + 1 + out.append('""') + continue + out.append(c) + i += 1 + return ''.join(out) + + +def check(path): + src = open(path, encoding='utf-8').read() + code = strip(src) + def cnt(k): + return len(re.findall(r'(? n: + raise ValueError('truncated literal run') + out += data[i:i + nb] + i += nb + else: + cnt = ctrl + if i + blk > n: + raise ValueError('truncated repeat unit') + unit = data[i:i + blk] + i += blk + out += unit * cnt + if out_len is not None and len(out) >= out_len: + break + return bytes(out) + + +def rle_compress(raw, max_run=127): + """LVGL RLE 编码器(rle_decompress 的逆向,blk=1)。 + + 贪心:先找同字节游程(>=2 用「重复」指令),否则攒「原样」指令。 + 输出长度总是 <= 原图像素流,因为最坏情况每条 literal 只多 1 个控制字节。 + """ + out = bytearray() + i, n = 0, len(raw) + while i < n: + v = raw[i] + j = i + 1 + while j < n and raw[j] == v and j - i < max_run: + j += 1 + run = j - i + if run >= 2: + out.append(run); out.append(v); i = j + continue + lit = bytearray() + k = i + while k < n and len(lit) < max_run: + if k + 1 < n and raw[k] == raw[k + 1]: + break + lit.append(raw[k]); k += 1 + if not lit: # 理论上到不了这里 + lit.append(v); k = i + 1 + out.append(0x80 | len(lit)); out += lit + i = k + return bytes(out) + + +def pad_rle(comp, want): + """把 RLE 流补到 want 字节:用 ctrl=0(重复 0 次 = 产出 0 像素)的 2 字节空指令。 + + ctrl=0 是合法指令(`len = 0`,两个越界判断都不触发),所以设备端解码器读完 csize + 字节既不会越界、也不会返回 INVALID —— 这是「等长覆盖」能成立的关键。 + 差值为奇数时先把一条 literal 拆成两条(payload 不变、多 1 个控制字节)。 + """ + d = want - len(comp) + if d < 0: + return None + if d % 2: + b = bytearray(comp) + for idx in range(len(b) - 1, -1, -1): + if b[idx] & 0x80 and (b[idx] & 0x7F) >= 2: + cnt = b[idx] & 0x7F + lit = bytes(b[idx + 1: idx + 1 + cnt]) + half = cnt // 2 + b[idx: idx + 1 + cnt] = (bytes([0x80 | half]) + lit[:half] + + bytes([0x80 | (cnt - half)]) + lit[half:]) + break + else: + return None + comp = bytes(b) + d -= 1 + return comp + b'\x00\x00' * (d // 2) + + +def parse_header(blob): + if len(blob) < 12 or blob[0] != MAGIC: + return None + magic, cf, flags, w, h, stride, rsv = struct.unpack('/payload/ NNN_.bin + payload.lua (bare filenames) + /payload_rollback/ NNN_.bin + payload.lua + /flash.sh linear nsh script (no $( ), no && , no globbing) + /rollback.sh + /payload.json machine-readable manifest + +All offsets are multiples of 512 and every emitted length is a multiple of 4, +so `dd bs=4 seek=offset/4 count=length/4` writes exact ranges with no partial +trailing block. +""" +import os, sys, json, hashlib +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +from paths import PROJ, INPUTS, WORK +STOCK = os.path.join(INPUTS, 'target', 'vela_app.bin') +PATCHED = os.path.join(WORK, 's4_from_s5', 'vela_app_s5icons.bin') +OUT = PROJ + +recs_in = json.load(open(os.path.join(WORK, 's4_from_s5', 'manifest.json'), encoding='utf-8')) +stock = open(STOCK, 'rb').read() +patched = open(PATCHED, 'rb').read() +assert len(stock) == len(patched) + +records = [dict(name='canary_superblock', offset=0, length=32, + data=stock[:32], changed=True)] +assert stock[:8] == b'-rom1fs-' +for r in recs_in['records']: + off, ln = r['offset'], r['length'] + records.append(dict(name=r['name'], offset=off, length=ln, + data=patched[off:off + ln], changed=stock[off:off + ln] != patched[off:off + ln])) + +emitted = [r for r in records if r['changed']] +dropped = [r for r in records if not r['changed']] + + +def write_pack(dirname, source): + d = os.path.join(OUT, dirname) + os.makedirs(d, exist_ok=True) + for f in os.listdir(d): + os.remove(os.path.join(d, f)) + rows = [] + for i, r in enumerate(emitted): + fn = f'{i:03d}_{r["name"]}.bin' + open(os.path.join(d, fn), 'wb').write(source(r)) + assert r['offset'] % 4 == 0, r['name'] # 偏移恒定 4 对齐(实际是 512) + rows.append(dict(name=r['name'], file=fn, offset=r['offset'], length=r['length'])) + with open(os.path.join(d, 'payload.lua'), 'w', encoding='utf-8', newline='\n') as f: + f.write('-- generated by _tools/make_dd_payload.py -- do not hand-edit\n') + f.write('-- dd: -if / -of /dev/app bs=4 seek=offset/4 count=length/4\n') + f.write('return {\n') + for r in rows: + f.write(f' {{ name = "{r["name"]}", file = "{r["file"]}", ' + f'offset = {r["offset"]}, length = {r["length"]} }},\n') + f.write('}\n') + return rows + + +lua_rows = write_pack('payload', lambda r: r['data']) +r_rows = write_pack('payload_rollback', + lambda r: stock[r['offset']:r['offset'] + r['length']]) + +# ---------------------------------------------------------------- 合并成「单个条目」 +# 关键:.face 里**每个文件 = 一个资源条目**。逐条打包会做出 480 个条目, +# 把表盘条目数顶到宿主吃不下的量(实测 6.59MB/170 条能跑,9.12MB/490 条黑屏; +# 原厂 resource.bin 到 9.43MB 都能跑 -> 卡的是条目数,不是体积)。 +# 所以给编译用的是一份「魔数 + 所有负载顺序拼接」的单文件,偏移在构建时按相对偏移回填。 +MAGIC_PAY = b'\x00S4E-PAYv1\x00\xa5\x5a\xc3\x3c' # 15 B,必须全局唯一 +MAGIC_RB = b'\x00S4E-RBKv1\x00\x5a\xa5\x3c\xc3' +BLOB = os.path.join(OUT, 'blob') +os.makedirs(BLOB, exist_ok=True) +for f in os.listdir(BLOB): + os.remove(os.path.join(BLOB, f)) + +pay_blob = MAGIC_PAY + b''.join(r['data'] for r in emitted) +rb_blob = MAGIC_RB + b''.join(stock[r['offset']:r['offset'] + r['length']] for r in emitted) +open(os.path.join(BLOB, 'payload.bin'), 'wb').write(pay_blob) +open(os.path.join(BLOB, 'rollback.bin'), 'wb').write(rb_blob) + +rel, off = [], 0 +for r in emitted: + rel.append(dict(name=r['name'], rel=off, target=r['offset'], length=r['length'])) + off += r['length'] +json.dump(dict(magic_len=len(MAGIC_PAY), magic_rb_len=len(MAGIC_RB), + records=rel, pay_bytes=len(pay_blob), rb_bytes=len(rb_blob)), + open(os.path.join(BLOB, 'offsets.json'), 'w', encoding='utf-8'), indent=1) +print('blob/ 单条目打包: payload.bin %d B + rollback.bin %d B(%d 条记录,相对偏移表已写)' + % (len(pay_blob), len(rb_blob), len(rel))) + +json.dump(dict(device='Xiaomi Watch S4 eSIM (o62lte)', target='/dev/app', + dd="dd if=/ of=/dev/app bs=4 seek= count=", + block_size=4, records=lua_rows, + dropped_identical=[r['name'] for r in dropped]), + open(os.path.join(OUT, 'payload.json'), 'w'), indent=1, ensure_ascii=False) + + +def script(fname, rows, header, base, verify, tail): + with open(os.path.join(OUT, fname), 'w', encoding='utf-8', newline='\n') as f: + f.write('#!/bin/sh\n') + for line in header: + f.write('# ' + line + '\n') + f.write('\nDEV=/dev/app\nBASE=' + base + '\n\n') + f.write('echo "target check"\nls -l $DEV\n\n') + for r in rows: + # RLE 素材的槽位长度常常不是 4 的倍数 -> 那条退化成 bs=1(偏移本身就对齐) + b = 4 if (r['offset'] % 4 == 0 and r['length'] % 4 == 0) else 1 + f.write('echo "== ' + r['name'] + ' ==\n') + f.write(f'dd if=$BASE/{r["file"]} of=$DEV bs={b} seek={r["offset"]//b} ' + f'count={r["length"]//b}\n') + if verify: + f.write(f'dd if=$DEV of=/tmp/chk.bin bs={b} skip={r["offset"]//b} ' + f'count={r["length"]//b}\n') + f.write(f'cmp /tmp/chk.bin $BASE/{r["file"]}\n\n') + f.write(tail + '\n') + + +script('flash.sh', lua_rows, + ['S5 -> S4 icon replacement, run on-device in nsh (NuttX shell).', + '把 flash.sh 与 payload/ 放到同一个目录,例如 /data/s5icons/', + '然后: sh /data/s5icons/flash.sh 最后: reboot', + 'nsh 不支持 $( ) / && / || / 通配符,所以这里是线性展开的。'], + '/data/s5icons/payload', True, + 'rm /tmp/chk.bin\necho "all done - run reboot to apply"') + +script('rollback.sh', r_rows, + ['写回 S4 原厂图标'], '/data/s5icons/payload_rollback', False, + 'echo "rollback done - run reboot to apply"') + +total = sum(r['length'] for r in lua_rows) +print(f'emitted {len(lua_rows)} records, {total} bytes ({total/1048576:.2f} MB)') +print(f'dropped {len(dropped)} byte-identical record(s): {[r["name"] for r in dropped]}') +for r in lua_rows[:3]: + print(f' {r["name"]:<22} off=0x{r["offset"]:08x} len={r["length"]:<7} seek={r["offset"]//4} count={r["length"]//4}') +print(' ...') diff --git a/s4e-icons/tools/make_overlays.py b/s4e-icons/tools/make_overlays.py new file mode 100644 index 0000000..fcee2f3 --- /dev/null +++ b/s4e-icons/tools/make_overlays.py @@ -0,0 +1,77 @@ +"""为官方 LuaDevTemplate 生成可编译的 overlay —— 本项目只有 S5Icons 一个表盘。""" +import os, json, shutil +from PIL import Image, ImageFont +from paths import PROJ, WORK + +OUT = os.path.join(WORK, 'overlay') +ASSETS = os.path.join(PROJ, 'assets') + +DEVICE_TYPE = "362" # 与官方模板一致(362 = S4 家族) +ENTRY_WIDGET = "app_lua%2Fmain.lua" +PREVIEW_SIZE = 326 # Compiler.exe 对 362 设备强制 326x326 +SCREEN_SIZE = 466 +WF_ID = "491552801" +FONTS = [r'C:\Windows\Fonts\msyhbd.ttc', r'C:\Windows\Fonts\msyh.ttc', + r'C:\Windows\Fonts\arialbd.ttf', r'C:\Windows\Fonts\arial.ttf'] + + +def font(size): + for p in FONTS: + try: + return ImageFont.truetype(p, size) + except OSError: + continue + return ImageFont.load_default() + + +def make_preview(path): + """表盘选择页的缩略图:直接用 assets/cover.png 缩到 326x326。""" + Image.open(os.path.join(ASSETS, 'cover.png')).convert('RGBA') \ + .resize((PREVIEW_SIZE, PREVIEW_SIZE), Image.LANCZOS).save(path) + + +def write_fprj(path, title): + xml = ('\r\n' + f'\r\n' + f' \r\n' + f' \r\n' + ' \r\n\r\n') + open(path, 'w', encoding='utf-16', newline='').write(xml) + + +def build(name): + base = os.path.join(OUT, name) + if os.path.exists(base): + shutil.rmtree(base) + fprj = os.path.join(base, 'watchface', 'fprj') + os.makedirs(os.path.join(fprj, 'images'), exist_ok=True) + os.makedirs(os.path.join(fprj, 'app', 'lua'), exist_ok=True) + + cfg = {"projectName": name, "watchfaceId": WF_ID, "power_consumption": "3", + "resourceBin": {"lvglVersion": 9, "colorFormat": "I8", "compress": "NONE", + "input": "watchface/fprj/images/preview.png", "name": "preview"}} + json.dump(cfg, open(os.path.join(base, 'watchface.config.json'), 'w', encoding='utf-8'), + indent=4, ensure_ascii=False) + write_fprj(os.path.join(fprj, f'{name}.fprj'), name) + make_preview(os.path.join(fprj, 'images', 'preview.png')) + shutil.copyfile(os.path.join(PROJ, 'main.lua'), + os.path.join(fprj, 'app', 'lua', 'main.lua')) + # 只放「合并后的单条目」(blob/) —— .face 里每个文件 = 一个资源条目, + # 逐条放会把条目数顶到宿主吃不下的量(见 make_dd_payload.py 注释)。 + blob = os.path.join(PROJ, 'blob') + if not os.path.isdir(blob): + raise SystemExit('缺少 blob/ —— 先跑 tools/make_dd_payload.py') + for src, rel in ((blob, 'pay'), (os.path.join(PROJ, 'ui_images'), 'ui')): + dst = os.path.join(fprj, 'app', 'images', rel) + if os.path.exists(dst): + shutil.rmtree(dst) + shutil.copytree(src, dst) + # 逐条负载与回滚仍然保留在 payload/ 与 payload_rollback/(flash.sh、校验脚本用), + # 但**不进** .face。 + return base + + +if __name__ == '__main__': + build('S5Icons') + print('overlay written to', os.path.join(OUT, 'S5Icons')) diff --git a/s4e-icons/tools/make_s5_to_s4_inset.py b/s4e-icons/tools/make_s5_to_s4_inset.py new file mode 100644 index 0000000..e4fb570 --- /dev/null +++ b/s4e-icons/tools/make_s5_to_s4_inset.py @@ -0,0 +1,898 @@ +"""S5 -> S4 launcher icons, but matching the STOCK S4 sizing convention. + +Measured difference (why icons looked bigger): + stock S4 art : 136x136 inside the 144x144 canvas (outer 4px fully transparent) + S5 art : 144x144 full-bleed (outer ring painted) + +So for every replaced slot whose source art is full-bleed we scale it to +136x136 and centre it in the same 144x144 canvas, then re-encode into the S4 +slot's exact colour format. Every emitted blob keeps the slot's byte length, so +ROMFS absolute offsets stay valid (a length change would corrupt the image). + +Slots whose S5 counterpart is already inset (== stock) are still copied +verbatim, so identical icons stay bit-exact. +""" +import os, sys, json, shutil, hashlib, struct +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from romfs import Romfs, img_header +from lvgl_bin import decode_lvgl_bin +import iconpack as ip +from PIL import Image, ImageFilter + +from paths import PROJ, INPUTS, WORK +S4 = os.path.join(INPUTS, 'target', 'vela_app.bin') +S5_IMAGES = [os.path.join(INPUTS, 'source', 'vela_app.bin'), + os.path.join(INPUTS, 'source', 'vela_health.bin')] +OUTDIR = os.path.join(WORK, 's4_from_s5') +PATCHED = os.path.join(OUTDIR, 'vela_app_s5icons.bin') +MARGIN = 4 + +# 出「分组表盘」用:ICON_SKIP_WALKIE=1 -> 只出图标(体积回到 6.58MB 那档) +# ICON_ONLY_WALKIE=1 -> 只出对讲机素材(小表盘) +# 对讲机素材:**默认不打包**(用户 2026-10-02 决定不改)。ICON_WALKIE=1 可打开。 +DO_WALKIE = os.environ.get('ICON_WALKIE') == '1' +SKIP_WALKIE = (os.environ.get('ICON_SKIP_WALKIE') == '1') or not DO_WALKIE +ONLY_WALKIE = os.environ.get('ICON_ONLY_WALKIE') == '1' +if not DO_WALKIE: + print('!! 对讲机素材默认不打包(ICON_WALKIE=1 可打开)') +if SKIP_WALKIE: + print('!! ICON_SKIP_WALKIE=1:本次不打包对讲机素材') +if ONLY_WALKIE: + print('!! ICON_ONLY_WALKIE=1:本次只打包对讲机素材') + +# Slots the user hand-replaced with their own PNGs. These win over the S5 +# firmware source and always go through normalization (crop to the painted area, +# scale to the slot's own painted size, centre) so the emitted blob keeps the +# slot's exact byte length: +# activities / sports_course / interconnect : 112x112 self-made art +# voice_aivs : 144x144 "小爱同学" user PNG +OVERRIDE = {k: os.path.join(PROJ, 'assets', 'override', k + '.png') + for k in ('activities', 'sports_course', 'interconnect', 'voice_aivs')} + +os.makedirs(OUTDIR, exist_ok=True) +if os.path.exists(PATCHED): + bak = os.path.join(OUTDIR, 'vela_app_s5icons_fullbleed_backup.bin') + if not os.path.exists(bak): + shutil.copyfile(PATCHED, bak) + print(f'backed up previous (full-bleed) image -> {os.path.basename(bak)}') + + +def ink_bbox(img, thr=16): + """bounding box of painted pixels (alpha > thr), or None""" + a = img.convert('RGBA').getchannel('A') + w, h = a.size + px = a.load() + x0, y0, x1, y1 = w, h, -1, -1 + for y in range(h): + for x in range(w): + if px[x, y] > thr: + if x < x0: x0 = x + if y < y0: y0 = y + if x > x1: x1 = x + if y > y1: y1 = y + if x1 < 0: + return None + return (x0, y0, x1, y1) + + +def normalize(img, target, canvas_wh): + """Scale the painted area so its larger side becomes `target`, keeping the + aspect ratio, and centre it on a transparent canvas of the SLOT size. + + canvas_wh is the slot's own w,h -- the source may be any size (the S5 art is + 144x144, the Band 11 art the user dropped in is 112x112), so the output + canvas must not be taken from the source. + """ + cw, ch = canvas_wh + bb = ink_bbox(img) + if bb is None: + return Image.new('RGBA', (cw, ch), (0, 0, 0, 0)) + x0, y0, x1, y1 = bb + bw, bh = x1 - x0 + 1, y1 - y0 + 1 + s = float(target) / float(max(bw, bh)) + if abs(s - 1.0) < 1e-6 and (bw, bh) == (cw, ch): + return img.convert('RGBA') + art = img.convert('RGBA').crop((x0, y0, x1 + 1, y1 + 1)) + nw, nh = max(1, int(round(bw * s))), max(1, int(round(bh * s))) + art = art.resize((nw, nh), Image.LANCZOS) + if s > 1.05: + # upsampling always softens; give the edges some bite back + art = art.filter(ImageFilter.UnsharpMask(radius=2, percent=70, threshold=3)) + canvas = Image.new('RGBA', (cw, ch), (0, 0, 0, 0)) + canvas.paste(art, ((cw - nw) // 2, (ch - nh) // 2)) + return canvas + + +s4 = Romfs(S4) +s4t = ip.targets(s4) + +s5 = {} +S5ROM = [] +for path in S5_IMAGES: + r = Romfs(path) + S5ROM.append(r) + tag = r.volume + for key, ino in ip.targets(r).items(): + if key in s5: + continue + blob = r.read(ino) + _, img = decode_lvgl_bin(blob) + hh = img_header(blob) + # an I8 source ships its own 256-entry palette: reuse it verbatim + pal = blob[12:12 + 1024] if (hh and hh['cf'] == 0x0a and len(blob) >= 1036) else None + s5[key] = (blob, img, tag, ino.size, pal) + +print(f'S4 slots: {len(s4t)} S5 icons: {len(s5)}\n') + +plan, skipped, failed, new_blobs, extra_ino = [], [], [], {}, {} +for key, ino in sorted(s4t.items()): + if ONLY_WALKIE: + break + if key not in s5: + skipped.append((key, 'no S5 counterpart')) + continue + h4 = img_header(s4.read(ino)) + blob5, img5, tag5, size5, pal5 = s5[key] + slot_len = ino.size + how = None + + # a hand-replaced PNG wins over the firmware source + ov = OVERRIDE.get(key) + if ov and os.path.exists(ov): + img5 = Image.open(ov).convert('RGBA') + blob5 = b'' # force re-encode, never verbatim copy + pal5 = None # the S5 palette does not describe THIS art + tag5 = 'user-png %dx%d' % img5.size + + # what sizing does THIS slot use in the stock image? + _, stock_img = decode_lvgl_bin(s4.read(ino)) + sb = ink_bbox(stock_img) if stock_img is not None else None + target = max(sb[2] - sb[0] + 1, sb[3] - sb[1] + 1) if sb else 136 + + nb = ink_bbox(img5) if img5 is not None else None + need = nb is not None and (nb[2] - nb[0] + 1, nb[3] - nb[1] + 1) != (target, target) + if need: + src = normalize(img5, target, (h4["w"], h4["h"])) + if src.size != (h4['w'], h4['h']): + src = src.resize((h4['w'], h4['h']), Image.LANCZOS) + try: + out = ip.encode(src, h4['cf'], h4['w'], h4['h'], h4['flags'], pad_to=slot_len, palette=pal5) + except ValueError: + out = None # slot is RLE-compressed: an uncompressed encode + # cannot fit, so fall through to the copy path + if out is not None and len(out) == slot_len: + how = 'normalize->%d' % target + else: + out = None + if how is None: + if len(blob5) == slot_len: + out, how = blob5, 'copy' + elif img5 is None: + failed.append((key, 'S5 source not decodable')) + continue + else: + src = img5 + if src.size != (h4['w'], h4['h']): + src = src.resize((h4['w'], h4['h']), Image.LANCZOS) + out = ip.encode(src, h4['cf'], h4['w'], h4['h'], h4['flags'], palette=pal5) + if len(out) != slot_len: + failed.append((key, f'encoded {len(out)} != slot {slot_len}')) + continue + how = 're-encode' + + plan.append(dict(name=key, offset=ino.data_off, length=slot_len, + s4_format=ip.FMT.get(h4['cf'], f"0x{h4['cf']:02x}"), + s4_flags=h4['flags'], s5_bytes=len(blob5), s5_from=tag5, + method=how)) + new_blobs[key] = out + +# ---------------------------------------------------------------- 控制中心磁贴(q63 -> S4 eSIM) +# S4 eSIM / S5 eSIM 的控制中心是「蓝色主题」(选中 = 实心蓝盘,未选中 = 深色盘); +# q63(S5 41mm / OS4)是「浅色主题」(选中 = 白盘 + 蓝字形)。整批一起搬才协调。 +CC_DIR = 'control_center/icon/' +# 除了整目录的控制中心磁贴,再点名搬这几张系统 UI 素材(和 S5 eSIM 那份一致)。 +# 健康分区的两条(notification_stand / notification_vigor)本来就和 q63 逐字节相同, +# 而且 S4 这边只解出了 app 分区,所以列在这里也会被跳过。 +SYS_PATHS = [ + 'system/icon/lock_icon.bin', + 'common/icon/system_lock.bin', + 'common/icon/lock.bin', + 'system/icon/unlock.bin', + 'common/icon/quiet_mode.bin', + 'common/icon/unread_msg.bin', + 'notifications/default.bin', + 'notifications/default_small.bin', + 'notifications/no_message.bin', + 'settings/icon/notify.bin', + 'settings/disturb/confirm.bin', + 'esimsms/icon/esim_sms_reminder.bin', + 'esimsms/icon/sms_no_read.bin', + 'esimsms/icon/sms_reminder_watch.bin', + 'phone/icon/send_sms_new.bin', + 'mijia/enable_donot_disturb.bin', + 'mijia/disable_donot_disturb.bin', + # 压力 App 的小表情(S4 没有 health 分区,这些都在 /dev/app;q63 那份在它的 health 里) + 'pressure/icon/presure_relax.bin', + 'pressure/icon/presure_mild.bin', + 'pressure/icon/presure_mid.bin', + 'pressure/icon/presure_severe.bin', + 'pressure/icon/presure_relax32.bin', + 'pressure/icon/presure_mild32.bin', + 'pressure/icon/presure_mid32.bin', + 'pressure/icon/presure_severe32.bin', + 'pressure/icon/presure_relax96.bin', + 'pressure/icon/presure_mild96.bin', + 'pressure/icon/presure_mid96.bin', + 'pressure/icon/presure_severe96.bin', + 'pressure/icon/presure_icon.bin', + 'pressure/icon/press_grade1.bin', + 'pressure/icon/press_grade2.bin', + 'pressure/icon/press_grade3.bin', + 'pressure/icon/press_grade4.bin', + 'pressure/icon/pressure_eomji_bg.bin', + 'pressure/icon/pressure_eomji_widbg.bin', +] +s4_all = {p: i for p, i in s4.entries() if i.type != 1} +s5_all = {} +for _r in S5ROM: + for _p, _i in _r.entries(): + if _i.type != 1 and _p not in s5_all: + s5_all[_p] = (_r, _i) + +print('\n--- 控制中心磁贴 + 系统 UI 素材(q63 -> S4 eSIM)---') +cc_paths = set() +_all_paths = sorted(p for p in s4_all if p.startswith(CC_DIR) and p.endswith('.bin')) +for _sp in SYS_PATHS: + if _sp in s4_all and _sp not in _all_paths: + _all_paths.append(_sp) +if ONLY_WALKIE: + _all_paths = [] +for path in _all_paths: + tino = s4_all[path] + got = s5_all.get(path) + if got is None: + skipped.append((path, 'q63 没有这个文件')); continue + qrom, oino = got + tgt_blob = s4.read(tino) + src_blob = qrom.read(oino) + if src_blob == tgt_blob: + skipped.append((path, '两边字节完全相同')); continue + th = img_header(tgt_blob) if len(tgt_blob) >= 12 else None + sh = img_header(src_blob) if len(src_blob) >= 12 else None + if len(src_blob) == len(tgt_blob): + new, how = src_blob, 'copy(等长)' + elif th and sh and th['cf'] == sh['cf']: + timg = decode_lvgl_bin(tgt_blob)[1] + simg = decode_lvgl_bin(src_blob)[1] + if timg is None or simg is None: + skipped.append((path, '无法解码(cf=0x%02x)' % (th['cf'] if th else 0))); continue + sb = ink_bbox(timg) + nb = ink_bbox(simg) + if sb and nb: + tw, thh = sb[2] - sb[0] + 1, sb[3] - sb[1] + 1 + art = simg.convert('RGBA').crop((nb[0], nb[1], nb[2] + 1, nb[3] + 1)) + if art.size != (tw, thh): + up = tw > art.size[0] or thh > art.size[1] + art = art.resize((tw, thh), Image.LANCZOS) + if up: + art = art.filter(ImageFilter.UnsharpMask(radius=2, percent=70, threshold=3)) + canvas = Image.new('RGBA', (th['w'], th['h']), (0, 0, 0, 0)) + canvas.paste(art, (sb[0], sb[1])) + out = canvas + else: + out = simg + pal = src_blob[12:12 + 1024] if sh['cf'] == ip.CF_I8 else None + new = ip.encode(out, th['cf'], th['w'], th['h'], th['flags'], palette=pal) + how = 'rescale %dx%d->%dx%d' % (sh['w'], sh['h'], th['w'], th['h']) + else: + skipped.append((path, '长度/格式不支持')); continue + if len(new) != len(tgt_blob): + failed.append((path, '编码 %d != 槽位 %d' % (len(new), len(tgt_blob)))); continue + is_cc = path.startswith(CC_DIR) + name = ('cc_' if is_cc else 'sys_') + path.split('/')[-1][:-4] + new_blobs[name] = new + extra_ino[name] = tino + cc_paths.add(path) + plan.append(dict(name=name, kind=('cc' if is_cc else 'sysicon'), path=path, offset=tino.data_off, length=len(tgt_blob), + s4_format=ip.FMT.get(th['cf'], '0x%02x' % th['cf']) if th else '?', + s4_flags=th['flags'] if th else 0, s5_bytes=len(src_blob), + s5_from='q63 %s' % os.path.basename(qrom.path), method=how)) + print(' %-46s %-22s %s' % (path, how, '')) + + +# ------------------------------------------------- 对讲机素材(用户提供的整套 PNG) +# walkie_talkie/ 下的槽位几乎**全是 I8+RLE 压缩**的:直接编码长度会变(ROMFS 偏移全平移), +# 所以走 ip.encode_into_slot():量化 -> RLE -> 用 ctrl=0 空指令补齐到原 csize, +# 保证与原槽位逐字节等长。 +WALKIE_SRC = os.path.join(PROJ, 'assets', 'walkie_talkie') +walk_paths = set() +mijia_paths = set() +print('\n--- 对讲机素材 walkie_talkie(用户 PNG)-> S4 eSIM ---') +walkie_n = 0 +if SKIP_WALKIE or not os.path.isdir(WALKIE_SRC): + print(' 跳过这一组(ICON_SKIP_WALKIE=%s / 目录存在=%s)' + % (SKIP_WALKIE, os.path.isdir(WALKIE_SRC))) +else: + _rels = [] + for _root, _, _files in os.walk(WALKIE_SRC): + for _f in _files: + if _f.lower().endswith('.png'): + _rels.append(os.path.relpath(os.path.join(_root, _f), WALKIE_SRC).replace('\\', '/')) + for rel in sorted(_rels): + path = 'walkie_talkie/' + rel[:-4] + '.bin' + tino = s4_all.get(path) + if tino is None: + skipped.append((path, '目标固件没有这个槽位')); continue + tgt_blob = s4.read(tino) + img = Image.open(os.path.join(WALKIE_SRC, rel)) + new, how = ip.encode_into_slot(img, tgt_blob, tino.size) + if new is None: + skipped.append((path, how)); continue + if new == tgt_blob: + skipped.append((path, '与现有一致')); continue + th = img_header(tgt_blob) + name = 'walk_' + rel[:-4].replace('/', '_') + new_blobs[name] = new + extra_ino[name] = tino + walk_paths.add(path) + plan.append(dict(name=name, kind='walkie', path=path, offset=tino.data_off, + length=tino.size, + s4_format=ip.FMT.get(th['cf'], '0x%02x' % th['cf']) if th else '?', + s4_flags=th['flags'] if th else 0, s5_bytes=0, + s5_from='用户 PNG assets/walkie_talkie/' + rel, method=how)) + walkie_n += 1 + if walkie_n <= 6 or walkie_n % 40 == 0: + print(' %-52s %-18s %d B' % (path, how, tino.size)) + print(' 对讲机素材写入 %d 条' % walkie_n) + +# ------------------------------------------------- 控制中心顶部状态区(q63 -> S4 eSIM) +# q63 的顶栏素材在 control_center/bar/:wifi 4 档 / 蜂窝信号 5 档 / 3G·4G 文字 / +# 蓝牙连接·断开 / 飞行模式 / 定位 / 勿扰 / 静音。本机同名槽位在 control_center/icon/(48x48 I8)。 +# 尺寸不同的(q63 那几张 36x36)按**实心范围**归一化:把 q63 的实心图缩放到本机槽位的实心范围、 +# 贴在同样的原点 —— 位置与视觉重量和本机原有图标一致。 +CC_TOP = [ + ('control_center/icon/network_4g.bin', 'control_center/bar/network_4g.bin', '4G 文字'), + ('control_center/icon/network_3g.bin', 'control_center/bar/network_3g.bin', '3G 文字'), + ('control_center/icon/signal_strength.bin', 'control_center/bar/signal_strength0.bin', '蜂窝信号 0'), + ('control_center/icon/signal_strength1.bin', 'control_center/bar/signal_strength1.bin', '蜂窝信号 1'), + ('control_center/icon/signal_strength2.bin', 'control_center/bar/signal_strength2.bin', '蜂窝信号 2'), + ('control_center/icon/signal_strength3.bin', 'control_center/bar/signal_strength3.bin', '蜂窝信号 3'), + ('control_center/icon/signal_strength4.bin', 'control_center/bar/signal_strength4.bin', '蜂窝信号 4'), + ('control_center/icon/air_mode_conn.bin', 'control_center/bar/air.bin', '飞行模式'), + ('control_center/icon/bt_connect.bin', 'control_center/bar/connect.bin', '蓝牙已连接手机'), + ('control_center/icon/bt_disconnect.bin', 'control_center/bar/disconnect.bin', '蓝牙未连接'), + ('control_center/icon/location.bin', 'control_center/bar/local.bin', '定位'), + ('control_center/icon/wifi_level_0.bin', 'control_center/bar/wifi_level_0.bin', 'WiFi 0'), + ('control_center/icon/wifi_level_1.bin', 'control_center/bar/wifi_level_1.bin', 'WiFi 1'), + ('control_center/icon/wifi_level_2.bin', 'control_center/bar/wifi_level_2.bin', 'WiFi 2'), + ('control_center/icon/wifi_level_3.bin', 'control_center/bar/wifi_level_3.bin', 'WiFi 3'), +] +# 本机把「3G/4G 文字 + 信号格」合成在一张图里,而 q63 是分开的两张 —— 按本机原有的两段实心区域 +# 分别填:左边文字段用 q63 的 3G/4G,右边信号段用 q63 的 signal_strengthN。 +CC_COMPOSED = [] +for _net in ('3g', '4g'): + for _n in range(1, 5): + CC_COMPOSED.append(('control_center/icon/network_%s_signal_%d.bin' % (_net, _n), + 'control_center/bar/network_%s.bin' % _net, + 'control_center/bar/signal_strength%d.bin' % _n)) + CC_COMPOSED.append(('control_center/icon/network_%s_no_signal.bin' % _net, + 'control_center/bar/network_%s.bin' % _net, + 'control_center/bar/signal_strength0.bin')) +# 手机电量(本机 phone/icon/battery_level_*):q63 同名,只是高度不同 +for _n in range(0, 11): + CC_TOP.append(('phone/icon/battery_level_%d.bin' % _n, + 'phone/icon/battery_level_%d.bin' % _n, '手机电量 %d' % _n)) + + +def _split_two(img): + """把「文字 + 信号格」这类合成图按空列切成左右两段,返回两段 bbox(失败返回 None)。""" + a = img.convert('RGBA').getchannel('A') + w, h = a.size + px = a.load() + cols = [any(px[x, y] > 16 for y in range(h)) for x in range(w)] + gaps, run = [], None + for x, on in enumerate(cols): + if not on: + run = x if run is None else run + else: + # 只认「两侧都有实心」的空隙:开头的空白段(0..n)不是切分点 + if run is not None and run > 0 and x - run >= 1: + gaps.append((run, x - 1)) + run = None + if not gaps: + return None + # 取**最左边**那个「两侧都有实心」的空隙 = 文字与信号格之间的分界 + # (取最宽的会落在信号格之间,把格数挤进一条缝里) + g0, g1 = gaps[0] + left = [x for x in range(0, g0) if cols[x]] + right = [x for x in range(g1 + 1, w) if cols[x]] + if not left or not right: + return None + + def bbox(xs): + ys = [y for y in range(h) for x in xs if px[x, y] > 16] + return (min(xs), min(ys), max(xs), max(ys)) + return bbox(left), bbox(right) + + +def normalize_to_bbox(src_img, target_bb, canvas_wh, fit=False): + """把 src 的**实心区域**放进 target_bb。 + + fit=False:拉伸填满 target_bb(槽位画法本来就一致时用) + fit=True :**等比**缩放到能放进 target_bb 的最大尺寸并居中(不拉伸 —— 合成图里 + 「信号格」这类会因横纵比例不同被压扁,必须走这条) + """ + cw, ch = canvas_wh + if target_bb is None: + return Image.new('RGBA', (cw, ch), (0, 0, 0, 0)) + sb = ink_bbox(src_img) + if sb is None: + return Image.new('RGBA', (cw, ch), (0, 0, 0, 0)) + tw, thh = target_bb[2] - target_bb[0] + 1, target_bb[3] - target_bb[1] + 1 + art = src_img.convert('RGBA').crop((sb[0], sb[1], sb[2] + 1, sb[3] + 1)) + if fit: + s = min(tw / art.size[0], thh / art.size[1]) + nw, nh = max(1, round(art.size[0] * s)), max(1, round(art.size[1] * s)) + if (nw, nh) != art.size: + up = s > 1.0 + art = art.resize((nw, nh), Image.LANCZOS) + if up: + art = art.filter(ImageFilter.UnsharpMask(radius=2, percent=70, threshold=3)) + pos = (target_bb[0] + (tw - nw) // 2, target_bb[1] + (thh - nh) // 2) + else: + if art.size != (tw, thh): + up = tw > art.size[0] or thh > art.size[1] + art = art.resize((tw, thh), Image.LANCZOS) + if up: + art = art.filter(ImageFilter.UnsharpMask(radius=2, percent=70, threshold=3)) + pos = (target_bb[0], target_bb[1]) + canvas = Image.new('RGBA', (cw, ch), (0, 0, 0, 0)) + canvas.paste(art, pos) + return canvas + + +print('\n--- 控制中心顶部状态区(q63 control_center/bar -> S4 eSIM)---') +cc_top_n = 0 +for tpath, spath, note in CC_TOP: + tino = s4_all.get(tpath) + got = s5_all.get(spath) + if tino is None: + skipped.append((tpath, '本机没有这个槽位')); continue + if got is None: + skipped.append((spath, 'q63 没有这个素材')); continue + qrom, oino = got + tgt_blob = s4.read(tino) + src_blob = qrom.read(oino) + th = img_header(tgt_blob) + timg = decode_lvgl_bin(tgt_blob)[1] + simg = decode_lvgl_bin(src_blob)[1] + if timg is None or simg is None: + skipped.append((tpath, '解码失败')); continue + sb = ink_bbox(timg) + out = normalize_to_bbox(simg, sb, (th['w'], th['h'])) + pal = src_blob[12:12 + 1024] if img_header(src_blob)['cf'] == ip.CF_I8 else None + new = ip.encode(out, th['cf'], th['w'], th['h'], th['flags'], palette=pal) + if len(new) != len(tgt_blob): + failed.append((tpath, '编码 %d != 槽位 %d' % (len(new), len(tgt_blob)))); continue + if new == tgt_blob: + skipped.append((tpath, '两边已经一致')); continue + # 与 sys 组用同名 -> 同一槽位只保留一条记录(后者覆盖前者) + name = ('cc_' if tpath.startswith(CC_DIR) else 'sys_') + tpath.split('/')[-1][:-4] + new_blobs[name] = new + extra_ino[name] = tino + cc_paths.add(tpath) + plan.append(dict(name=name, kind='cctop', path=tpath, offset=tino.data_off, length=tino.size, + s4_format=ip.FMT.get(th['cf'], '0x%02x' % th['cf']), s4_flags=th['flags'], + s5_bytes=len(src_blob), s5_from='q63 %s' % spath, method='归一化(%s)' % note)) + cc_top_n += 1 + print(' %-46s <- %-40s %s' % (tpath, spath, note)) +print(' 顶栏状态区写入 %d 条' % cc_top_n) + +print('\n--- 控制中心「3G/4G+信号格」合成图(S4e 的 network_* + S5e 的 signal_strength)---') +# 合成来源按要求改:文字取 **本机(S4e)** 的 network_3g/4g,信号格取 **S5e** 的 signal_strength* +# (S5e 表盘里的当前版本,若它已被移植过就是新的蓝色版;没有就用它的原厂版) +S5E_IMG = os.path.join(PROJ, '..', 's5e-icons', '.work', 's5e_from_os4', 'vela_app_os4.bin') +_s5e = Romfs(S5E_IMG) if os.path.exists(S5E_IMG) else None +_s5e_tab = {q: i for q, i in _s5e.entries()} if _s5e else {} +print(' S5e 信号格来源: %s' % ('S5e 表盘镜像' if _s5e else '(缺,回退 q63)')) +cc_comp_n = 0 +for tpath, tpath_text, tpath_bars in CC_COMPOSED: + tino = s4_all.get(tpath) + gt = s5_all.get(tpath_text) + gb = s5_all.get(tpath_bars) + if tino is None or gt is None or gb is None: + skipped.append((tpath, '缺槽位或 q63 缺素材')); continue + tgt_blob = s4.read(tino) + th = img_header(tgt_blob) + timg = decode_lvgl_bin(tgt_blob)[1] + if timg is None: + skipped.append((tpath, '本机图解码失败')); continue + parts = _split_two(timg) + if parts is None: + skipped.append((tpath, '切不开两段(布局不同)')); continue + (tx0, ty0, tx1, ty1), (bx0, by0, bx1, by1) = parts + canvas = Image.new('RGBA', (th['w'], th['h']), (0, 0, 0, 0)) + pal = None + ow = th['w'] - 4 # 用满整张宽度(左右各留 2px) + x0 = 2 + gap = 3 + tw_new = max(10, round(ow * 0.44)) + bw_new = max(10, ow - tw_new - gap) + text_bb = (x0, 0, x0 + tw_new - 1, th['h'] - 1) + bars_x0 = x0 + tw_new + gap + bars_bb = (bars_x0, 0, bars_x0 + bw_new - 1, th['h'] - 1) + + # 文字:优先用**本次刚写好的本机 network_3g/4g**(就是 S4e 上的那张) + tname = ('cc_' if tpath_text.startswith(CC_DIR) else 'sys_') + tpath_text.split('/')[-1][:-4] + text_img = decode_lvgl_bin(new_blobs[tname])[1] if tname in new_blobs else None + if text_img is None: + text_img = decode_lvgl_bin(gt[0].read(gt[1]))[1] + canvas.alpha_composite(normalize_to_bbox(text_img, text_bb, (th['w'], th['h']), fit=True)) + pal = new_blobs[tname][12:12 + 1024] if tname in new_blobs else None + + # 信号格:**S5e** 的同名槽位(signal_strength / signal_strength1..4) + lvl = tpath_bars.split('signal_strength')[-1][:-4] # '', '1'...'4' + s5e_name = 'control_center/icon/signal_strength%s.bin' % lvl + bars_img = None + if _s5e is not None and s5e_name in _s5e_tab: + bars_img = decode_lvgl_bin(_s5e.read(_s5e_tab[s5e_name]))[1] + if bars_img is None: + bars_img = decode_lvgl_bin(gb[0].read(gb[1]))[1] + if bars_img is None: + skipped.append((tpath, '信号格来源解码失败')); continue + canvas.alpha_composite(normalize_to_bbox(bars_img, bars_bb, (th['w'], th['h']), fit=True)) + + new = ip.encode(canvas, th['cf'], th['w'], th['h'], th['flags'], palette=pal) + if len(new) != len(tgt_blob): + failed.append((tpath, '编码 %d != 槽位 %d' % (len(new), len(tgt_blob)))); continue + if new == tgt_blob: + skipped.append((tpath, '两边已经一致')); continue + # 与 sys 组用同名 -> 同一槽位只保留一条记录(后者覆盖前者) + name = ('cc_' if tpath.startswith(CC_DIR) else 'sys_') + tpath.split('/')[-1][:-4] + new_blobs[name] = new + extra_ino[name] = tino + cc_paths.add(tpath) + plan.append(dict(name=name, kind='cctop', path=tpath, offset=tino.data_off, length=tino.size, + s4_format=ip.FMT.get(th['cf'], '0x%02x' % th['cf']), s4_flags=th['flags'], + s5_bytes=0, s5_from='S4e %s + S5e %s' % (tpath_text, s5e_name), + method='合成(文字+信号格)')) + cc_comp_n += 1 + print(' %-46s <- S4e %s + S5e %s' % (tpath, tpath_text.split('/')[-1], s5e_name.split('/')[-1])) +print(' 合成图写入 %d 条' % cc_comp_n) + +# ------------------------------------------------- 米家 / 融合中心(控制中心里的米家设备图标) +# 规则:q63 的 fusion_center/** 与 mijia/** 里,**同名路径**在本机也有的就移植 +# (S5e 还有一份 fusion_center/gauss/**,同名的一并写)。画布尺寸两边常常不同 +# (q63 是 116/64,本机是 96/128),所以按**实心范围**对齐 + 转成本机槽位的格式。 +PLATE_KEEP = ('headset',) # 只有这种「裸图案」槽位保留底版 + 图案归位 +# q63 的「柔光玻璃」底盘(干净的深色玻璃圆盘 + 发丝高光环)—— 用它当底版 +_glass = None +_g = s5_all.get('fusion_center/fg.bin') +if _g is not None: + _glass = decode_lvgl_bin(_g[0].read(_g[1]))[1] + if _glass is not None: + _glass = _glass.convert('RGBA') + print('柔光玻璃盘: q63 fusion_center/fg.bin %dx%d' % _glass.size) +MIJIA_PREFIX = ('fusion_center/', 'mijia/') +# 控制中心的米家/融合中心那组:**默认不打包**(用户 2026-10-02 决定不改)。ICON_MIJIA=1 可打开。 +DO_MIJIA = os.environ.get('ICON_MIJIA') == '1' +def plate_glyph_bbox(img, tol=26, min_plate=0.30): + """目标图标里「底版上的图案」的 bbox。 + + 本机很多设备图标是「深色圆底版 + 亮色图案」;q63 有的素材是**裸图案(没有底版)**, + 这时按实心范围缩放会把图案放大到整个底版 —— 必须改成:保留底版,把裸图案放回 + 原图案的位置/尺寸。做法:底版色 = 出现最多的颜色,其余像素的 bbox 就是原图案。 + """ + rgba = img.convert('RGBA') + w, h = rgba.size + px = rgba.load() + cnt = {} + for y in range(h): + for x in range(w): + p = px[x, y] + if p[3] > 16: + cnt[p] = cnt.get(p, 0) + 1 + if not cnt: + return None + plate, n = max(cnt.items(), key=lambda kv: kv[1]) + if n >= min_plate * w * h: + xs, ys = [], [] + for y in range(h): + for x in range(w): + p = px[x, y] + if p[3] > 16 and (abs(p[0] - plate[0]) + abs(p[1] - plate[1]) + abs(p[2] - plate[2])) > tol: + xs.append(x); ys.append(y) + if xs: + return (min(xs), min(ys), max(xs), max(ys)) + # 兜底:底版是渐变(没有主导纯色)时,用**亮度**找亮图案(深色底版上的白图案) + lum = {} + mx = 0 + for y in range(h): + for x in range(w): + p = px[x, y] + if p[3] > 16: + v = 0.299 * p[0] + 0.587 * p[1] + 0.114 * p[2] + lum[(x, y)] = v + mx = max(mx, v) + if not lum or mx <= 0: + return None + thr = 0.62 * mx + xs = [x for (x, y), v in lum.items() if v >= thr] + ys = [y for (x, y), v in lum.items() if v >= thr] + if not xs: + return None + bb = (min(xs), min(ys), max(xs), max(ys)) + if (bb[2] - bb[0] + 1) * (bb[3] - bb[1] + 1) > 0.92 * w * h: + return None # 亮像素几乎铺满 -> 它本身就是全幅图案,不做归位 + return bb + + +def ink_ratio(img): + """实心范围占画布的比例(两个方向取大者)—— 接近 1.0 说明是"满幅/带底版"的图。""" + bb = ink_bbox(img) + if bb is None: + return 0.0 + w, h = img.size + return max((bb[2] - bb[0] + 1) / w, (bb[3] - bb[1] + 1) / h) + + +def overlay_glyph_in_place(tgt_blob, th, src_img, target_bb): + """在**索引层**把 q63 的裸图案画进本机 I8 图标:底版像素一个比特都不动。 + + 为什么必须这么做:底版是抖动/渐变,重新量化(无论用谁的调色板)都会出噪点或白环。 + 只有直接改索引数组,底版才保持原样;图案像素按最近色映射到**本机自己的调色板** + (本机图标里本来就有白色,所以白色图案能精确命中)。 + """ + w, h, cf = th['w'], th['h'], th['cf'] + if cf != ip.CF_I8 or (th['flags'] & 0x08): + return None + if len(tgt_blob) != 12 + 1024 + w * h: + return None + pal_b = tgt_blob[12:12 + 1024] + pal = [(pal_b[i * 4 + 2], pal_b[i * 4 + 1], pal_b[i * 4 + 0], pal_b[i * 4 + 3]) for i in range(256)] + idx = bytearray(tgt_blob[12 + 1024:12 + 1024 + w * h]) + # 把图案等比放进 target_bb 后的实际位置/尺寸 + nb = ink_bbox(src_img) + if nb is None or target_bb is None: + return None + tw, thh = target_bb[2] - target_bb[0] + 1, target_bb[3] - target_bb[1] + 1 + art = src_img.convert('RGBA').crop((nb[0], nb[1], nb[2] + 1, nb[3] + 1)) + s = min(tw / art.size[0], thh / art.size[1]) + art = art.resize((max(1, round(art.size[0] * s)), max(1, round(art.size[1] * s))), Image.LANCZOS) + ox = target_bb[0] + (tw - art.width) // 2 + oy = target_bb[1] + (thh - art.height) // 2 + cache = {} + for y in range(art.height): + for x in range(art.width): + r, g, b, a = art.getpixel((x, y)) + if a < 24: + continue + key = (r, g, b, a) + ci = cache.get(key) + if ci is None: + best, bd = 0, 1 << 30 + for k in range(256): + pr, pg, pb, pa = pal[k] + d = (pr - r) ** 2 + (pg - g) ** 2 + (pb - b) ** 2 + 2 * (pa - a) ** 2 + if d < bd: + bd, best = d, k + cache[key] = ci = best + ix, iy = ox + x, oy + y + if 0 <= ix < w and 0 <= iy < h: + idx[iy * w + ix] = ci + return tgt_blob[:12 + 1024] + bytes(idx) + + +def plate_share(img, gbb): + """底版像素占实心像素的比例 —— 用来确认这张图**真的有底版**(不是纯图案)。""" + if gbb is None: + return 0.0 + a = img.convert('RGBA').getchannel('A') + w, h = a.size + px = a.load() + ink = tot = 0 + for y in range(h): + for x in range(w): + if px[x, y] > 16: + tot += 1 + if not (gbb[0] <= x <= gbb[2] and gbb[1] <= y <= gbb[3]): + ink += 1 + return (ink / tot) if tot else 0.0 + + +print('\n--- 米家 / 融合中心素材(q63 -> S4 eSIM)%s ---' % ('' if DO_MIJIA else ':已按设置跳过')) +mijia_n = 0 +for _q in sorted(s5_all): + if not DO_MIJIA or not _q.startswith(MIJIA_PREFIX): + continue + if _q.endswith('launcher.bin'): + continue # 应用图标由 launcher 组处理(那边处理 ARGB 画法) + _got = s5_all[_q] + if _got[0] is None: + continue + qrom, oino = _got + cands = [_q] + if _q.startswith('fusion_center/icon/'): + cands.append(_q.replace('fusion_center/icon/', 'fusion_center/gauss/')) + for tpath in cands: + tino = s4_all.get(tpath) + if tino is None: + if tpath == _q: + skipped.append((tpath, '本机没有这个槽位')) + continue + tgt_blob = s4.read(tino) + src_blob = qrom.read(oino) + th, sh = img_header(tgt_blob), img_header(src_blob) + if th is None or sh is None: + skipped.append((tpath, '头部异常')); continue + simg = decode_lvgl_bin(src_blob)[1] + if simg is None: + skipped.append((tpath, 'q63 素材解码失败(cf=0x%02x)' % sh['cf'])); continue + how = None + if (th['flags'] & 0x08) or th['cf'] not in (ip.CF_I8, ip.CF_ARGB8888): + new, how = ip.encode_into_slot(simg, tgt_blob, tino.size) + else: + timg = decode_lvgl_bin(tgt_blob)[1] + if timg is None: + skipped.append((tpath, '本机图解码失败(cf=0x%02x)' % th['cf'])); continue + gbb = plate_glyph_bbox(timg) + # 只对「裸图案」的槽位用这个模式(本机是底版+图案、q63 是裸图案)。 + # 底版直接用 **q63 那张柔光玻璃盘**(fusion_center/fg.bin),而不是本机原来的平底, + # 这样移植过去的图标看着就是 S5 41mm 的玻璃质感。 + if (gbb is not None and ink_ratio(simg) < 0.90 + and plate_share(timg, gbb) >= 0.45 + and tpath.rsplit('/', 1)[-1][:-4] in PLATE_KEEP): + # 底版原样保留(索引层),只把 q63 的裸图案画进去 + new = overlay_glyph_in_place(tgt_blob, th, simg, gbb) + if new is None: + skipped.append((tpath, '索引层合成不可用(非未压缩 I8)')); continue + out, how = None, '底版原样+图案入索引 %dx%d->%dx%d' % (sh['w'], sh['h'], th['w'], th['h']) + else: + out = normalize_to_bbox(simg, ink_bbox(timg), (th['w'], th['h'])) + how = '等比归一化 %dx%d->%dx%d' % (sh['w'], sh['h'], th['w'], th['h']) + pal = src_blob[12:12 + 1024] if sh['cf'] == ip.CF_I8 else None + new = ip.encode(out, th['cf'], th['w'], th['h'], th['flags'], palette=pal) + if new is None or len(new) != tino.size: + failed.append((tpath, '编码失败/长度不符 (%s)' % how)); continue + if new == tgt_blob: + skipped.append((tpath, '两边已经一致')); continue + name = 'sys_' + tpath.replace('/', '_')[:-4] + new_blobs[name] = new + extra_ino[name] = tino + mijia_paths.add(tpath) + plan.append(dict(name=name, kind='mijia', path=tpath, offset=tino.data_off, length=tino.size, + s4_format=ip.FMT.get(th['cf'], '0x%02x' % th['cf']), s4_flags=th['flags'], + s5_bytes=len(src_blob), s5_from='q63 %s' % _q, method=how)) + mijia_n += 1 + if mijia_n <= 8 or mijia_n % 20 == 0: + print(' %-52s <- %-34s %s' % (tpath, _q, how)) +print(' 米家/融合中心写入 %d 条' % mijia_n) + +# ------------------------------------------------- 焦点通知素材(q63 notifications/** -> 本机) +# 同名路径对齐移植;尺寸不同的按实心范围归一化,格式不同自动转换(RLE / ARGB / I8)。 +PORT_PREFIXES = ('notifications/', 'phone/', 'recorder/', 'timer/', 'todolist/') +# 用户指定:这两张保持原厂不换(本机槽位形状差太多,压过去效果不好) +NOTIF_SKIP = ('notifications/dotted_line.bin', 'notifications/track.bin', + # 用户指定:录音机这 3 张不换 + 'recorder/mark_opt.bin', 'recorder/menu.bin', 'recorder/confirm.bin', + # 用户指定:拨号列表这 3 张不换 + 'phone/icon/list_icon_keypad.bin', 'phone/icon/list_icon_call_log.bin', + 'phone/icon/list_icon_contact.bin', + # 用户指定:组件预览底图这 2 张不换 + 'phone/widget/phone_widget.bin', 'phone/widget/pre_phone_widget.bin') +# 应用图标归「launcher 组」管(那边处理 I8↔ARGB 与留边画法),这里跳过避免重复写同一槽位 +PORT_SKIP_TAIL = ('launcher.bin', 'launcher') +print('\n--- 通知/电话/录音机素材(q63 同名移植 -> S4 eSIM)---') +notif_n = 0 +for _q in sorted(s5_all): + if not _q.startswith(PORT_PREFIXES): + continue + if _q in NOTIF_SKIP: + skipped.append((_q, '按约定保持原厂')); continue + if _q.rsplit('/', 1)[-1].endswith('launcher.bin'): + continue + _got = s5_all[_q] + if _got[0] is None: + continue + qrom, oino = _got + tino = s4_all.get(_q) + if tino is None: + skipped.append((_q, '本机没有这个槽位')); continue + tgt_blob = s4.read(tino) + src_blob = qrom.read(oino) + th, sh = img_header(tgt_blob), img_header(src_blob) + if th is None or sh is None: + skipped.append((_q, '头部异常')); continue + simg = decode_lvgl_bin(src_blob)[1] + if simg is None: + skipped.append((_q, 'q63 素材解码失败(cf=0x%02x)' % sh['cf'])); continue + how = None + if (th['flags'] & 0x08) or th['cf'] not in (ip.CF_I8, ip.CF_ARGB8888): + new, how = ip.encode_into_slot(simg, tgt_blob, tino.size) + if new is None: + skipped.append((_q, how)); continue + else: + timg = decode_lvgl_bin(tgt_blob)[1] + if timg is None: + skipped.append((_q, '本机图解码失败(cf=0x%02x)' % th['cf'])); continue + # 一律等比缩放(长宽比不同也不拉伸) + out = normalize_to_bbox(simg, ink_bbox(timg), (th['w'], th['h']), fit=True) + pal = src_blob[12:12 + 1024] if sh['cf'] == ip.CF_I8 else None + new = ip.encode(out, th['cf'], th['w'], th['h'], th['flags'], palette=pal) + how = '等比归一化 %dx%d->%dx%d' % (sh['w'], sh['h'], th['w'], th['h']) + if len(new) != tino.size: + failed.append((_q, '编码 %d != 槽位 %d' % (len(new), tino.size))); continue + if new == tgt_blob: + skipped.append((_q, '两边已经一致')); continue + name = 'sys_' + _q.replace('/', '_')[:-4] + new_blobs[name] = new + extra_ino[name] = tino + mijia_paths.add(_q) + plan.append(dict(name=name, kind='notif', path=_q, offset=tino.data_off, length=tino.size, + s4_format=ip.FMT.get(th['cf'], '0x%02x' % th['cf']), s4_flags=th['flags'], + s5_bytes=len(src_blob), s5_from='q63 %s' % _q, method=how)) + notif_n += 1 + print(' %-46s <- q63 %-30s %s' % (_q, _q.split('/')[-1], how)) +print(' 通知/电话/录音机写入 %d 条' % notif_n) + +n_inset = sum(1 for p in plan if p['method'] == 'inset') +print(f'{"name":<24}{"slot":>8}{"fmt":>10}{"S5 bytes":>10} method') +for p in plan: + print(f'{p["name"]:<24}{p["length"]:>8}{p["s4_format"]:>10}{p["s5_bytes"]:>10} {p["method"]}') +for k, why in skipped: + print(f'{k:<24}{"-":>8}{"-":>10}{"-":>10} SKIP {why}') +for k, why in failed: + print(f'{k:<24}{"-":>8}{"-":>10}{"-":>10} FAIL {why}') +print(f'\nreplace {len(plan)} (inset {n_inset}) skip {len(skipped)} failed {len(failed)}') + +# 同一槽位可能被多个组写过(sys / 顶栏 / 米家)-> 按 **inode** 去重,后写的赢 +_byoff = {} +for _n, _i in extra_ino.items(): + _byoff[_i.off] = (_n, _i) +extra_ino = {_n: _i for _n, _i in _byoff.values()} + +all_items = [(key, ino) for key, ino in sorted(s4t.items()) if key in new_blobs] +all_items += [(name, ino) for name, ino in extra_ino.items()] + +# plan 里同一槽位的旧记录也去掉(只留最后一条) +_dedup = {} +for _p in plan: + _dedup[_p['name']] = _p +plan = list(_dedup.values()) +patch, rollback = bytearray(), bytearray() +for key, ino in all_items: + new, old = new_blobs[key], s4.read(ino) + patch += struct.pack(' {len(b)} inode table identical: {a == b}') +print(f'superblock identical: {s4.sb_size == r2.sb_size and s4.sb_cksum == r2.sb_cksum}') +i1 = {p: i for p, i in s4.entries()} +i2 = {p: i for p, i in r2.entries()} +changed = sorted(p for p in i1 if s4.read(i1[p]) != r2.read(i2[p])) +diff_items = [(k, i) for k, i in all_items if new_blobs[k] != s4.read(i)] +print(f'changed files ({len(changed)}), 预期 {len(diff_items)}') +assert len(changed) == len(diff_items), 'changed %d != expected %d' % (len(changed), len(diff_items)) +assert all(c.rsplit('/', 1)[-1].endswith('launcher.bin') or c in cc_paths or c in walk_paths + or c in mijia_paths for c in changed), changed[:5] +print('sha256', hashlib.sha256(open(PATCHED, "rb").read()).hexdigest()) diff --git a/s4e-icons/tools/make_ui_images.py b/s4e-icons/tools/make_ui_images.py new file mode 100644 index 0000000..bcdcb11 --- /dev/null +++ b/s4e-icons/tools/make_ui_images.py @@ -0,0 +1,58 @@ +"""把 assets/ 里的 UI 素材转成 LVGL v9 二进制图片(.bin),供表盘运行时加载。 + +格式(已在真机验证): + header 12B: magic=0x19 | cf | flags | w | h | stride | reserved + cf=0x10 ARGB8888 -> 数据为 BGRA,长度 = w*h*4 +编码器用 iconpack.encode(与图标包同一套,字节级验证过)。 +""" +import os, sys +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +import iconpack as ip +from PIL import Image, ImageDraw + +from paths import PROJ, DOCS +ASSETS = os.path.join(PROJ, 'assets') +OUT = os.path.join(PROJ, 'ui_images') + +# 显示尺寸取自 CSS:列表图标 30x30,返回箭头 17x28 +SPEC = [ + ('back', 'backarrow.png', 17, 28), + ('ic_help', 'MdiLightbulbOutline.png', 30, 30), + ('ic_replace', 'MdiBandage.png', 30, 30), + ('ic_about', 'MdiInformationOutline.png', 30, 30), + ('ic_check', 'MdiClipboardTextSearchOutline.png', 30, 30), + ('ic_rollback', 'MdiReplyAllOutline.png', 30, 30), + ('ic_qq', 'qq.png', 30, 30), +] + +os.makedirs(OUT, exist_ok=True) +for name, png, w, h in SPEC: + p = os.path.join(ASSETS, png) + if not os.path.exists(p): + print(f' 缺少 {png}'); continue + img = Image.open(p).convert('RGBA') + if img.size != (w, h): + img = img.resize((w, h), Image.LANCZOS) + blob = ip.encode(img, ip.CF_ARGB8888, w, h, 0) + dst = os.path.join(OUT, name + '.bin') + open(dst, 'wb').write(blob) + print(f' {name:<12} <- {png:<34} {w}x{h} {len(blob)} bytes') + +# 关于页的 logo:封面图缩到 78x78,并加圆形遮罩 +# (封面是圆形图案 + 黑色方角,不裁成圆的话会在墨绿卡片上露出黑方块) +logo_src = os.path.join(ASSETS, 'cover.png') +if os.path.exists(logo_src): + L = 78 + img = Image.open(logo_src).convert('RGBA').resize((L, L), Image.LANCZOS) + mask = Image.new('L', (L, L), 0) + ImageDraw.Draw(mask).ellipse([0, 0, L - 1, L - 1], fill=255) + img.putalpha(mask) + blob = ip.encode(img, ip.CF_ARGB8888, L, L, 0) + open(os.path.join(OUT, 'logo.bin'), 'wb').write(blob) + # 顺便存一份 PNG 给本机预览用(注意别放进打包目录,会一起被打进去) + prev = os.path.join(DOCS, 'logo_preview.png') + os.makedirs(os.path.dirname(prev), exist_ok=True) + img.save(prev) + print(f' {"logo":<12} <- cover.png(圆形裁切) {L}x{L} {len(blob)} bytes') + +print('written to', OUT) diff --git a/s4e-icons/tools/pages.py b/s4e-icons/tools/pages.py new file mode 100644 index 0000000..cb8f259 --- /dev/null +++ b/s4e-icons/tools/pages.py @@ -0,0 +1,180 @@ +"""页面规格 + 本机渲染器:按 CSS 逐条落地,用真 MiSans 字体和真素材渲染预览。 + +CSS -> 实际能力 的映射(已在真机验证过): + left/top/width/height -> x / y / w / h(466 画布绝对坐标) + background -> bg_color + border-radius -> radius(50x50 圆的 radius=25) + color (alpha 1) -> text_color '#rrggbb' + color (alpha 0.6) -> 白 60% 叠在纯黑底上 = #999999(页面底色是黑,见下) + font-weight 600 -> MiSans-Semibold + font-weight 500 -> MiSans-Medium + vertical-align middle -> vcenter=True + 多色文本 -> LVGL recolor: "灰#ffffff 白#灰" +""" +import os + +from paths import PROJ, DOCS +ROOT = PROJ +ASSETS = os.path.join(ROOT, 'assets') +W = H = 466 +BG = (0, 0, 0) # 封面四角纯黑 -> 页面底色 +GRAY = '999999' # rgba(255,255,255,0.6) 叠黑 +WHITE = 'ffffff' +PLATE = '002927' # rgba(0,41,39,1) +CIRCLE = '4f4f4f' # rgba(79,79,79,1) +OK = '43cf7c' # rgba(67,207,124,1) +ERR = 'd43030' # rgba(212,48,48,1) + +S600, S500 = 'semibold', 'medium' + +# 列表项几何(来自第一个表项的 CSS,其余按同一相对关系排布) +ITEM_X, ITEM_W, ITEM_H, ITEM_R = 49, 368, 88, 34 +PITCH = 96 # 88 + 8px 间距 +ICONC_DX, ICONC_DY, ICONC_SZ = 20, 19, 50 # 圆形遮罩 69-49 / 104-85 +ICON_DX, ICON_DY, ICON_SZ = 30, 29, 30 # 图标图片 79-49 / 114-85 +TITLE_DX, TITLE_DY, TITLE_W, TITLE_H = 80, 17, 208, 54 # 129-49 / 102-85 + + +def item(y, icon, title): + """一个列表项:底板 + 圆形遮罩 + 图标 + 标题""" + return [ + dict(kind='rect', x=ITEM_X, y=y, w=ITEM_W, h=ITEM_H, bg=PLATE, radius=ITEM_R), + dict(kind='circle', x=ITEM_X + ICONC_DX, y=y + ICONC_DY, + w=ICONC_SZ, h=ICONC_SZ, bg=CIRCLE), + dict(kind='image', x=ITEM_X + ICON_DX, y=y + ICON_DY, + w=ICON_SZ, h=ICON_SZ, src=os.path.join(ASSETS, icon)), + dict(kind='label', x=ITEM_X + TITLE_DX, y=y + TITLE_DY, + w=TITLE_W, h=TITLE_H, text=title, size=28, weight=S500, + color=WHITE, align='left', vcenter=True, lh=37.13), + ] + + +def clock(t='10:24'): + return dict(kind='label', x=168, y=0, w=130, h=45, text=t, size=28, + weight=S600, color=GRAY, align='center', vcenter=True, lh=33.6) + + +def title(text, x=106, w=255): + return dict(kind='label', x=x, y=34, w=w, h=45, text=text, size=30, + weight=S500, color=WHITE, align='center', vcenter=True, lh=44.8) + + +def back_arrow(): + return dict(kind='image', x=160, y=43, w=17, h=28, + src=os.path.join(ASSETS, 'backarrow.png')) + + +def big_button(y=365, text='确认替换'): + return [ + dict(kind='rect', x=108, y=y, w=250, h=80, bg=PLATE, radius=40), + dict(kind='label', x=173, y=y + 13, w=120, h=54, text=text, size=28, + weight=S500, color=WHITE, align='center', vcenter=True, lh=37.13), + ] + + +# ---------------------------------------------------------------- 1. 首页 +PAGE_HOME = [ + clock(), + title('OS4 Icons'), + *item(85, 'MdiLightbulbOutline.png', '快速帮助'), + *item(181, 'MdiBandage.png', '开始替换'), + *item(277, 'MdiInformationOutline.png', '关于'), +] + +# ---------------------------------------------------------------- 2. 快速帮助 +_HELP_HEAD = [clock(), back_arrow(), title('快速帮助', x=179, w=128)] +_HELP_I1 = item(85, 'MdiClipboardTextSearchOutline.png', '环境预检查') +_HELP_D1 = dict(kind='label', x=65, y=176, w=338, h=72, + text='检查原始资源状态、分区可写情况等。', size=24, weight=S500, + color=GRAY, align='left', vtop=True, lh=31.82) +_HELP_I2 = item(256, 'MdiReplyAllOutline.png', '回滚修改') +_HELP_D2 = dict(kind='label', x=65, y=344, w=334, h=72, + text='将图标恢复到系统原始样式。', size=24, weight=S500, + color=GRAY, align='left', vtop=True, lh=31.82) + +PAGE_HELP = _HELP_HEAD + _HELP_I1 + [_HELP_D1] + _HELP_I2 + [_HELP_D2] + +# 帮助页的两种结果状态(同一标签换色换字) +HELP_DESC_OK = dict(kind='label', x=65, y=176, w=338, h=72, + text='预检查通过 | 38 条 | 2.58MB\n您可以返回主页开始修改。', + size=24, weight=S500, color=OK, align='left', vtop=True, lh=31.82) +HELP_DESC_ERR = dict(kind='label', x=65, y=176, w=338, h=72, + text='预检查失败:分区头不是 ROMFS,位置 0x00。', + size=24, weight=S500, color=ERR, align='left', vtop=True, lh=31.82) + +PAGE_HELP_OK = _HELP_HEAD + _HELP_I1 + [HELP_DESC_OK] + _HELP_I2 + [_HELP_D2] +PAGE_HELP_ERR = _HELP_HEAD + _HELP_I1 + [HELP_DESC_ERR] + _HELP_I2 + [_HELP_D2] + +# ---------------------------------------------------------------- 3. 开始替换 +# 多色文本**不能**用 recolor(AP 里那几处是图片重着色,不是文本属性), +# 所以拆成多个标签 + 手工算 x。折行也写死,否则多色片段位置不确定。 +# 宽度用真 MiSans 24px 量得: +# "的系统版本号为 " = 177 "3.202.79" = 99 +# "Xiaomi Watch S4 eSIM(非" = 313 "15 周年纪念版)" = 178 +WARN_LABELS = [ + dict(kind='label', x=64, y=90, w=338, h=70, + text='替换图标包之前,请先确认自己\n的系统版本号为 ', + size=24, weight=S500, color=GRAY, align='left', vtop=True, lh=33), + dict(kind='label', x=241, y=123, w=104, h=33, text='3.202.79', + size=24, weight=S500, color=WHITE, align='left', vtop=True, lh=33), + dict(kind='label', x=340, y=123, w=30, h=33, text='!', + size=24, weight=S500, color=GRAY, align='left', vtop=True, lh=33), + dict(kind='label', x=64, y=189, w=338, h=33, text='请确认自己的手表型号为', + size=24, weight=S500, color=GRAY, align='left', vtop=True, lh=33), + dict(kind='label', x=64, y=222, w=338, h=70, + text='Xiaomi Watch S4 eSIM(非\n15 周年纪念版)', + size=24, weight=S500, color=WHITE, align='left', vtop=True, lh=33), + dict(kind='label', x=242, y=255, w=30, h=33, text='!', + size=24, weight=S500, color=GRAY, align='left', vtop=True, lh=33), + dict(kind='label', x=64, y=321, w=338, h=33, text='搞机有风险,变砖后果自负。', + size=24, weight=S500, color=GRAY, align='left', vtop=True, lh=33), +] + +PAGE_CONFIRM = [ + clock(), + back_arrow(), + title('开始替换', x=179, w=128), + *WARN_LABELS, + *big_button(365, '确认替换'), +] + +# ---------------------------------------------------------------- 4. 替换中 +LOG_MID = ('> 开始替换\n' + '> 读取 resource.bin\n' + '> 写入 12/38\n' + '> 写入 38/38 完成,0 失败\n' + '> 替换完毕,请重启手表。若发现问题,请回滚修改。') + +PAGE_RUNNING = [ + clock(), + title('正在替换', x=179, w=128), + dict(kind='label', x=64, y=90, w=338, h=269, text=LOG_MID, size=24, + weight=S500, color=WHITE, align='left', vtop=True, lh=31.82), + *big_button(365, '重启手表'), +] + +# ---------------------------------------------------------------- 5. 关于 +PAGE_ABOUT = [ + clock(), + dict(kind='image', x=191, y=43, w=17, h=28, + src=os.path.join(ASSETS, 'backarrow.png')), + title('关于', x=212, w=64), + dict(kind='rect', x=52, y=79, w=362, h=204, bg=PLATE, radius=30), + dict(kind='image', x=194, y=108, w=78, h=78, + src=os.path.join(DOCS, 'logo_preview.png')), + dict(kind='label', x=164, y=192, w=138, h=38, text='OS4 Icons', + size=28, weight=S500, color=WHITE, align='left', vtop=True, lh=38), + dict(kind='label', x=210, y=227, w=45, h=27, text='1.0.0', + size=20, weight=S500, color=GRAY, align='left', vtop=True, lh=27), + *item(291, 'qq.png', '732681995'), +] + +PAGES = { + 'home': PAGE_HOME, + 'help': PAGE_HELP, + 'help_ok': PAGE_HELP_OK, + 'help_err': PAGE_HELP_ERR, + 'confirm': PAGE_CONFIRM, + 'about': PAGE_ABOUT, + 'running': PAGE_RUNNING, +} diff --git a/s4e-icons/tools/paths.py b/s4e-icons/tools/paths.py new file mode 100644 index 0000000..a2da22a --- /dev/null +++ b/s4e-icons/tools/paths.py @@ -0,0 +1,35 @@ +"""项目里所有脚本的路径入口 —— 换机器 / 换目录只看这一个文件。 + +默认布局(都相对项目根): + inputs/target/ 目标固件的分区镜像(要自己放,见 README) + inputs/source/ 图标来源的分区镜像(要自己放) + inputs/fonts/ 可选:MiSans-Medium.ttf / MiSans-Semibold.ttf(渲染预览用) + vendor/ 第三方 LuaDevTemplate-main(里面有 Compiler.exe,要自己放) + .work/ 构建工作区(产物,可删) + dist/ 交付的 .face + docs/ 说明与预览图 + +环境变量可覆盖:ICON_EDIT_INPUTS / ICON_EDIT_VENDOR / ICON_EDIT_WORK / + ICON_EDIT_DIST / ICON_EDIT_DOCS / ICON_EDIT_FONTS +""" +import os + +PROJ = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + + +def _p(env, default): + return os.environ.get(env) or os.path.join(PROJ, default) + + +INPUTS = _p('ICON_EDIT_INPUTS', 'inputs') +VENDOR = _p('ICON_EDIT_VENDOR', 'vendor') +WORK = _p('ICON_EDIT_WORK', '.work') +DIST = _p('ICON_EDIT_DIST', 'dist') +DOCS = _p('ICON_EDIT_DOCS', 'docs') +FONTS = os.environ.get('ICON_EDIT_FONTS') or os.path.join(INPUTS, 'fonts') +TEMPLATE = os.path.join(VENDOR, 'LuaDevTemplate-main') +INPUT_TARGET = os.path.join(INPUTS, 'target') +INPUT_SOURCE = os.path.join(INPUTS, 'source') + +for _d in (WORK, DIST, DOCS, INPUT_TARGET, INPUT_SOURCE): + os.makedirs(_d, exist_ok=True) diff --git a/s4e-icons/tools/render_ui.py b/s4e-icons/tools/render_ui.py new file mode 100644 index 0000000..3c75dc5 --- /dev/null +++ b/s4e-icons/tools/render_ui.py @@ -0,0 +1,193 @@ +"""按页面规格渲染 466x466 预览图(真 MiSans 字体 + 真素材 + 圆形遮罩)。 + +支持 kind: rect / circle / image / label +label 支持 recolor 标记 "#RRGGBB 文字#"(LVGL 的写法),以及 align/vcenter/vtop/lh +""" +import os, re, sys +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from PIL import Image, ImageDraw, ImageFont + +from paths import PROJ, INPUTS, FONTS, WORK +ROOT = PROJ +FONT_FILES = {'semibold': 'MiSans-Semibold.ttf', 'medium': 'MiSans-Medium.ttf', + 'regular': 'MiSans-Regular.ttf'} +W = H = 466 +_fc = {} + + +def _font_from_firmware(name): + """从 inputs/target/vela_font.bin 里现取 MiSans(固件里就有这几个 ttf)。""" + p = os.path.join(INPUTS, 'target', 'vela_font.bin') + if not os.path.exists(p): + return None + try: + from romfs import Romfs + r = Romfs(p) + except Exception: + return None + for path, ino in r.entries(): + if path == name: + d = os.path.join(WORK, 'fonts') + os.makedirs(d, exist_ok=True) + out = os.path.join(d, name) + open(out, 'wb').write(r.read(ino)) + return out + return None + + +def font(weight, size): + k = (weight, size) + if k in _fc: + return _fc[k] + name = FONT_FILES.get(weight, FONT_FILES['medium']) + p = os.path.join(FONTS, name) + if not os.path.exists(p): + p = _font_from_firmware(name) or p + if not os.path.exists(p): + for alt in (r'C:\Windows\Fonts\msyh.ttc', r'C:\Windows\Fonts\msyhbd.ttc', + r'C:\Windows\Fonts\arial.ttf'): + if os.path.exists(alt): + p = alt + print(' [warn] %s 缺失,预览改用 %s(只影响预览缩略图)' % (name, os.path.basename(alt))) + break + _fc[k] = ImageFont.truetype(p, size) if os.path.exists(p) else ImageFont.load_default() + return _fc[k] + + +def _rgb(c): + return tuple(int(c[i:i + 2], 16) for i in (0, 2, 4)) + + +def split_recolor(text, base): + """LVGL 写法: '灰#ffffff 白#灰' + - # + 6 位十六进制 -> 切到该颜色(该字符不显示) + - 再次遇到单独的 # -> 结束着色,回到基础色(该字符不显示) + """ + runs, buf, cur, incol = [], '', base, False + i = 0 + while i < len(text): + if text[i] == '#': + if re.match(r'[0-9a-fA-F]{6}', text[i + 1:i + 7]): + runs.append((cur, buf)); buf = '' + cur = text[i + 1:i + 7].lower(); incol = True + i += 7 + continue + if incol: + runs.append((cur, buf)); buf = '' + cur = base; incol = False + i += 1 + continue + buf += text[i]; i += 1 + runs.append((cur, buf)) + return runs + + +def wrap_runs(d, runs, f, maxw): + """按像素宽度折行,返回 [[(color, text), ...], ...](尽量保持原换行)""" + lines = [[]] + for color, chunk in runs: + for k, seg in enumerate(chunk.split('\n')): + if k: + lines.append([]) + if not seg: + continue + cur = '' + for ch in seg: + if d.textlength(cur + ch, font=f) > maxw and cur: + lines[-1].append((color, cur)) + lines.append([]) + cur = ch + else: + cur += ch + if cur: + lines[-1].append((color, cur)) + return lines + + +OVERFLOW = [] # (标签文本前 12 字, css_lh, 行数, 块高, 框高) + + +def draw_label(img, d, it): + f = font(it.get('weight', 'medium'), it['size']) + runs = split_recolor(it['text'], it.get('color', 'ffffff')) \ + if it.get('recolor') else [(it.get('color', 'ffffff'), it['text'])] + asc, desc = f.getmetrics() + natural = asc + desc + lh = max(it.get('lh', 0), natural) # LVGL 用字体自身行高,取较大者预测 + lines = wrap_runs(d, runs, f, it['w']) + total = lh * len(lines) + if total > it['h'] + 0.5: + OVERFLOW.append((it['text'][:14].replace('\n', '/'), + round(it.get('lh', 0), 2), len(lines), + round(total, 1), it['h'], round(natural, 2))) + if it.get('vcenter'): + y = it['y'] + (it['h'] - total) / 2 + elif it.get('vtop'): + y = it['y'] + else: + y = it['y'] + (it['h'] - total) / 2 + for ln in lines: + wsum = sum(d.textlength(t, font=f) for _, t in ln) + if it.get('align') == 'center': + x = it['x'] + (it['w'] - wsum) / 2 + elif it.get('align') == 'right': + x = it['x'] + it['w'] - wsum + else: + x = it['x'] + for color, t in ln: + d.text((x, y), t, font=f, fill=_rgb(color) + (255,)) + x += d.textlength(t, font=f) + y += lh + + +def render(items, out, mask=True, ring=False): + img = Image.new('RGBA', (W, H), (0, 0, 0, 0)) + d = ImageDraw.Draw(img) + for it in items: + k = it.get('kind') + if k == 'rect': + box = [it['x'], it['y'], it['x'] + it['w'] - 1, it['y'] + it['h'] - 1] + r = it.get('radius', 0) + (d.rounded_rectangle if r else d.rectangle)( + box, radius=r, fill=_rgb(it['bg']) + (255,)) if r else d.rectangle( + box, fill=_rgb(it['bg']) + (255,)) + elif k == 'circle': + box = [it['x'], it['y'], it['x'] + it['w'] - 1, it['y'] + it['h'] - 1] + d.ellipse(box, fill=_rgb(it['bg']) + (255,)) + elif k == 'image': + p = it['src'] + if os.path.exists(p): + im = Image.open(p).convert('RGBA').resize((it['w'], it['h']), Image.LANCZOS) + img.alpha_composite(im, (it['x'], it['y'])) + elif k == 'label': + draw_label(img, d, it) + if mask: + m = Image.new('L', (W, H), 0) + ImageDraw.Draw(m).ellipse([0, 0, W - 1, H - 1], fill=255) + img.putalpha(m) + base = Image.new('RGB', (W, H), (0, 0, 0)) + base.paste(img, (0, 0), img) + if ring: + ImageDraw.Draw(base).ellipse([0, 0, W - 1, H - 1], outline=(80, 80, 90)) + base.save(out) + return out + + +if __name__ == '__main__': + from pages import PAGES + outs = [] + for name, items in PAGES.items(): + p = os.path.join(ROOT, '_out', f'ui_{name}.png') + render(items, p) + outs.append(p) + print('wrote', p) + # 拼一张总览图 + cols = 3 + rows = (len(outs) + cols - 1) // cols + sheet = Image.new('RGB', (cols * (W + 12) + 12, rows * (H + 12) + 12), (28, 28, 32)) + for i, p in enumerate(outs): + im = Image.open(p) + sheet.paste(im, (12 + (i % cols) * (W + 12), 12 + (i // cols) * (H + 12))) + sp = os.path.join(ROOT, '_out', 'ui_all.png') + sheet.save(sp) + print('wrote', sp, sheet.size) diff --git a/s4e-icons/tools/romfs.py b/s4e-icons/tools/romfs.py new file mode 100644 index 0000000..753dda2 --- /dev/null +++ b/s4e-icons/tools/romfs.py @@ -0,0 +1,112 @@ +"""Minimal reader for Xiaomi Vela ROMFS partition images (Linux 'rom1fs' variant). + +Layout (big-endian): + super : magic "-rom1fs-", u32 size, u32 checksum, name (NUL-term, padded to 16) + inode : u32 next (low 4 bits = ROMFH_ type flags), u32 spec, u32 size, u32 checksum, + name (NUL-term, padded to 16), then file data immediately after. + type : 0=hardlink (spec = target inode offset), 1=dir (spec = offset of first child), + 2=regular file, 8=exec bit OR-ed in. + For a directory, its children are the chain starting at `spec`, linked by + `next & ~0xF`, and they stay inside the directory's own block. +""" +import struct, os + +ROUND16 = lambda n: (n + 15) & ~15 + +class Inode: + __slots__ = ('off', 'next', 'spec', 'size', 'cksum', 'name', 'namelen', 'data_off', 'kind') + + @property + def type_flags(self): + return self.next & 0xF + + @property + def type(self): + return self.next & 0x7 + + @property + def exec_bit(self): + return bool(self.next & 0x8) + + @property + def next_off(self): + return self.next & ~0xF + + KIND = {0: 'HRD', 1: 'DIR', 2: 'REG', 3: 'SYM', 4: 'BLK', 5: 'CHR', 6: 'SCK', 7: 'FIF'} + + def __repr__(self): + return (f'') + + +class Romfs: + def __init__(self, path): + self.path = path + self.data = open(path, 'rb').read() + d = self.data + if d[0:8] != b'-rom1fs-': + raise ValueError('not a romfs image') + self.sb_size, self.sb_cksum = struct.unpack('>II', d[8:16]) + end = d.index(b'\x00', 16) + self.volume = d[16:end].decode('latin1') + self.root_off = 16 + ROUND16(end - 16 + 1) + + def inode(self, off): + i = Inode() + i.off = off + i.next, i.spec, i.size, i.cksum = struct.unpack('>IIII', self.data[off:off+16]) + p = off + 16 + end = self.data.find(b'\x00', p, p + 256) + if end < 0: + raise ValueError(f'bad name at 0x{off:x}') + i.name = self.data[p:end].decode('latin1') + i.namelen = end - p + i.data_off = off + 16 + ROUND16(i.namelen + 1) + return i + + def walk(self, first, limit, prefix=''): + """Yield (path, Inode) for every entry reachable from `first` below `limit`. + + `limit` bounds the sibling chain. The LAST entry of a chain has next==0, + but its own children still live after it: they inherit the parent's limit + instead of being bounded by that zero. + """ + off = first + seen = set() + while off and (limit == 0 or off < limit) and off not in seen: + seen.add(off) + i = self.inode(off) + path = prefix + i.name + yield path, i + if i.type == 1 and i.spec: # directory -> first child at spec + yield from self.walk(i.spec, i.next_off or limit, path + '/') + off = i.next_off + + def entries(self): + """All entries of the image, as (path, Inode).""" + return self.walk(self.root_off + 0x20, len(self.data)) + + def read(self, ino): + return self.data[ino.data_off:ino.data_off + ino.size] + + +def img_header(blob): + if len(blob) < 12 or blob[0] != 0x19: + return None + magic, cf, flags, w, h, stride, res = struct.unpack(' 2 and sys.argv[2] not in path: + continue + kind = ('DIR ' if i.type == 1 else 'LNK ' if i.type == 0 else 'FILE') + print(f'{kind} {i.size:>9} {path}') + print(f'-- total entries: {n}') diff --git a/s4e-icons/tools/s4e_verify.py b/s4e-icons/tools/s4e_verify.py new file mode 100644 index 0000000..2e0fded --- /dev/null +++ b/s4e-icons/tools/s4e_verify.py @@ -0,0 +1,175 @@ +# -*- coding: utf-8 -*- +"""S4 图标表盘交付物的独立校验(不信任构建脚本的账本)。 + +从 dist/S5Icons.face 里把 Lua 源码抠出来 -> 解析 UIIMG / PAYTBL 两张表 -> +每一条都拿去和 **固件镜像**(inputs/target 原厂 + .work/s4_from_s5 改后)以及 +ui_images/ 目录逐字节比对;另外单独验证 voice_aivs 那条的字节就是 +assets/override/voice_aivs.png 按槽位格式重编码的结果。 + +用法: python tools/s4e_verify.py +""" +import os, re, sys, json, hashlib +sys.stdout.reconfigure(encoding='utf-8', errors='replace') +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from romfs import Romfs +from lvgl_bin import decode_lvgl_bin +import iconpack as ip +from PIL import Image, ImageFilter + +from paths import PROJ, INPUTS, WORK, DIST + +FACE = os.path.join(DIST, 'S5Icons.face') +STOCK = os.path.join(INPUTS, 'target', 'vela_app.bin') +PATCHED = os.path.join(WORK, 's4_from_s5', 'vela_app_s5icons.bin') +MANIFEST = os.path.join(WORK, 's4_from_s5', 'manifest.json') +WID = '362150101' + +fails = [] + + +def ck(cond, msg): + print((' OK ' if cond else ' FAIL ') + msg) + if not cond: + fails.append(msg) + + +face = open(FACE, 'rb').read() +_end = face.find(b'return ui\n') +if _end < 0: + raise SystemExit('face 里找不到 Lua 结尾') +_start = face.rfind(b'--[[', 0, _end) +lua = face[_start:_end + len(b'return ui\n')] +if not lua.startswith(b'--[[--') or b'local UIIMG = {' not in lua: + raise SystemExit('Lua 提取失败:%r ... %r' % (lua[:24], lua[-24:])) +txt = lua.decode('utf-8') + +print('== 头部 ==') +ck(face[0:4] == b'\x5a\xa5\x34\x12', 'magic 5a a5 34 12') +ck(face[40:50] == (WID + '\x00').encode(), 'id = %r' % face[40:50]) +ck(face[5] == 0, 'b5 = %d(固件自带表盘也是 0)' % face[5]) +ck(face.count(lua) == 1, 'Lua 源码原样出现 %d 次(%d B)' % (face.count(lua), len(lua))) +print(' 文件 %d B' % len(face)) + +print('== UI 图片 ==') +ui = re.findall(r'\{(\d+),(\d+),"([a-z_]+)"\}', txt) +files = {f[:-4]: open(os.path.join(PROJ, 'ui_images', f), 'rb').read() + for f in os.listdir(os.path.join(PROJ, 'ui_images')) if f.endswith('.bin')} +ck(len(ui) == len(files), 'UIIMG %d 条(ui_images 里 %d 个)' % (len(ui), len(files))) +for o, l, n in ui: + o, l = int(o), int(l) + ck(n in files and face[o:o + l] == files[n], + '%-12s @0x%06x len=%-6d 与 ui_images/%s.bin 一致' % (n, o, l, n)) + +print('== 负载表结构 ==') +_pay_at = txt.index('pay = {') +_rb_at = txt.index('rb = {', _pay_at) +pay = [(int(a), int(b), int(c)) for a, b, c in re.findall(r'\{(\d+),(\d+),(\d+)\}', txt[_pay_at:_rb_at])] +rb = [(int(a), int(b), int(c)) for a, b, c in re.findall(r'\{(\d+),(\d+),(\d+)\}', txt[_rb_at:])] +exp = json.load(open(MANIFEST, encoding='utf-8'))['records'] # 计划替换的 81 条 +pj = json.load(open(os.path.join(PROJ, 'payload.json'), encoding='utf-8')) +emit = pj['records'] # 实际下发的(含 canary) +dropped = pj['dropped_identical'] +ck(len(emit) - 1 == len(exp) - len(dropped), + '下发 %d 条 = 计划 %d 条 - 与源逐字节相同的 %d 条 %s + canary' + % (len(emit), len(exp), len(dropped), dropped)) +ck(len(pay) == len(rb) == len(emit), 'pay %d 条 / rb %d 条(payload.json %d 条)' + % (len(pay), len(rb), len(emit))) +ck(len(pay) > 0 and len(pay) == len(rb), '两张表条数相同') + +print('== 逐条字节比对(负载 vs 改后镜像、回滚 vs 原厂镜像)==') +stock = open(STOCK, 'rb').read() +patched = open(PATCHED, 'rb').read() +ck(len(stock) == len(patched) and stock[:8] == b'-rom1fs-', '两个整分区镜像等长且是 ROMFS') +bad, misalign, manifest_bad = 0, 0, 0 +for i, ((po, pt, pl), (ro, rt, rl)) in enumerate(zip(pay, rb)): + if not (po and ro) or (pt, pl) != (rt, rl): + manifest_bad += 1 + continue + if pt % 4 or pt + pl > len(stock): # RLE 素材的长度可以是任意值,只有偏移必须对齐 + misalign += 1 + if (face[po:po + pl] != patched[pt:pt + pl] or len(face[po:po + pl]) != pl + or face[ro:ro + pl] != stock[pt:pt + pl] or len(face[ro:ro + pl]) != pl): + bad += 1 + print(' FAIL #%d %s off=0x%x len=%d' + % (i, emit[i]['name'] if i < len(emit) else '?', pt, pl)) +ck(bad == 0, '%d 条:负载 == 改后镜像,回滚 == 原厂镜像' % len(pay)) +ck(misalign == 0, '全部 4 对齐且不越界') +ck(manifest_bad == 0, 'pay/rb 的目标偏移与长度一一对应') +names_in_lua = [e['name'] for e in emit] +plan_only = [r['name'] for r in exp if r['name'] not in dropped] +ck(names_in_lua[1:] == plan_only, '下发名单(去掉 canary)== 计划名单(去掉 %s)' % dropped) + +print('== 负载名单 ==') +names = [r['name'] for r in exp] +ck('voice_aivs' in names, 'voice_aivs 在负载里') +ck(sorted(names) == sorted(set(names)), '没有重名记录') + +print('== voice_aivs 的字节 == 用户 PNG 重编码 ==') +rec = next(r for r in exp if r['name'] == 'voice_aivs') +r = Romfs(STOCK) +sheet = {p: i for p, i in r.entries() if p == 'voice/aivs/launcher.bin'} +ino = sheet['voice/aivs/launcher.bin'] +ck(ino.data_off == rec['offset'] and ino.size == rec['length'], + 'manifest 偏移/长度 == ROMFS inode(0x%08x / %d)' % (ino.data_off, ino.size)) +h, stock_img = decode_lvgl_bin(r.read(ino)) + + +def ink_bbox(img, thr=16): + a = img.convert('RGBA').getchannel('A'); w, hh = a.size; px = a.load() + x0, y0, x1, y1 = w, hh, -1, -1 + for y in range(hh): + for x in range(w): + if px[x, y] > thr: + x0 = min(x0, x); y0 = min(y0, y); x1 = max(x1, x); y1 = max(y1, y) + return None if x1 < 0 else (x0, y0, x1, y1) + + +sp = os.path.join(PROJ, 'assets', 'override', 'voice_aivs.png') +src = Image.open(sp).convert('RGBA') +sb = ink_bbox(stock_img) +target = max(sb[2] - sb[0] + 1, sb[3] - sb[1] + 1) +nb = ink_bbox(src) +art = src.crop((nb[0], nb[1], nb[2] + 1, nb[3] + 1)).resize((target, target), Image.LANCZOS) +cv = Image.new('RGBA', (h['w'], h['h']), (0, 0, 0, 0)) +cv.paste(art, ((h['w'] - target) // 2, (h['h'] - target) // 2)) +want = ip.encode(cv, h['cf'], h['w'], h['h'], h['flags']) +ck(len(want) == ino.size, '按槽位格式重编码 %d B == 槽位 %d B' % (len(want), ino.size)) +got = [(o, t, l) for (o, t, l) in pay if t == rec['offset']] +ck(bool(got) and face[got[0][0]:got[0][0] + rec['length']] == want, + '交付物里 voice_aivs 的负载字节 == 用户 PNG 重编码结果') +ck(patched[ino.data_off:ino.data_off + ino.size] == want, '改后整分区镜像里也是同一份字节') +ck(stock[ino.data_off:ino.data_off + ino.size] != want, '与 S4 原厂字节确实不同') +ck(os.path.basename(sp) == 'voice_aivs.png' and + hashlib.sha256(open(sp, 'rb').read()).hexdigest()[:16] == '362166b54ac03f2b', + 'override 源图 sha256 362166b54ac03f2b(= 用户给的 smallai.png)') + +print('== ROMFS 结构未被破坏 ==') +a = [(p, i.off, i.next, i.spec, i.size, i.cksum) for p, i in Romfs(STOCK).entries()] +b = [(p, i.off, i.next, i.spec, i.size, i.cksum) for p, i in Romfs(PATCHED).entries()] +ck(a == b, 'inode 表逐位不变(%d 条)' % len(a)) +ck(Romfs(STOCK).sb_size == Romfs(PATCHED).sb_size and + Romfs(STOCK).sb_cksum == Romfs(PATCHED).sb_cksum, 'superblock 不变') +r2 = Romfs(PATCHED) +i1 = {p: i for p, i in Romfs(STOCK).entries()} +i2 = {p: i for p, i in r2.entries()} +changed = sorted(p for p in i1 if Romfs(STOCK).read(i1[p]) != r2.read(i2[p])) +ck(len(changed) == len(pay) - 1, '真正变化的文件 %d 个(负载 %d 条,其中 canary 是分区头)' + % (len(changed), len(pay))) + +print('== 负载/回滚文件在 .face 里连续存放 ==') +pfiles = [(f, open(os.path.join(PROJ, 'payload', f), 'rb').read()) + for f in sorted(os.listdir(os.path.join(PROJ, 'payload'))) if f.endswith('.bin')] +rbfiles = [(f, open(os.path.join(PROJ, 'payload_rollback', f), 'rb').read()) + for f in sorted(os.listdir(os.path.join(PROJ, 'payload_rollback'))) if f.endswith('.bin')] +ck(len(pfiles) == len(pay), 'payload/ 里 %d 个文件 == %d 条' % (len(pfiles), len(pay))) +hit_pay = sum(1 for (o, t, l) in pay if any(face[o:o + l] == d for _, d in pfiles)) +hit_rb = sum(1 for (o, t, l) in rb if any(face[o:o + l] == d for _, d in rbfiles)) +ck(hit_pay == len(pay), '每条负载都能在 payload/ 里找到同名同字节的文件(%d/%d)' % (hit_pay, len(pay))) +ck(hit_rb == len(rb), '每条回滚都能在 payload_rollback/ 里找到(%d/%d)' % (hit_rb, len(rb))) + +print() +if fails: + print('!! %d 项未通过' % len(fails)) + sys.exit(1) +print('全部通过 —— %s (%d B, sha256 %s)' % (os.path.basename(FACE), len(face), + hashlib.sha256(face).hexdigest()[:16])) diff --git a/s4e-icons/ui_images/back.bin b/s4e-icons/ui_images/back.bin new file mode 100644 index 0000000..35542a3 Binary files /dev/null and b/s4e-icons/ui_images/back.bin differ diff --git a/s4e-icons/ui_images/ic_about.bin b/s4e-icons/ui_images/ic_about.bin new file mode 100644 index 0000000..7db6e44 Binary files /dev/null and b/s4e-icons/ui_images/ic_about.bin differ diff --git a/s4e-icons/ui_images/ic_check.bin b/s4e-icons/ui_images/ic_check.bin new file mode 100644 index 0000000..372fcf0 Binary files /dev/null and b/s4e-icons/ui_images/ic_check.bin differ diff --git a/s4e-icons/ui_images/ic_help.bin b/s4e-icons/ui_images/ic_help.bin new file mode 100644 index 0000000..bc6ec53 Binary files /dev/null and b/s4e-icons/ui_images/ic_help.bin differ diff --git a/s4e-icons/ui_images/ic_qq.bin b/s4e-icons/ui_images/ic_qq.bin new file mode 100644 index 0000000..718cfc7 Binary files /dev/null and b/s4e-icons/ui_images/ic_qq.bin differ diff --git a/s4e-icons/ui_images/ic_replace.bin b/s4e-icons/ui_images/ic_replace.bin new file mode 100644 index 0000000..1d3c76a Binary files /dev/null and b/s4e-icons/ui_images/ic_replace.bin differ diff --git a/s4e-icons/ui_images/ic_rollback.bin b/s4e-icons/ui_images/ic_rollback.bin new file mode 100644 index 0000000..c92aaab Binary files /dev/null and b/s4e-icons/ui_images/ic_rollback.bin differ diff --git a/s4e-icons/ui_images/logo.bin b/s4e-icons/ui_images/logo.bin new file mode 100644 index 0000000..701c108 Binary files /dev/null and b/s4e-icons/ui_images/logo.bin differ diff --git a/s4e-pressure-anim/.gitignore b/s4e-pressure-anim/.gitignore new file mode 100644 index 0000000..13a0346 --- /dev/null +++ b/s4e-pressure-anim/.gitignore @@ -0,0 +1,10 @@ +# 大文件 / 中间产物:不进仓库 +.work/ +inputs/ +vendor/ +__pycache__/ +*.pyc +payload/ +payload_restore/ +blob_replace/ +blob_restore/ diff --git a/s4e-pressure-anim/README.md b/s4e-pressure-anim/README.md new file mode 100644 index 0000000..a8f121b --- /dev/null +++ b/s4e-pressure-anim/README.md @@ -0,0 +1,246 @@ +# 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 通过 | \ No newline at end of file diff --git a/s4e-pressure-anim/assets/MdiBandage.png b/s4e-pressure-anim/assets/MdiBandage.png new file mode 100644 index 0000000..26abc5b Binary files /dev/null and b/s4e-pressure-anim/assets/MdiBandage.png differ diff --git a/s4e-pressure-anim/assets/MdiClipboardTextSearchOutline.png b/s4e-pressure-anim/assets/MdiClipboardTextSearchOutline.png new file mode 100644 index 0000000..85ea2da Binary files /dev/null and b/s4e-pressure-anim/assets/MdiClipboardTextSearchOutline.png differ diff --git a/s4e-pressure-anim/assets/MdiInformationOutline.png b/s4e-pressure-anim/assets/MdiInformationOutline.png new file mode 100644 index 0000000..3af2a29 Binary files /dev/null and b/s4e-pressure-anim/assets/MdiInformationOutline.png differ diff --git a/s4e-pressure-anim/assets/MdiLightbulbOutline.png b/s4e-pressure-anim/assets/MdiLightbulbOutline.png new file mode 100644 index 0000000..7d7819c Binary files /dev/null and b/s4e-pressure-anim/assets/MdiLightbulbOutline.png differ diff --git a/s4e-pressure-anim/assets/MdiReplyAllOutline.png b/s4e-pressure-anim/assets/MdiReplyAllOutline.png new file mode 100644 index 0000000..f419de0 Binary files /dev/null and b/s4e-pressure-anim/assets/MdiReplyAllOutline.png differ diff --git a/s4e-pressure-anim/assets/backarrow.png b/s4e-pressure-anim/assets/backarrow.png new file mode 100644 index 0000000..9e55859 Binary files /dev/null and b/s4e-pressure-anim/assets/backarrow.png differ diff --git a/s4e-pressure-anim/assets/cover.png b/s4e-pressure-anim/assets/cover.png new file mode 100644 index 0000000..23c1cf8 Binary files /dev/null and b/s4e-pressure-anim/assets/cover.png differ diff --git a/s4e-pressure-anim/assets/override/activities.png b/s4e-pressure-anim/assets/override/activities.png new file mode 100644 index 0000000..ae5c552 Binary files /dev/null and b/s4e-pressure-anim/assets/override/activities.png differ diff --git a/s4e-pressure-anim/assets/override/interconnect.png b/s4e-pressure-anim/assets/override/interconnect.png new file mode 100644 index 0000000..5739b3b Binary files /dev/null and b/s4e-pressure-anim/assets/override/interconnect.png differ diff --git a/s4e-pressure-anim/assets/override/sports_course.png b/s4e-pressure-anim/assets/override/sports_course.png new file mode 100644 index 0000000..4b95e98 Binary files /dev/null and b/s4e-pressure-anim/assets/override/sports_course.png differ diff --git a/s4e-pressure-anim/assets/override/voice_aivs.png b/s4e-pressure-anim/assets/override/voice_aivs.png new file mode 100644 index 0000000..228914f Binary files /dev/null and b/s4e-pressure-anim/assets/override/voice_aivs.png differ diff --git a/s4e-pressure-anim/assets/qq.png b/s4e-pressure-anim/assets/qq.png new file mode 100644 index 0000000..3982762 Binary files /dev/null and b/s4e-pressure-anim/assets/qq.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/background.png b/s4e-pressure-anim/assets/walkie_talkie/background.png new file mode 100644 index 0000000..879c98f Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/background.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/cancel_btn.png b/s4e-pressure-anim/assets/walkie_talkie/cancel_btn.png new file mode 100644 index 0000000..cc56188 Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/cancel_btn.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/check.png b/s4e-pressure-anim/assets/walkie_talkie/check.png new file mode 100644 index 0000000..bc89d28 Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/check.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/checked.png b/s4e-pressure-anim/assets/walkie_talkie/checked.png new file mode 100644 index 0000000..64bf2b9 Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/checked.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/close_btn.png b/s4e-pressure-anim/assets/walkie_talkie/close_btn.png new file mode 100644 index 0000000..a872247 Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/close_btn.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/confirm_btn.png b/s4e-pressure-anim/assets/walkie_talkie/confirm_btn.png new file mode 100644 index 0000000..932d711 Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/confirm_btn.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/delete_icon.png b/s4e-pressure-anim/assets/walkie_talkie/delete_icon.png new file mode 100644 index 0000000..b3d948d Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/delete_icon.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/guidance/finish_guidance.png b/s4e-pressure-anim/assets/walkie_talkie/guidance/finish_guidance.png new file mode 100644 index 0000000..932d711 Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/guidance/finish_guidance.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/history_anim/anim0.png b/s4e-pressure-anim/assets/walkie_talkie/history_anim/anim0.png new file mode 100644 index 0000000..cdbf172 Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/history_anim/anim0.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/history_anim/anim1.png b/s4e-pressure-anim/assets/walkie_talkie/history_anim/anim1.png new file mode 100644 index 0000000..9eb6f8b Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/history_anim/anim1.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/history_anim/anim10.png b/s4e-pressure-anim/assets/walkie_talkie/history_anim/anim10.png new file mode 100644 index 0000000..927d24a Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/history_anim/anim10.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/history_anim/anim11.png b/s4e-pressure-anim/assets/walkie_talkie/history_anim/anim11.png new file mode 100644 index 0000000..38d3985 Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/history_anim/anim11.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/history_anim/anim12.png b/s4e-pressure-anim/assets/walkie_talkie/history_anim/anim12.png new file mode 100644 index 0000000..32c452c Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/history_anim/anim12.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/history_anim/anim13.png b/s4e-pressure-anim/assets/walkie_talkie/history_anim/anim13.png new file mode 100644 index 0000000..b4338f7 Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/history_anim/anim13.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/history_anim/anim14.png b/s4e-pressure-anim/assets/walkie_talkie/history_anim/anim14.png new file mode 100644 index 0000000..6dd42f1 Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/history_anim/anim14.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/history_anim/anim15.png b/s4e-pressure-anim/assets/walkie_talkie/history_anim/anim15.png new file mode 100644 index 0000000..e4795f0 Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/history_anim/anim15.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/history_anim/anim16.png b/s4e-pressure-anim/assets/walkie_talkie/history_anim/anim16.png new file mode 100644 index 0000000..5cdb8ad Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/history_anim/anim16.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/history_anim/anim17.png b/s4e-pressure-anim/assets/walkie_talkie/history_anim/anim17.png new file mode 100644 index 0000000..9eb6f8b Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/history_anim/anim17.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/history_anim/anim18.png b/s4e-pressure-anim/assets/walkie_talkie/history_anim/anim18.png new file mode 100644 index 0000000..cdbf172 Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/history_anim/anim18.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/history_anim/anim2.png b/s4e-pressure-anim/assets/walkie_talkie/history_anim/anim2.png new file mode 100644 index 0000000..5cdb8ad Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/history_anim/anim2.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/history_anim/anim3.png b/s4e-pressure-anim/assets/walkie_talkie/history_anim/anim3.png new file mode 100644 index 0000000..e4795f0 Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/history_anim/anim3.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/history_anim/anim4.png b/s4e-pressure-anim/assets/walkie_talkie/history_anim/anim4.png new file mode 100644 index 0000000..6dd42f1 Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/history_anim/anim4.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/history_anim/anim5.png b/s4e-pressure-anim/assets/walkie_talkie/history_anim/anim5.png new file mode 100644 index 0000000..b4338f7 Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/history_anim/anim5.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/history_anim/anim6.png b/s4e-pressure-anim/assets/walkie_talkie/history_anim/anim6.png new file mode 100644 index 0000000..32c452c Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/history_anim/anim6.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/history_anim/anim7.png b/s4e-pressure-anim/assets/walkie_talkie/history_anim/anim7.png new file mode 100644 index 0000000..38d3985 Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/history_anim/anim7.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/history_anim/anim8.png b/s4e-pressure-anim/assets/walkie_talkie/history_anim/anim8.png new file mode 100644 index 0000000..927d24a Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/history_anim/anim8.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/history_anim/anim9.png b/s4e-pressure-anim/assets/walkie_talkie/history_anim/anim9.png new file mode 100644 index 0000000..6d5503a Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/history_anim/anim9.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/history_anim_right/anim0.png b/s4e-pressure-anim/assets/walkie_talkie/history_anim_right/anim0.png new file mode 100644 index 0000000..cdbf172 Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/history_anim_right/anim0.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/history_anim_right/anim1.png b/s4e-pressure-anim/assets/walkie_talkie/history_anim_right/anim1.png new file mode 100644 index 0000000..5d99f04 Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/history_anim_right/anim1.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/history_anim_right/anim10.png b/s4e-pressure-anim/assets/walkie_talkie/history_anim_right/anim10.png new file mode 100644 index 0000000..493b920 Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/history_anim_right/anim10.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/history_anim_right/anim11.png b/s4e-pressure-anim/assets/walkie_talkie/history_anim_right/anim11.png new file mode 100644 index 0000000..e596207 Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/history_anim_right/anim11.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/history_anim_right/anim12.png b/s4e-pressure-anim/assets/walkie_talkie/history_anim_right/anim12.png new file mode 100644 index 0000000..44ccabf Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/history_anim_right/anim12.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/history_anim_right/anim13.png b/s4e-pressure-anim/assets/walkie_talkie/history_anim_right/anim13.png new file mode 100644 index 0000000..cc6d96c Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/history_anim_right/anim13.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/history_anim_right/anim14.png b/s4e-pressure-anim/assets/walkie_talkie/history_anim_right/anim14.png new file mode 100644 index 0000000..709fe42 Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/history_anim_right/anim14.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/history_anim_right/anim15.png b/s4e-pressure-anim/assets/walkie_talkie/history_anim_right/anim15.png new file mode 100644 index 0000000..08c909e Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/history_anim_right/anim15.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/history_anim_right/anim16.png b/s4e-pressure-anim/assets/walkie_talkie/history_anim_right/anim16.png new file mode 100644 index 0000000..5b5a34a Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/history_anim_right/anim16.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/history_anim_right/anim17.png b/s4e-pressure-anim/assets/walkie_talkie/history_anim_right/anim17.png new file mode 100644 index 0000000..5d99f04 Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/history_anim_right/anim17.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/history_anim_right/anim18.png b/s4e-pressure-anim/assets/walkie_talkie/history_anim_right/anim18.png new file mode 100644 index 0000000..cdbf172 Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/history_anim_right/anim18.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/history_anim_right/anim2.png b/s4e-pressure-anim/assets/walkie_talkie/history_anim_right/anim2.png new file mode 100644 index 0000000..5b5a34a Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/history_anim_right/anim2.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/history_anim_right/anim3.png b/s4e-pressure-anim/assets/walkie_talkie/history_anim_right/anim3.png new file mode 100644 index 0000000..08c909e Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/history_anim_right/anim3.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/history_anim_right/anim4.png b/s4e-pressure-anim/assets/walkie_talkie/history_anim_right/anim4.png new file mode 100644 index 0000000..709fe42 Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/history_anim_right/anim4.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/history_anim_right/anim5.png b/s4e-pressure-anim/assets/walkie_talkie/history_anim_right/anim5.png new file mode 100644 index 0000000..cc6d96c Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/history_anim_right/anim5.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/history_anim_right/anim6.png b/s4e-pressure-anim/assets/walkie_talkie/history_anim_right/anim6.png new file mode 100644 index 0000000..44ccabf Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/history_anim_right/anim6.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/history_anim_right/anim7.png b/s4e-pressure-anim/assets/walkie_talkie/history_anim_right/anim7.png new file mode 100644 index 0000000..e596207 Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/history_anim_right/anim7.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/history_anim_right/anim8.png b/s4e-pressure-anim/assets/walkie_talkie/history_anim_right/anim8.png new file mode 100644 index 0000000..493b920 Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/history_anim_right/anim8.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/history_anim_right/anim9.png b/s4e-pressure-anim/assets/walkie_talkie/history_anim_right/anim9.png new file mode 100644 index 0000000..2286ae4 Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/history_anim_right/anim9.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/icon_plus.png b/s4e-pressure-anim/assets/walkie_talkie/icon_plus.png new file mode 100644 index 0000000..a1117ab Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/icon_plus.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/image/Speaker0.png b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker0.png new file mode 100644 index 0000000..fc91da7 Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker0.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/image/Speaker1.png b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker1.png new file mode 100644 index 0000000..66fc559 Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker1.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/image/Speaker10.png b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker10.png new file mode 100644 index 0000000..2bb9b59 Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker10.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/image/Speaker100.png b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker100.png new file mode 100644 index 0000000..521ed3e Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker100.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/image/Speaker101.png b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker101.png new file mode 100644 index 0000000..9b955dd Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker101.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/image/Speaker102.png b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker102.png new file mode 100644 index 0000000..7825c03 Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker102.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/image/Speaker11.png b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker11.png new file mode 100644 index 0000000..c4415dd Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker11.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/image/Speaker12.png b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker12.png new file mode 100644 index 0000000..844818b Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker12.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/image/Speaker13.png b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker13.png new file mode 100644 index 0000000..89029fd Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker13.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/image/Speaker14.png b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker14.png new file mode 100644 index 0000000..221f19a Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker14.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/image/Speaker15.png b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker15.png new file mode 100644 index 0000000..4423df7 Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker15.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/image/Speaker16.png b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker16.png new file mode 100644 index 0000000..bd8aa8a Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker16.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/image/Speaker17.png b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker17.png new file mode 100644 index 0000000..9ea2e8b Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker17.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/image/Speaker18.png b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker18.png new file mode 100644 index 0000000..fb5a10b Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker18.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/image/Speaker19.png b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker19.png new file mode 100644 index 0000000..3ad3bf7 Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker19.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/image/Speaker2.png b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker2.png new file mode 100644 index 0000000..8601636 Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker2.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/image/Speaker20.png b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker20.png new file mode 100644 index 0000000..b6ea25b Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker20.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/image/Speaker21.png b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker21.png new file mode 100644 index 0000000..14200fd Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker21.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/image/Speaker22.png b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker22.png new file mode 100644 index 0000000..3d9c48f Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker22.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/image/Speaker23.png b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker23.png new file mode 100644 index 0000000..e91fd3d Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker23.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/image/Speaker24.png b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker24.png new file mode 100644 index 0000000..393fc07 Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker24.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/image/Speaker25.png b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker25.png new file mode 100644 index 0000000..fa9e68e Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker25.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/image/Speaker26.png b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker26.png new file mode 100644 index 0000000..6d43f7d Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker26.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/image/Speaker27.png b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker27.png new file mode 100644 index 0000000..0a65418 Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker27.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/image/Speaker28.png b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker28.png new file mode 100644 index 0000000..b0160e4 Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker28.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/image/Speaker29.png b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker29.png new file mode 100644 index 0000000..1a8c275 Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker29.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/image/Speaker3.png b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker3.png new file mode 100644 index 0000000..142da0d Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker3.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/image/Speaker30.png b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker30.png new file mode 100644 index 0000000..c07d68b Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker30.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/image/Speaker31.png b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker31.png new file mode 100644 index 0000000..b5bf877 Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker31.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/image/Speaker32.png b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker32.png new file mode 100644 index 0000000..c7635aa Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker32.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/image/Speaker33.png b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker33.png new file mode 100644 index 0000000..55c5c23 Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker33.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/image/Speaker34.png b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker34.png new file mode 100644 index 0000000..8291262 Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker34.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/image/Speaker35.png b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker35.png new file mode 100644 index 0000000..7299baa Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker35.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/image/Speaker36.png b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker36.png new file mode 100644 index 0000000..ab8b08e Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker36.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/image/Speaker37.png b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker37.png new file mode 100644 index 0000000..5f76111 Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker37.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/image/Speaker38.png b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker38.png new file mode 100644 index 0000000..41cb88e Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker38.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/image/Speaker39.png b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker39.png new file mode 100644 index 0000000..72a5863 Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker39.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/image/Speaker4.png b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker4.png new file mode 100644 index 0000000..c7ad479 Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker4.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/image/Speaker40.png b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker40.png new file mode 100644 index 0000000..81c7391 Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker40.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/image/Speaker41.png b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker41.png new file mode 100644 index 0000000..9881ad6 Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker41.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/image/Speaker42.png b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker42.png new file mode 100644 index 0000000..7e9ec32 Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker42.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/image/Speaker43.png b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker43.png new file mode 100644 index 0000000..216afb2 Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker43.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/image/Speaker44.png b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker44.png new file mode 100644 index 0000000..873728e Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker44.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/image/Speaker45.png b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker45.png new file mode 100644 index 0000000..70535de Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker45.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/image/Speaker46.png b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker46.png new file mode 100644 index 0000000..43563a3 Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker46.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/image/Speaker47.png b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker47.png new file mode 100644 index 0000000..f7bd1b7 Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker47.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/image/Speaker48.png b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker48.png new file mode 100644 index 0000000..6a942f9 Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker48.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/image/Speaker49.png b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker49.png new file mode 100644 index 0000000..f65235c Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker49.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/image/Speaker5.png b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker5.png new file mode 100644 index 0000000..8681f10 Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker5.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/image/Speaker50.png b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker50.png new file mode 100644 index 0000000..be90a63 Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker50.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/image/Speaker51.png b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker51.png new file mode 100644 index 0000000..d6de73f Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker51.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/image/Speaker52.png b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker52.png new file mode 100644 index 0000000..8b58b2d Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker52.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/image/Speaker53.png b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker53.png new file mode 100644 index 0000000..7b3b206 Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker53.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/image/Speaker54.png b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker54.png new file mode 100644 index 0000000..2367008 Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker54.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/image/Speaker55.png b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker55.png new file mode 100644 index 0000000..dbdf016 Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker55.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/image/Speaker56.png b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker56.png new file mode 100644 index 0000000..1d3eacb Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker56.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/image/Speaker57.png b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker57.png new file mode 100644 index 0000000..97c19e3 Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker57.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/image/Speaker58.png b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker58.png new file mode 100644 index 0000000..a7195d0 Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker58.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/image/Speaker59.png b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker59.png new file mode 100644 index 0000000..3e36cba Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker59.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/image/Speaker6.png b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker6.png new file mode 100644 index 0000000..2503467 Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker6.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/image/Speaker60.png b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker60.png new file mode 100644 index 0000000..cb0e1b9 Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker60.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/image/Speaker61.png b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker61.png new file mode 100644 index 0000000..93b8905 Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker61.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/image/Speaker62.png b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker62.png new file mode 100644 index 0000000..d171dab Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker62.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/image/Speaker63.png b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker63.png new file mode 100644 index 0000000..3bdfd0f Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker63.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/image/Speaker64.png b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker64.png new file mode 100644 index 0000000..392093f Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker64.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/image/Speaker65.png b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker65.png new file mode 100644 index 0000000..e26ca7c Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker65.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/image/Speaker66.png b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker66.png new file mode 100644 index 0000000..57aff14 Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker66.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/image/Speaker67.png b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker67.png new file mode 100644 index 0000000..2b7281a Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker67.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/image/Speaker68.png b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker68.png new file mode 100644 index 0000000..cdb5592 Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker68.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/image/Speaker69.png b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker69.png new file mode 100644 index 0000000..dc7eba5 Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker69.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/image/Speaker7.png b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker7.png new file mode 100644 index 0000000..054a489 Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker7.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/image/Speaker70.png b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker70.png new file mode 100644 index 0000000..6bbe93d Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker70.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/image/Speaker71.png b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker71.png new file mode 100644 index 0000000..bc451fb Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker71.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/image/Speaker72.png b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker72.png new file mode 100644 index 0000000..69b5ee7 Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker72.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/image/Speaker73.png b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker73.png new file mode 100644 index 0000000..909de00 Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker73.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/image/Speaker74.png b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker74.png new file mode 100644 index 0000000..a2d65b2 Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker74.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/image/Speaker75.png b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker75.png new file mode 100644 index 0000000..77d3cf6 Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker75.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/image/Speaker76.png b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker76.png new file mode 100644 index 0000000..4bacc59 Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker76.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/image/Speaker77.png b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker77.png new file mode 100644 index 0000000..3527380 Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker77.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/image/Speaker78.png b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker78.png new file mode 100644 index 0000000..f9cfe31 Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker78.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/image/Speaker79.png b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker79.png new file mode 100644 index 0000000..4583948 Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker79.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/image/Speaker8.png b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker8.png new file mode 100644 index 0000000..6b885ec Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker8.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/image/Speaker80.png b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker80.png new file mode 100644 index 0000000..6dbe5d3 Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker80.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/image/Speaker81.png b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker81.png new file mode 100644 index 0000000..cdff971 Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker81.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/image/Speaker82.png b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker82.png new file mode 100644 index 0000000..304ccea Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker82.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/image/Speaker83.png b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker83.png new file mode 100644 index 0000000..fe62e25 Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker83.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/image/Speaker84.png b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker84.png new file mode 100644 index 0000000..263f40c Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker84.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/image/Speaker85.png b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker85.png new file mode 100644 index 0000000..060ae88 Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker85.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/image/Speaker86.png b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker86.png new file mode 100644 index 0000000..c8fec2a Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker86.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/image/Speaker87.png b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker87.png new file mode 100644 index 0000000..053ee6d Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker87.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/image/Speaker88.png b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker88.png new file mode 100644 index 0000000..15505f5 Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker88.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/image/Speaker89.png b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker89.png new file mode 100644 index 0000000..162e637 Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker89.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/image/Speaker9.png b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker9.png new file mode 100644 index 0000000..03b9b1e Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker9.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/image/Speaker90.png b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker90.png new file mode 100644 index 0000000..ceec461 Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker90.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/image/Speaker91.png b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker91.png new file mode 100644 index 0000000..b3b35ae Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker91.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/image/Speaker92.png b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker92.png new file mode 100644 index 0000000..3464b7d Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker92.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/image/Speaker93.png b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker93.png new file mode 100644 index 0000000..de800f6 Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker93.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/image/Speaker94.png b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker94.png new file mode 100644 index 0000000..51e6c2a Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker94.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/image/Speaker95.png b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker95.png new file mode 100644 index 0000000..a22dffe Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker95.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/image/Speaker96.png b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker96.png new file mode 100644 index 0000000..f7151aa Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker96.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/image/Speaker97.png b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker97.png new file mode 100644 index 0000000..eae1262 Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker97.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/image/Speaker98.png b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker98.png new file mode 100644 index 0000000..328931c Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker98.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/image/Speaker99.png b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker99.png new file mode 100644 index 0000000..11a494e Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/image/Speaker99.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/image/go_next.png b/s4e-pressure-anim/assets/walkie_talkie/image/go_next.png new file mode 100644 index 0000000..e9a1d81 Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/image/go_next.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/image/stop.png b/s4e-pressure-anim/assets/walkie_talkie/image/stop.png new file mode 100644 index 0000000..b611709 Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/image/stop.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/launcher.png b/s4e-pressure-anim/assets/walkie_talkie/launcher.png new file mode 100644 index 0000000..9a611a9 Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/launcher.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/launcher1.png b/s4e-pressure-anim/assets/walkie_talkie/launcher1.png new file mode 100644 index 0000000..60aa469 Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/launcher1.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/listen.png b/s4e-pressure-anim/assets/walkie_talkie/listen.png new file mode 100644 index 0000000..e06f591 Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/listen.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/login_success.png b/s4e-pressure-anim/assets/walkie_talkie/login_success.png new file mode 100644 index 0000000..6e8c826 Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/login_success.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/next_btn.png b/s4e-pressure-anim/assets/walkie_talkie/next_btn.png new file mode 100644 index 0000000..415abc0 Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/next_btn.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/poc/default_icon.png b/s4e-pressure-anim/assets/walkie_talkie/poc/default_icon.png new file mode 100644 index 0000000..d5c33f0 Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/poc/default_icon.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/poc/group_icon1.png b/s4e-pressure-anim/assets/walkie_talkie/poc/group_icon1.png new file mode 100644 index 0000000..9f22404 Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/poc/group_icon1.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/poc/group_icon2.png b/s4e-pressure-anim/assets/walkie_talkie/poc/group_icon2.png new file mode 100644 index 0000000..0adef53 Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/poc/group_icon2.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/poc/group_icon3.png b/s4e-pressure-anim/assets/walkie_talkie/poc/group_icon3.png new file mode 100644 index 0000000..4f1560b Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/poc/group_icon3.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/poc/group_icon4.png b/s4e-pressure-anim/assets/walkie_talkie/poc/group_icon4.png new file mode 100644 index 0000000..5bb064a Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/poc/group_icon4.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/poc/group_icon5.png b/s4e-pressure-anim/assets/walkie_talkie/poc/group_icon5.png new file mode 100644 index 0000000..abd170d Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/poc/group_icon5.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/poc/member_icon.png b/s4e-pressure-anim/assets/walkie_talkie/poc/member_icon.png new file mode 100644 index 0000000..d5c33f0 Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/poc/member_icon.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/poc/no_sound.png b/s4e-pressure-anim/assets/walkie_talkie/poc/no_sound.png new file mode 100644 index 0000000..2244475 Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/poc/no_sound.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/poc/say_button_mic_icon.png b/s4e-pressure-anim/assets/walkie_talkie/poc/say_button_mic_icon.png new file mode 100644 index 0000000..64e5ea1 Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/poc/say_button_mic_icon.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/poc/sound.png b/s4e-pressure-anim/assets/walkie_talkie/poc/sound.png new file mode 100644 index 0000000..9b7b453 Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/poc/sound.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/poc/talk_listen.png b/s4e-pressure-anim/assets/walkie_talkie/poc/talk_listen.png new file mode 100644 index 0000000..c9a9688 Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/poc/talk_listen.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/poc/toast_listen.png b/s4e-pressure-anim/assets/walkie_talkie/poc/toast_listen.png new file mode 100644 index 0000000..c9a9688 Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/poc/toast_listen.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/poc/toast_say.png b/s4e-pressure-anim/assets/walkie_talkie/poc/toast_say.png new file mode 100644 index 0000000..f5b5dec Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/poc/toast_say.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/poc/tools.png b/s4e-pressure-anim/assets/walkie_talkie/poc/tools.png new file mode 100644 index 0000000..e7cf9be Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/poc/tools.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/poc/voice_icon.png b/s4e-pressure-anim/assets/walkie_talkie/poc/voice_icon.png new file mode 100644 index 0000000..85e366d Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/poc/voice_icon.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/poc/voice_icon_rt.png b/s4e-pressure-anim/assets/walkie_talkie/poc/voice_icon_rt.png new file mode 100644 index 0000000..501320a Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/poc/voice_icon_rt.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/speak.png b/s4e-pressure-anim/assets/walkie_talkie/speak.png new file mode 100644 index 0000000..1b715e3 Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/speak.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/walkie_talkie.png b/s4e-pressure-anim/assets/walkie_talkie/walkie_talkie.png new file mode 100644 index 0000000..9a611a9 Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/walkie_talkie.png differ diff --git a/s4e-pressure-anim/assets/walkie_talkie/walkie_toast_icon.png b/s4e-pressure-anim/assets/walkie_talkie/walkie_toast_icon.png new file mode 100644 index 0000000..81f4e3f Binary files /dev/null and b/s4e-pressure-anim/assets/walkie_talkie/walkie_toast_icon.png differ diff --git a/s4e-pressure-anim/blob/offsets.json b/s4e-pressure-anim/blob/offsets.json new file mode 100644 index 0000000..e3a4de6 --- /dev/null +++ b/s4e-pressure-anim/blob/offsets.json @@ -0,0 +1,710 @@ +{ + "magic_len": 15, + "magic_rb_len": 15, + "records": [ + { + "name": "canary_superblock", + "rel": 0, + "target": 0, + "length": 32 + }, + { + "name": "003_Measuring4_4", + "rel": 32, + "target": 121341952, + "length": 5078 + }, + { + "name": "004_Measuring5_5", + "rel": 5110, + "target": 119374336, + "length": 6327 + }, + { + "name": "005_Measuring6_6", + "rel": 11437, + "target": 117413376, + "length": 8436 + }, + { + "name": "006_Measuring7_7", + "rel": 19873, + "target": 115506688, + "length": 11785 + }, + { + "name": "007_Measuring8_8", + "rel": 31658, + "target": 113678336, + "length": 17510 + }, + { + "name": "008_Measuring9_9", + "rel": 49168, + "target": 112016384, + "length": 23011 + }, + { + "name": "009_Measuring10_10", + "rel": 72179, + "target": 126875136, + "length": 31377 + }, + { + "name": "010_Measuring11_11", + "rel": 103556, + "target": 126014464, + "length": 43104 + }, + { + "name": "011_Measuring12_12", + "rel": 146660, + "target": 125605888, + "length": 54415 + }, + { + "name": "012_Measuring13_13", + "rel": 201075, + "target": 125539840, + "length": 65693 + }, + { + "name": "013_Measuring14_14", + "rel": 266768, + "target": 125463552, + "length": 75926 + }, + { + "name": "014_Measuring15_15", + "rel": 342694, + "target": 125372928, + "length": 90514 + }, + { + "name": "015_Measuring16_16", + "rel": 433208, + "target": 125251584, + "length": 120902 + }, + { + "name": "016_Measuring17_17", + "rel": 554110, + "target": 125123584, + "length": 127580 + }, + { + "name": "017_Measuring18_18", + "rel": 681690, + "target": 124991488, + "length": 131691 + }, + { + "name": "018_Measuring19_19", + "rel": 813381, + "target": 124854784, + "length": 136658 + }, + { + "name": "019_Measuring20_20", + "rel": 950039, + "target": 124708352, + "length": 141610 + }, + { + "name": "020_Measuring21_21", + "rel": 1091649, + "target": 124561920, + "length": 145939 + }, + { + "name": "021_Measuring22_22", + "rel": 1237588, + "target": 124407296, + "length": 154315 + }, + { + "name": "022_Measuring23_23", + "rel": 1391903, + "target": 124248064, + "length": 158922 + }, + { + "name": "023_Measuring24_24", + "rel": 1550825, + "target": 124083712, + "length": 164011 + }, + { + "name": "024_Measuring25_25", + "rel": 1714836, + "target": 123916288, + "length": 166983 + }, + { + "name": "025_Measuring26_26", + "rel": 1881819, + "target": 123746816, + "length": 169431 + }, + { + "name": "026_Measuring27_27", + "rel": 2051250, + "target": 123574784, + "length": 171709 + }, + { + "name": "027_Measuring28_28", + "rel": 2222959, + "target": 123400704, + "length": 173905 + }, + { + "name": "028_Measuring29_29", + "rel": 2396864, + "target": 123225088, + "length": 175146 + }, + { + "name": "029_Measuring30_30", + "rel": 2572010, + "target": 123042816, + "length": 176699 + }, + { + "name": "030_Measuring31_31", + "rel": 2748709, + "target": 122864128, + "length": 178248 + }, + { + "name": "031_Measuring32_32", + "rel": 2926957, + "target": 122683392, + "length": 180668 + }, + { + "name": "032_Measuring33_33", + "rel": 3107625, + "target": 122497024, + "length": 186322 + }, + { + "name": "033_Measuring34_34", + "rel": 3293947, + "target": 122308608, + "length": 188260 + }, + { + "name": "034_Measuring35_35", + "rel": 3482207, + "target": 122118656, + "length": 189862 + }, + { + "name": "035_Measuring36_36", + "rel": 3672069, + "target": 121927680, + "length": 190691 + }, + { + "name": "036_Measuring37_37", + "rel": 3862760, + "target": 121735680, + "length": 191837 + }, + { + "name": "037_Measuring38_38", + "rel": 4054597, + "target": 121542144, + "length": 193189 + }, + { + "name": "038_Measuring39_39", + "rel": 4247786, + "target": 121347072, + "length": 194939 + }, + { + "name": "039_Measuring40_40", + "rel": 4442725, + "target": 121146368, + "length": 195239 + }, + { + "name": "040_Measuring41_41", + "rel": 4637964, + "target": 120950272, + "length": 195793 + }, + { + "name": "041_Measuring42_42", + "rel": 4833757, + "target": 120754176, + "length": 196046 + }, + { + "name": "042_Measuring43_43", + "rel": 5029803, + "target": 120558080, + "length": 196021 + }, + { + "name": "043_Measuring44_44", + "rel": 5225824, + "target": 120361472, + "length": 196086 + }, + { + "name": "044_Measuring45_45", + "rel": 5421910, + "target": 120165376, + "length": 195652 + }, + { + "name": "045_Measuring46_46", + "rel": 5617562, + "target": 119969280, + "length": 195974 + }, + { + "name": "046_Measuring47_47", + "rel": 5813536, + "target": 119773184, + "length": 195999 + }, + { + "name": "047_Measuring48_48", + "rel": 6009535, + "target": 119577088, + "length": 195847 + }, + { + "name": "048_Measuring49_49", + "rel": 6205382, + "target": 119380992, + "length": 195899 + }, + { + "name": "049_Measuring50_50", + "rel": 6401281, + "target": 119177728, + "length": 196154 + }, + { + "name": "050_Measuring51_51", + "rel": 6597435, + "target": 118981120, + "length": 196246 + }, + { + "name": "051_Measuring52_52", + "rel": 6793681, + "target": 118785024, + "length": 196042 + }, + { + "name": "052_Measuring53_53", + "rel": 6989723, + "target": 118588928, + "length": 195985 + }, + { + "name": "053_Measuring54_54", + "rel": 7185708, + "target": 118392320, + "length": 196538 + }, + { + "name": "054_Measuring55_55", + "rel": 7382246, + "target": 118198272, + "length": 193546 + }, + { + "name": "055_Measuring56_56", + "rel": 7575792, + "target": 118004224, + "length": 193984 + }, + { + "name": "056_Measuring57_57", + "rel": 7769776, + "target": 117810176, + "length": 193954 + }, + { + "name": "057_Measuring58_58", + "rel": 7963730, + "target": 117616128, + "length": 193929 + }, + { + "name": "058_Measuring59_59", + "rel": 8157659, + "target": 117422080, + "length": 193761 + }, + { + "name": "059_Measuring60_60", + "rel": 8351420, + "target": 117218816, + "length": 194029 + }, + { + "name": "060_Measuring61_61", + "rel": 8545449, + "target": 117029376, + "length": 189089 + }, + { + "name": "061_Measuring62_62", + "rel": 8734538, + "target": 116838400, + "length": 190474 + }, + { + "name": "062_Measuring63_63", + "rel": 8925012, + "target": 116647424, + "length": 190748 + }, + { + "name": "063_Measuring64_64", + "rel": 9115760, + "target": 116455936, + "length": 190975 + }, + { + "name": "064_Measuring65_65", + "rel": 9306735, + "target": 116263424, + "length": 192023 + }, + { + "name": "065_Measuring66_66", + "rel": 9498758, + "target": 116071424, + "length": 191903 + }, + { + "name": "066_Measuring67_67", + "rel": 9690661, + "target": 115888640, + "length": 182358 + }, + { + "name": "067_Measuring68_68", + "rel": 9873019, + "target": 115704320, + "length": 183906 + }, + { + "name": "068_Measuring69_69", + "rel": 10056925, + "target": 115518976, + "length": 185033 + }, + { + "name": "069_Measuring70_70", + "rel": 10241958, + "target": 115321856, + "length": 184662 + }, + { + "name": "070_Measuring71_71", + "rel": 10426620, + "target": 115136512, + "length": 184877 + }, + { + "name": "071_Measuring72_72", + "rel": 10611497, + "target": 114951680, + "length": 184574 + }, + { + "name": "072_Measuring73_73", + "rel": 10796071, + "target": 114770432, + "length": 180773 + }, + { + "name": "073_Measuring74_74", + "rel": 10976844, + "target": 114589696, + "length": 180628 + }, + { + "name": "074_Measuring75_75", + "rel": 11157472, + "target": 114409472, + "length": 179919 + }, + { + "name": "075_Measuring76_76", + "rel": 11337391, + "target": 114228736, + "length": 180517 + }, + { + "name": "076_Measuring77_77", + "rel": 11517908, + "target": 114048000, + "length": 180677 + }, + { + "name": "077_Measuring78_78", + "rel": 11698585, + "target": 113867264, + "length": 180537 + }, + { + "name": "078_Measuring79_79", + "rel": 11879122, + "target": 113696256, + "length": 170518 + }, + { + "name": "079_Measuring80_80", + "rel": 12049640, + "target": 113507840, + "length": 170059 + }, + { + "name": "080_Measuring81_81", + "rel": 12219699, + "target": 113336320, + "length": 171247 + }, + { + "name": "081_Measuring82_82", + "rel": 12390946, + "target": 113165312, + "length": 170939 + }, + { + "name": "082_Measuring83_83", + "rel": 12561885, + "target": 112994816, + "length": 170193 + }, + { + "name": "083_Measuring84_84", + "rel": 12732078, + "target": 112840704, + "length": 153998 + }, + { + "name": "084_Measuring85_85", + "rel": 12886076, + "target": 112683520, + "length": 157044 + }, + { + "name": "085_Measuring86_86", + "rel": 13043120, + "target": 112524800, + "length": 158180 + }, + { + "name": "086_Measuring87_87", + "rel": 13201300, + "target": 112364544, + "length": 160025 + }, + { + "name": "087_Measuring88_88", + "rel": 13361325, + "target": 112203264, + "length": 160963 + }, + { + "name": "088_Measuring89_89", + "rel": 13522288, + "target": 112039936, + "length": 163153 + }, + { + "name": "089_Measuring90_90", + "rel": 13685441, + "target": 111863808, + "length": 152329 + }, + { + "name": "090_Measuring91_91", + "rel": 13837770, + "target": 111712256, + "length": 151129 + }, + { + "name": "091_Measuring92_92", + "rel": 13988899, + "target": 111562240, + "length": 149917 + }, + { + "name": "092_Measuring93_93", + "rel": 14138816, + "target": 111413248, + "length": 148930 + }, + { + "name": "093_Measuring94_94", + "rel": 14287746, + "target": 111264768, + "length": 148040 + }, + { + "name": "094_Measuring95_95", + "rel": 14435786, + "target": 111118848, + "length": 145756 + }, + { + "name": "095_Measuring96_96", + "rel": 14581542, + "target": 110998016, + "length": 120568 + }, + { + "name": "096_Measuring97_97", + "rel": 14702110, + "target": 110875648, + "length": 121902 + }, + { + "name": "097_Measuring98_98", + "rel": 14824012, + "target": 110754304, + "length": 121294 + }, + { + "name": "098_Measuring99_99", + "rel": 14945306, + "target": 110638592, + "length": 115589 + }, + { + "name": "099_Measuring100_100", + "rel": 15060895, + "target": 126764032, + "length": 110723 + }, + { + "name": "100_Measuring101_101", + "rel": 15171618, + "target": 126656512, + "length": 107240 + }, + { + "name": "101_Measuring102_102", + "rel": 15278858, + "target": 126569984, + "length": 86055 + }, + { + "name": "102_Measuring103_103", + "rel": 15364913, + "target": 126484480, + "length": 85118 + }, + { + "name": "103_Measuring104_104", + "rel": 15450031, + "target": 126400000, + "length": 84012 + }, + { + "name": "104_Measuring105_105", + "rel": 15534043, + "target": 126318592, + "length": 81068 + }, + { + "name": "105_Measuring106_106", + "rel": 15615111, + "target": 126240256, + "length": 77823 + }, + { + "name": "106_Measuring107_107", + "rel": 15692934, + "target": 126164480, + "length": 75410 + }, + { + "name": "107_Measuring108_108", + "rel": 15768344, + "target": 126111744, + "length": 52464 + }, + { + "name": "108_Measuring109_109", + "rel": 15820808, + "target": 126057984, + "length": 53592 + }, + { + "name": "109_Measuring110_110", + "rel": 15874400, + "target": 125961728, + "length": 52559 + }, + { + "name": "110_Measuring111_111", + "rel": 15926959, + "target": 125914624, + "length": 46569 + }, + { + "name": "111_Measuring112_112", + "rel": 15973528, + "target": 125874176, + "length": 40243 + }, + { + "name": "112_Measuring113_113", + "rel": 16013771, + "target": 125828608, + "length": 45031 + }, + { + "name": "113_Measuring114_114", + "rel": 16058802, + "target": 125788160, + "length": 40136 + }, + { + "name": "114_Measuring115_115", + "rel": 16098938, + "target": 125754880, + "length": 32924 + }, + { + "name": "115_Measuring116_116", + "rel": 16131862, + "target": 125727744, + "length": 27031 + }, + { + "name": "116_Measuring117_117", + "rel": 16158893, + "target": 125703168, + "length": 24325 + }, + { + "name": "117_Measuring118_118", + "rel": 16183218, + "target": 125680128, + "length": 22811 + }, + { + "name": "118_Measuring119_119", + "rel": 16206029, + "target": 125660672, + "length": 19025 + } + ], + "pay_bytes": 16225069, + "rb_bytes": 16225069 +} \ No newline at end of file diff --git a/s4e-pressure-anim/blob/payload.bin b/s4e-pressure-anim/blob/payload.bin new file mode 100644 index 0000000..fbeab6c Binary files /dev/null and b/s4e-pressure-anim/blob/payload.bin differ diff --git a/s4e-pressure-anim/blob/rollback.bin b/s4e-pressure-anim/blob/rollback.bin new file mode 100644 index 0000000..c64772b Binary files /dev/null and b/s4e-pressure-anim/blob/rollback.bin differ diff --git a/s4e-pressure-anim/dist/S4PAnim.face b/s4e-pressure-anim/dist/S4PAnim.face new file mode 100644 index 0000000..64661e0 Binary files /dev/null and b/s4e-pressure-anim/dist/S4PAnim.face differ diff --git a/s4e-pressure-anim/dist/S4PAnimRestore.face b/s4e-pressure-anim/dist/S4PAnimRestore.face new file mode 100644 index 0000000..a0ddb91 Binary files /dev/null and b/s4e-pressure-anim/dist/S4PAnimRestore.face differ diff --git a/s4e-pressure-anim/docs/anim_before_after.png b/s4e-pressure-anim/docs/anim_before_after.png new file mode 100644 index 0000000..6abcea3 Binary files /dev/null and b/s4e-pressure-anim/docs/anim_before_after.png differ diff --git a/s4e-pressure-anim/docs/anim_color_v5_vs_v6.png b/s4e-pressure-anim/docs/anim_color_v5_vs_v6.png new file mode 100644 index 0000000..0eb9443 Binary files /dev/null and b/s4e-pressure-anim/docs/anim_color_v5_vs_v6.png differ diff --git a/s4e-pressure-anim/docs/anim_first_frames.png b/s4e-pressure-anim/docs/anim_first_frames.png new file mode 100644 index 0000000..b90263b Binary files /dev/null and b/s4e-pressure-anim/docs/anim_first_frames.png differ diff --git a/s4e-pressure-anim/docs/anim_new.mp4 b/s4e-pressure-anim/docs/anim_new.mp4 new file mode 100644 index 0000000..ad93193 Binary files /dev/null and b/s4e-pressure-anim/docs/anim_new.mp4 differ diff --git a/s4e-pressure-anim/docs/anim_scale_profile.png b/s4e-pressure-anim/docs/anim_scale_profile.png new file mode 100644 index 0000000..491f423 Binary files /dev/null and b/s4e-pressure-anim/docs/anim_scale_profile.png differ diff --git a/s4e-pressure-anim/flash.sh b/s4e-pressure-anim/flash.sh new file mode 100644 index 0000000..53a9fb2 --- /dev/null +++ b/s4e-pressure-anim/flash.sh @@ -0,0 +1,609 @@ +#!/bin/sh +# S5 41mm 压力检测动画 -> S4 eSIM(只写负载) +# 把 flash.sh 与 payload/ 放到同一个目录,例如 /data/s4panim/ +# 然后: sh /data/s4panim/flash.sh 最后: reboot +# 还原用 restore.sh 或安装还原盘 S4PAnimRestore.face + +DEV=/dev/app +BASE=/data/s4panim/payload + +echo "target check" +ls -l $DEV + +echo "== 000_Measuring1_1 == +dd if=$BASE/000_000_Measuring1_1.bin of=$DEV bs=1 seek=126906880 count=4485 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=126906880 count=4485 +cmp /tmp/chk.bin $BASE/000_000_Measuring1_1.bin + +echo "== 001_Measuring2_2 == +dd if=$BASE/001_001_Measuring2_2.bin of=$DEV bs=1 seek=124850176 count=4530 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=124850176 count=4530 +cmp /tmp/chk.bin $BASE/001_001_Measuring2_2.bin + +echo "== 002_Measuring3_3 == +dd if=$BASE/002_002_Measuring3_3.bin of=$DEV bs=1 seek=123219968 count=4695 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=123219968 count=4695 +cmp /tmp/chk.bin $BASE/002_002_Measuring3_3.bin + +echo "== 003_Measuring4_4 == +dd if=$BASE/003_003_Measuring4_4.bin of=$DEV bs=1 seek=121341952 count=5078 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=121341952 count=5078 +cmp /tmp/chk.bin $BASE/003_003_Measuring4_4.bin + +echo "== 004_Measuring5_5 == +dd if=$BASE/004_004_Measuring5_5.bin of=$DEV bs=1 seek=119374336 count=6327 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=119374336 count=6327 +cmp /tmp/chk.bin $BASE/004_004_Measuring5_5.bin + +echo "== 005_Measuring6_6 == +dd if=$BASE/005_005_Measuring6_6.bin of=$DEV bs=4 seek=29353344 count=2109 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=29353344 count=2109 +cmp /tmp/chk.bin $BASE/005_005_Measuring6_6.bin + +echo "== 006_Measuring7_7 == +dd if=$BASE/006_006_Measuring7_7.bin of=$DEV bs=1 seek=115506688 count=11785 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=115506688 count=11785 +cmp /tmp/chk.bin $BASE/006_006_Measuring7_7.bin + +echo "== 007_Measuring8_8 == +dd if=$BASE/007_007_Measuring8_8.bin of=$DEV bs=1 seek=113678336 count=17510 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=113678336 count=17510 +cmp /tmp/chk.bin $BASE/007_007_Measuring8_8.bin + +echo "== 008_Measuring9_9 == +dd if=$BASE/008_008_Measuring9_9.bin of=$DEV bs=1 seek=112016384 count=23011 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=112016384 count=23011 +cmp /tmp/chk.bin $BASE/008_008_Measuring9_9.bin + +echo "== 009_Measuring10_10 == +dd if=$BASE/009_009_Measuring10_10.bin of=$DEV bs=1 seek=126875136 count=31377 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=126875136 count=31377 +cmp /tmp/chk.bin $BASE/009_009_Measuring10_10.bin + +echo "== 010_Measuring11_11 == +dd if=$BASE/010_010_Measuring11_11.bin of=$DEV bs=4 seek=31503616 count=10776 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=31503616 count=10776 +cmp /tmp/chk.bin $BASE/010_010_Measuring11_11.bin + +echo "== 011_Measuring12_12 == +dd if=$BASE/011_011_Measuring12_12.bin of=$DEV bs=1 seek=125605888 count=54415 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=125605888 count=54415 +cmp /tmp/chk.bin $BASE/011_011_Measuring12_12.bin + +echo "== 012_Measuring13_13 == +dd if=$BASE/012_012_Measuring13_13.bin of=$DEV bs=1 seek=125539840 count=65693 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=125539840 count=65693 +cmp /tmp/chk.bin $BASE/012_012_Measuring13_13.bin + +echo "== 013_Measuring14_14 == +dd if=$BASE/013_013_Measuring14_14.bin of=$DEV bs=1 seek=125463552 count=75926 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=125463552 count=75926 +cmp /tmp/chk.bin $BASE/013_013_Measuring14_14.bin + +echo "== 014_Measuring15_15 == +dd if=$BASE/014_014_Measuring15_15.bin of=$DEV bs=1 seek=125372928 count=90514 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=125372928 count=90514 +cmp /tmp/chk.bin $BASE/014_014_Measuring15_15.bin + +echo "== 015_Measuring16_16 == +dd if=$BASE/015_015_Measuring16_16.bin of=$DEV bs=1 seek=125251584 count=120902 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=125251584 count=120902 +cmp /tmp/chk.bin $BASE/015_015_Measuring16_16.bin + +echo "== 016_Measuring17_17 == +dd if=$BASE/016_016_Measuring17_17.bin of=$DEV bs=4 seek=31280896 count=31895 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=31280896 count=31895 +cmp /tmp/chk.bin $BASE/016_016_Measuring17_17.bin + +echo "== 017_Measuring18_18 == +dd if=$BASE/017_017_Measuring18_18.bin of=$DEV bs=1 seek=124991488 count=131691 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=124991488 count=131691 +cmp /tmp/chk.bin $BASE/017_017_Measuring18_18.bin + +echo "== 018_Measuring19_19 == +dd if=$BASE/018_018_Measuring19_19.bin of=$DEV bs=1 seek=124854784 count=136658 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=124854784 count=136658 +cmp /tmp/chk.bin $BASE/018_018_Measuring19_19.bin + +echo "== 019_Measuring20_20 == +dd if=$BASE/019_019_Measuring20_20.bin of=$DEV bs=1 seek=124708352 count=141610 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=124708352 count=141610 +cmp /tmp/chk.bin $BASE/019_019_Measuring20_20.bin + +echo "== 020_Measuring21_21 == +dd if=$BASE/020_020_Measuring21_21.bin of=$DEV bs=1 seek=124561920 count=145939 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=124561920 count=145939 +cmp /tmp/chk.bin $BASE/020_020_Measuring21_21.bin + +echo "== 021_Measuring22_22 == +dd if=$BASE/021_021_Measuring22_22.bin of=$DEV bs=1 seek=124407296 count=154315 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=124407296 count=154315 +cmp /tmp/chk.bin $BASE/021_021_Measuring22_22.bin + +echo "== 022_Measuring23_23 == +dd if=$BASE/022_022_Measuring23_23.bin of=$DEV bs=1 seek=124248064 count=158922 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=124248064 count=158922 +cmp /tmp/chk.bin $BASE/022_022_Measuring23_23.bin + +echo "== 023_Measuring24_24 == +dd if=$BASE/023_023_Measuring24_24.bin of=$DEV bs=1 seek=124083712 count=164011 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=124083712 count=164011 +cmp /tmp/chk.bin $BASE/023_023_Measuring24_24.bin + +echo "== 024_Measuring25_25 == +dd if=$BASE/024_024_Measuring25_25.bin of=$DEV bs=1 seek=123916288 count=166983 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=123916288 count=166983 +cmp /tmp/chk.bin $BASE/024_024_Measuring25_25.bin + +echo "== 025_Measuring26_26 == +dd if=$BASE/025_025_Measuring26_26.bin of=$DEV bs=1 seek=123746816 count=169431 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=123746816 count=169431 +cmp /tmp/chk.bin $BASE/025_025_Measuring26_26.bin + +echo "== 026_Measuring27_27 == +dd if=$BASE/026_026_Measuring27_27.bin of=$DEV bs=1 seek=123574784 count=171709 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=123574784 count=171709 +cmp /tmp/chk.bin $BASE/026_026_Measuring27_27.bin + +echo "== 027_Measuring28_28 == +dd if=$BASE/027_027_Measuring28_28.bin of=$DEV bs=1 seek=123400704 count=173905 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=123400704 count=173905 +cmp /tmp/chk.bin $BASE/027_027_Measuring28_28.bin + +echo "== 028_Measuring29_29 == +dd if=$BASE/028_028_Measuring29_29.bin of=$DEV bs=1 seek=123225088 count=175146 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=123225088 count=175146 +cmp /tmp/chk.bin $BASE/028_028_Measuring29_29.bin + +echo "== 029_Measuring30_30 == +dd if=$BASE/029_029_Measuring30_30.bin of=$DEV bs=1 seek=123042816 count=176699 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=123042816 count=176699 +cmp /tmp/chk.bin $BASE/029_029_Measuring30_30.bin + +echo "== 030_Measuring31_31 == +dd if=$BASE/030_030_Measuring31_31.bin of=$DEV bs=4 seek=30716032 count=44562 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=30716032 count=44562 +cmp /tmp/chk.bin $BASE/030_030_Measuring31_31.bin + +echo "== 031_Measuring32_32 == +dd if=$BASE/031_031_Measuring32_32.bin of=$DEV bs=4 seek=30670848 count=45167 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=30670848 count=45167 +cmp /tmp/chk.bin $BASE/031_031_Measuring32_32.bin + +echo "== 032_Measuring33_33 == +dd if=$BASE/032_032_Measuring33_33.bin of=$DEV bs=1 seek=122497024 count=186322 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=122497024 count=186322 +cmp /tmp/chk.bin $BASE/032_032_Measuring33_33.bin + +echo "== 033_Measuring34_34 == +dd if=$BASE/033_033_Measuring34_34.bin of=$DEV bs=4 seek=30577152 count=47065 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=30577152 count=47065 +cmp /tmp/chk.bin $BASE/033_033_Measuring34_34.bin + +echo "== 034_Measuring35_35 == +dd if=$BASE/034_034_Measuring35_35.bin of=$DEV bs=1 seek=122118656 count=189862 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=122118656 count=189862 +cmp /tmp/chk.bin $BASE/034_034_Measuring35_35.bin + +echo "== 035_Measuring36_36 == +dd if=$BASE/035_035_Measuring36_36.bin of=$DEV bs=1 seek=121927680 count=190691 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=121927680 count=190691 +cmp /tmp/chk.bin $BASE/035_035_Measuring36_36.bin + +echo "== 036_Measuring37_37 == +dd if=$BASE/036_036_Measuring37_37.bin of=$DEV bs=1 seek=121735680 count=191837 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=121735680 count=191837 +cmp /tmp/chk.bin $BASE/036_036_Measuring37_37.bin + +echo "== 037_Measuring38_38 == +dd if=$BASE/037_037_Measuring38_38.bin of=$DEV bs=1 seek=121542144 count=193189 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=121542144 count=193189 +cmp /tmp/chk.bin $BASE/037_037_Measuring38_38.bin + +echo "== 038_Measuring39_39 == +dd if=$BASE/038_038_Measuring39_39.bin of=$DEV bs=1 seek=121347072 count=194939 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=121347072 count=194939 +cmp /tmp/chk.bin $BASE/038_038_Measuring39_39.bin + +echo "== 039_Measuring40_40 == +dd if=$BASE/039_039_Measuring40_40.bin of=$DEV bs=1 seek=121146368 count=195239 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=121146368 count=195239 +cmp /tmp/chk.bin $BASE/039_039_Measuring40_40.bin + +echo "== 040_Measuring41_41 == +dd if=$BASE/040_040_Measuring41_41.bin of=$DEV bs=1 seek=120950272 count=195793 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=120950272 count=195793 +cmp /tmp/chk.bin $BASE/040_040_Measuring41_41.bin + +echo "== 041_Measuring42_42 == +dd if=$BASE/041_041_Measuring42_42.bin of=$DEV bs=1 seek=120754176 count=196046 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=120754176 count=196046 +cmp /tmp/chk.bin $BASE/041_041_Measuring42_42.bin + +echo "== 042_Measuring43_43 == +dd if=$BASE/042_042_Measuring43_43.bin of=$DEV bs=1 seek=120558080 count=196021 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=120558080 count=196021 +cmp /tmp/chk.bin $BASE/042_042_Measuring43_43.bin + +echo "== 043_Measuring44_44 == +dd if=$BASE/043_043_Measuring44_44.bin of=$DEV bs=1 seek=120361472 count=196086 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=120361472 count=196086 +cmp /tmp/chk.bin $BASE/043_043_Measuring44_44.bin + +echo "== 044_Measuring45_45 == +dd if=$BASE/044_044_Measuring45_45.bin of=$DEV bs=4 seek=30041344 count=48913 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=30041344 count=48913 +cmp /tmp/chk.bin $BASE/044_044_Measuring45_45.bin + +echo "== 045_Measuring46_46 == +dd if=$BASE/045_045_Measuring46_46.bin of=$DEV bs=1 seek=119969280 count=195974 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=119969280 count=195974 +cmp /tmp/chk.bin $BASE/045_045_Measuring46_46.bin + +echo "== 046_Measuring47_47 == +dd if=$BASE/046_046_Measuring47_47.bin of=$DEV bs=1 seek=119773184 count=195999 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=119773184 count=195999 +cmp /tmp/chk.bin $BASE/046_046_Measuring47_47.bin + +echo "== 047_Measuring48_48 == +dd if=$BASE/047_047_Measuring48_48.bin of=$DEV bs=1 seek=119577088 count=195847 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=119577088 count=195847 +cmp /tmp/chk.bin $BASE/047_047_Measuring48_48.bin + +echo "== 048_Measuring49_49 == +dd if=$BASE/048_048_Measuring49_49.bin of=$DEV bs=1 seek=119380992 count=195899 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=119380992 count=195899 +cmp /tmp/chk.bin $BASE/048_048_Measuring49_49.bin + +echo "== 049_Measuring50_50 == +dd if=$BASE/049_049_Measuring50_50.bin of=$DEV bs=1 seek=119177728 count=196154 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=119177728 count=196154 +cmp /tmp/chk.bin $BASE/049_049_Measuring50_50.bin + +echo "== 050_Measuring51_51 == +dd if=$BASE/050_050_Measuring51_51.bin of=$DEV bs=1 seek=118981120 count=196246 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=118981120 count=196246 +cmp /tmp/chk.bin $BASE/050_050_Measuring51_51.bin + +echo "== 051_Measuring52_52 == +dd if=$BASE/051_051_Measuring52_52.bin of=$DEV bs=1 seek=118785024 count=196042 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=118785024 count=196042 +cmp /tmp/chk.bin $BASE/051_051_Measuring52_52.bin + +echo "== 052_Measuring53_53 == +dd if=$BASE/052_052_Measuring53_53.bin of=$DEV bs=1 seek=118588928 count=195985 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=118588928 count=195985 +cmp /tmp/chk.bin $BASE/052_052_Measuring53_53.bin + +echo "== 053_Measuring54_54 == +dd if=$BASE/053_053_Measuring54_54.bin of=$DEV bs=1 seek=118392320 count=196538 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=118392320 count=196538 +cmp /tmp/chk.bin $BASE/053_053_Measuring54_54.bin + +echo "== 054_Measuring55_55 == +dd if=$BASE/054_054_Measuring55_55.bin of=$DEV bs=1 seek=118198272 count=193546 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=118198272 count=193546 +cmp /tmp/chk.bin $BASE/054_054_Measuring55_55.bin + +echo "== 055_Measuring56_56 == +dd if=$BASE/055_055_Measuring56_56.bin of=$DEV bs=4 seek=29501056 count=48496 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=29501056 count=48496 +cmp /tmp/chk.bin $BASE/055_055_Measuring56_56.bin + +echo "== 056_Measuring57_57 == +dd if=$BASE/056_056_Measuring57_57.bin of=$DEV bs=1 seek=117810176 count=193954 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=117810176 count=193954 +cmp /tmp/chk.bin $BASE/056_056_Measuring57_57.bin + +echo "== 057_Measuring58_58 == +dd if=$BASE/057_057_Measuring58_58.bin of=$DEV bs=1 seek=117616128 count=193929 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=117616128 count=193929 +cmp /tmp/chk.bin $BASE/057_057_Measuring58_58.bin + +echo "== 058_Measuring59_59 == +dd if=$BASE/058_058_Measuring59_59.bin of=$DEV bs=1 seek=117422080 count=193761 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=117422080 count=193761 +cmp /tmp/chk.bin $BASE/058_058_Measuring59_59.bin + +echo "== 059_Measuring60_60 == +dd if=$BASE/059_059_Measuring60_60.bin of=$DEV bs=1 seek=117218816 count=194029 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=117218816 count=194029 +cmp /tmp/chk.bin $BASE/059_059_Measuring60_60.bin + +echo "== 060_Measuring61_61 == +dd if=$BASE/060_060_Measuring61_61.bin of=$DEV bs=1 seek=117029376 count=189089 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=117029376 count=189089 +cmp /tmp/chk.bin $BASE/060_060_Measuring61_61.bin + +echo "== 061_Measuring62_62 == +dd if=$BASE/061_061_Measuring62_62.bin of=$DEV bs=1 seek=116838400 count=190474 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=116838400 count=190474 +cmp /tmp/chk.bin $BASE/061_061_Measuring62_62.bin + +echo "== 062_Measuring63_63 == +dd if=$BASE/062_062_Measuring63_63.bin of=$DEV bs=4 seek=29161856 count=47687 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=29161856 count=47687 +cmp /tmp/chk.bin $BASE/062_062_Measuring63_63.bin + +echo "== 063_Measuring64_64 == +dd if=$BASE/063_063_Measuring64_64.bin of=$DEV bs=1 seek=116455936 count=190975 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=116455936 count=190975 +cmp /tmp/chk.bin $BASE/063_063_Measuring64_64.bin + +echo "== 064_Measuring65_65 == +dd if=$BASE/064_064_Measuring65_65.bin of=$DEV bs=1 seek=116263424 count=192023 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=116263424 count=192023 +cmp /tmp/chk.bin $BASE/064_064_Measuring65_65.bin + +echo "== 065_Measuring66_66 == +dd if=$BASE/065_065_Measuring66_66.bin of=$DEV bs=1 seek=116071424 count=191903 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=116071424 count=191903 +cmp /tmp/chk.bin $BASE/065_065_Measuring66_66.bin + +echo "== 066_Measuring67_67 == +dd if=$BASE/066_066_Measuring67_67.bin of=$DEV bs=1 seek=115888640 count=182358 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=115888640 count=182358 +cmp /tmp/chk.bin $BASE/066_066_Measuring67_67.bin + +echo "== 067_Measuring68_68 == +dd if=$BASE/067_067_Measuring68_68.bin of=$DEV bs=1 seek=115704320 count=183906 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=115704320 count=183906 +cmp /tmp/chk.bin $BASE/067_067_Measuring68_68.bin + +echo "== 068_Measuring69_69 == +dd if=$BASE/068_068_Measuring69_69.bin of=$DEV bs=1 seek=115518976 count=185033 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=115518976 count=185033 +cmp /tmp/chk.bin $BASE/068_068_Measuring69_69.bin + +echo "== 069_Measuring70_70 == +dd if=$BASE/069_069_Measuring70_70.bin of=$DEV bs=1 seek=115321856 count=184662 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=115321856 count=184662 +cmp /tmp/chk.bin $BASE/069_069_Measuring70_70.bin + +echo "== 070_Measuring71_71 == +dd if=$BASE/070_070_Measuring71_71.bin of=$DEV bs=1 seek=115136512 count=184877 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=115136512 count=184877 +cmp /tmp/chk.bin $BASE/070_070_Measuring71_71.bin + +echo "== 071_Measuring72_72 == +dd if=$BASE/071_071_Measuring72_72.bin of=$DEV bs=1 seek=114951680 count=184574 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=114951680 count=184574 +cmp /tmp/chk.bin $BASE/071_071_Measuring72_72.bin + +echo "== 072_Measuring73_73 == +dd if=$BASE/072_072_Measuring73_73.bin of=$DEV bs=1 seek=114770432 count=180773 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=114770432 count=180773 +cmp /tmp/chk.bin $BASE/072_072_Measuring73_73.bin + +echo "== 073_Measuring74_74 == +dd if=$BASE/073_073_Measuring74_74.bin of=$DEV bs=4 seek=28647424 count=45157 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=28647424 count=45157 +cmp /tmp/chk.bin $BASE/073_073_Measuring74_74.bin + +echo "== 074_Measuring75_75 == +dd if=$BASE/074_074_Measuring75_75.bin of=$DEV bs=1 seek=114409472 count=179919 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=114409472 count=179919 +cmp /tmp/chk.bin $BASE/074_074_Measuring75_75.bin + +echo "== 075_Measuring76_76 == +dd if=$BASE/075_075_Measuring76_76.bin of=$DEV bs=1 seek=114228736 count=180517 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=114228736 count=180517 +cmp /tmp/chk.bin $BASE/075_075_Measuring76_76.bin + +echo "== 076_Measuring77_77 == +dd if=$BASE/076_076_Measuring77_77.bin of=$DEV bs=1 seek=114048000 count=180677 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=114048000 count=180677 +cmp /tmp/chk.bin $BASE/076_076_Measuring77_77.bin + +echo "== 077_Measuring78_78 == +dd if=$BASE/077_077_Measuring78_78.bin of=$DEV bs=1 seek=113867264 count=180537 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=113867264 count=180537 +cmp /tmp/chk.bin $BASE/077_077_Measuring78_78.bin + +echo "== 078_Measuring79_79 == +dd if=$BASE/078_078_Measuring79_79.bin of=$DEV bs=1 seek=113696256 count=170518 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=113696256 count=170518 +cmp /tmp/chk.bin $BASE/078_078_Measuring79_79.bin + +echo "== 079_Measuring80_80 == +dd if=$BASE/079_079_Measuring80_80.bin of=$DEV bs=1 seek=113507840 count=170059 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=113507840 count=170059 +cmp /tmp/chk.bin $BASE/079_079_Measuring80_80.bin + +echo "== 080_Measuring81_81 == +dd if=$BASE/080_080_Measuring81_81.bin of=$DEV bs=1 seek=113336320 count=171247 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=113336320 count=171247 +cmp /tmp/chk.bin $BASE/080_080_Measuring81_81.bin + +echo "== 081_Measuring82_82 == +dd if=$BASE/081_081_Measuring82_82.bin of=$DEV bs=1 seek=113165312 count=170939 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=113165312 count=170939 +cmp /tmp/chk.bin $BASE/081_081_Measuring82_82.bin + +echo "== 082_Measuring83_83 == +dd if=$BASE/082_082_Measuring83_83.bin of=$DEV bs=1 seek=112994816 count=170193 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=112994816 count=170193 +cmp /tmp/chk.bin $BASE/082_082_Measuring83_83.bin + +echo "== 083_Measuring84_84 == +dd if=$BASE/083_083_Measuring84_84.bin of=$DEV bs=1 seek=112840704 count=153998 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=112840704 count=153998 +cmp /tmp/chk.bin $BASE/083_083_Measuring84_84.bin + +echo "== 084_Measuring85_85 == +dd if=$BASE/084_084_Measuring85_85.bin of=$DEV bs=4 seek=28170880 count=39261 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=28170880 count=39261 +cmp /tmp/chk.bin $BASE/084_084_Measuring85_85.bin + +echo "== 085_Measuring86_86 == +dd if=$BASE/085_085_Measuring86_86.bin of=$DEV bs=4 seek=28131200 count=39545 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=28131200 count=39545 +cmp /tmp/chk.bin $BASE/085_085_Measuring86_86.bin + +echo "== 086_Measuring87_87 == +dd if=$BASE/086_086_Measuring87_87.bin of=$DEV bs=1 seek=112364544 count=160025 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=112364544 count=160025 +cmp /tmp/chk.bin $BASE/086_086_Measuring87_87.bin + +echo "== 087_Measuring88_88 == +dd if=$BASE/087_087_Measuring88_88.bin of=$DEV bs=1 seek=112203264 count=160963 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=112203264 count=160963 +cmp /tmp/chk.bin $BASE/087_087_Measuring88_88.bin + +echo "== 088_Measuring89_89 == +dd if=$BASE/088_088_Measuring89_89.bin of=$DEV bs=1 seek=112039936 count=163153 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=112039936 count=163153 +cmp /tmp/chk.bin $BASE/088_088_Measuring89_89.bin + +echo "== 089_Measuring90_90 == +dd if=$BASE/089_089_Measuring90_90.bin of=$DEV bs=1 seek=111863808 count=152329 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=111863808 count=152329 +cmp /tmp/chk.bin $BASE/089_089_Measuring90_90.bin + +echo "== 090_Measuring91_91 == +dd if=$BASE/090_090_Measuring91_91.bin of=$DEV bs=1 seek=111712256 count=151129 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=111712256 count=151129 +cmp /tmp/chk.bin $BASE/090_090_Measuring91_91.bin + +echo "== 091_Measuring92_92 == +dd if=$BASE/091_091_Measuring92_92.bin of=$DEV bs=1 seek=111562240 count=149917 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=111562240 count=149917 +cmp /tmp/chk.bin $BASE/091_091_Measuring92_92.bin + +echo "== 092_Measuring93_93 == +dd if=$BASE/092_092_Measuring93_93.bin of=$DEV bs=1 seek=111413248 count=148930 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=111413248 count=148930 +cmp /tmp/chk.bin $BASE/092_092_Measuring93_93.bin + +echo "== 093_Measuring94_94 == +dd if=$BASE/093_093_Measuring94_94.bin of=$DEV bs=4 seek=27816192 count=37010 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=27816192 count=37010 +cmp /tmp/chk.bin $BASE/093_093_Measuring94_94.bin + +echo "== 094_Measuring95_95 == +dd if=$BASE/094_094_Measuring95_95.bin of=$DEV bs=4 seek=27779712 count=36439 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=27779712 count=36439 +cmp /tmp/chk.bin $BASE/094_094_Measuring95_95.bin + +echo "== 095_Measuring96_96 == +dd if=$BASE/095_095_Measuring96_96.bin of=$DEV bs=4 seek=27749504 count=30142 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=27749504 count=30142 +cmp /tmp/chk.bin $BASE/095_095_Measuring96_96.bin + +echo "== 096_Measuring97_97 == +dd if=$BASE/096_096_Measuring97_97.bin of=$DEV bs=1 seek=110875648 count=121902 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=110875648 count=121902 +cmp /tmp/chk.bin $BASE/096_096_Measuring97_97.bin + +echo "== 097_Measuring98_98 == +dd if=$BASE/097_097_Measuring98_98.bin of=$DEV bs=1 seek=110754304 count=121294 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=110754304 count=121294 +cmp /tmp/chk.bin $BASE/097_097_Measuring98_98.bin + +echo "== 098_Measuring99_99 == +dd if=$BASE/098_098_Measuring99_99.bin of=$DEV bs=1 seek=110638592 count=115589 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=110638592 count=115589 +cmp /tmp/chk.bin $BASE/098_098_Measuring99_99.bin + +echo "== 099_Measuring100_100 == +dd if=$BASE/099_099_Measuring100_100.bin of=$DEV bs=1 seek=126764032 count=110723 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=126764032 count=110723 +cmp /tmp/chk.bin $BASE/099_099_Measuring100_100.bin + +echo "== 100_Measuring101_101 == +dd if=$BASE/100_100_Measuring101_101.bin of=$DEV bs=4 seek=31664128 count=26810 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=31664128 count=26810 +cmp /tmp/chk.bin $BASE/100_100_Measuring101_101.bin + +echo "== 101_Measuring102_102 == +dd if=$BASE/101_101_Measuring102_102.bin of=$DEV bs=1 seek=126569984 count=86055 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=126569984 count=86055 +cmp /tmp/chk.bin $BASE/101_101_Measuring102_102.bin + +echo "== 102_Measuring103_103 == +dd if=$BASE/102_102_Measuring103_103.bin of=$DEV bs=1 seek=126484480 count=85118 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=126484480 count=85118 +cmp /tmp/chk.bin $BASE/102_102_Measuring103_103.bin + +echo "== 103_Measuring104_104 == +dd if=$BASE/103_103_Measuring104_104.bin of=$DEV bs=4 seek=31600000 count=21003 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=31600000 count=21003 +cmp /tmp/chk.bin $BASE/103_103_Measuring104_104.bin + +echo "== 104_Measuring105_105 == +dd if=$BASE/104_104_Measuring105_105.bin of=$DEV bs=4 seek=31579648 count=20267 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=31579648 count=20267 +cmp /tmp/chk.bin $BASE/104_104_Measuring105_105.bin + +echo "== 105_Measuring106_106 == +dd if=$BASE/105_105_Measuring106_106.bin of=$DEV bs=1 seek=126240256 count=77823 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=126240256 count=77823 +cmp /tmp/chk.bin $BASE/105_105_Measuring106_106.bin + +echo "== 106_Measuring107_107 == +dd if=$BASE/106_106_Measuring107_107.bin of=$DEV bs=1 seek=126164480 count=75410 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=126164480 count=75410 +cmp /tmp/chk.bin $BASE/106_106_Measuring107_107.bin + +echo "== 107_Measuring108_108 == +dd if=$BASE/107_107_Measuring108_108.bin of=$DEV bs=4 seek=31527936 count=13116 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=31527936 count=13116 +cmp /tmp/chk.bin $BASE/107_107_Measuring108_108.bin + +echo "== 108_Measuring109_109 == +dd if=$BASE/108_108_Measuring109_109.bin of=$DEV bs=4 seek=31514496 count=13398 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=31514496 count=13398 +cmp /tmp/chk.bin $BASE/108_108_Measuring109_109.bin + +echo "== 109_Measuring110_110 == +dd if=$BASE/109_109_Measuring110_110.bin of=$DEV bs=1 seek=125961728 count=52559 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=125961728 count=52559 +cmp /tmp/chk.bin $BASE/109_109_Measuring110_110.bin + +echo "== 110_Measuring111_111 == +dd if=$BASE/110_110_Measuring111_111.bin of=$DEV bs=1 seek=125914624 count=46569 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=125914624 count=46569 +cmp /tmp/chk.bin $BASE/110_110_Measuring111_111.bin + +echo "== 111_Measuring112_112 == +dd if=$BASE/111_111_Measuring112_112.bin of=$DEV bs=1 seek=125874176 count=40243 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=125874176 count=40243 +cmp /tmp/chk.bin $BASE/111_111_Measuring112_112.bin + +echo "== 112_Measuring113_113 == +dd if=$BASE/112_112_Measuring113_113.bin of=$DEV bs=1 seek=125828608 count=45031 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=125828608 count=45031 +cmp /tmp/chk.bin $BASE/112_112_Measuring113_113.bin + +echo "== 113_Measuring114_114 == +dd if=$BASE/113_113_Measuring114_114.bin of=$DEV bs=4 seek=31447040 count=10034 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=31447040 count=10034 +cmp /tmp/chk.bin $BASE/113_113_Measuring114_114.bin + +echo "== 114_Measuring115_115 == +dd if=$BASE/114_114_Measuring115_115.bin of=$DEV bs=4 seek=31438720 count=8231 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=31438720 count=8231 +cmp /tmp/chk.bin $BASE/114_114_Measuring115_115.bin + +echo "== 115_Measuring116_116 == +dd if=$BASE/115_115_Measuring116_116.bin of=$DEV bs=1 seek=125727744 count=27031 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=125727744 count=27031 +cmp /tmp/chk.bin $BASE/115_115_Measuring116_116.bin + +echo "== 116_Measuring117_117 == +dd if=$BASE/116_116_Measuring117_117.bin of=$DEV bs=1 seek=125703168 count=24325 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=125703168 count=24325 +cmp /tmp/chk.bin $BASE/116_116_Measuring117_117.bin + +echo "== 117_Measuring118_118 == +dd if=$BASE/117_117_Measuring118_118.bin of=$DEV bs=1 seek=125680128 count=22811 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=125680128 count=22811 +cmp /tmp/chk.bin $BASE/117_117_Measuring118_118.bin + +echo "== 118_Measuring119_119 == +dd if=$BASE/118_118_Measuring119_119.bin of=$DEV bs=1 seek=125660672 count=19025 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=125660672 count=19025 +cmp /tmp/chk.bin $BASE/118_118_Measuring119_119.bin + +rm /tmp/chk.bin +echo "all done - run reboot to apply" diff --git a/s4e-pressure-anim/main.lua b/s4e-pressure-anim/main.lua new file mode 100644 index 0000000..e5b444e --- /dev/null +++ b/s4e-pressure-anim/main.lua @@ -0,0 +1,735 @@ +--[[---------------------------------------------------------------------------- + S4PAnim —— S5 41mm(OS4) 压力检测动画 -> S4 eSIM + + 界面按 CSS 设计稿 1:1 落地(本机用真 MiSans 字体渲染预览验证过): + * 页面底色纯黑(取自封面图四角) + * rgba(255,255,255,0.6) -> #999999(白 60% 叠黑) + * font-weight 600 -> MiSans-Semibold ; 500 -> MiSans-Medium + * vertical-align:middle -> 手工算 y(LVGL 标签没有垂直居中样式) + * 多色文本用 LVGL recolor:"灰#ffffff 白#灰" + * 圆屏安全区:实测所有页面 0 像素越界(大按钮全靠 40px 圆角收进圆内) + + 真机已验证的底层能力: + * 宿主不调用 ui.init -> 加载期建界面 + * SCRIPT_PATH 为空 -> 用 debug.getinfo 反推目录 + * .face 不解包,资源仍在 resource.bin 里 -> 按偏移读出来用 + * /dev/app 可写,dd 读写校验通过 + * 没有 utf8 标准库 -> 自带按位 UTF-8 解码(本文件未用到,留作提醒) +------------------------------------------------------------------------------]] + +local lvgl = require("lvgl") + +local W, H = 466, 466 +local DEV, BLK = "/dev/app", 4 +local TMPH = "/tmp/os4_head.bin" +local TMPR = "/tmp/os4_back.bin" + +-- 等长覆盖只保证「偏移」512 对齐;RLE 素材的长度常常不是 4 的倍数(如 1635 / 3047), +-- 那种记录必须退化成 bs=1(偏移本身对齐,所以 bs=1 也精确),否则会多写相邻文件的字节。 +local function ddBlk(off, len) + if off % BLK == 0 and len % BLK == 0 then return BLK end + return 1 +end +local TMPB = "/tmp/os4_blob.bin" + + +--------------------------------------------------------------------- 自身目录 +local SRC = "" +if debug and debug.getinfo then + local info = debug.getinfo(1, "S") + SRC = (info and info.source) or "" +end +if type(SRC) ~= "string" then SRC = "" end +if SRC:sub(1, 1) == "@" then SRC = SRC:sub(2) end +SRC = SRC:gsub("\\", "/") +local THIS_DIR = SRC:match("^(.*)/[^/]+$") or "" +local SP = THIS_DIR ~= "" and (THIS_DIR .. "/") or "" +local PARENT = SP:gsub("[^/]+/$", "") + +local RES = nil +do + local c = { PARENT .. "resource.bin", SP .. "../resource.bin", SP .. "resource.bin" } + for i = 1, #c do + local f = io.open(c[i], "rb") + if f then f:close(); RES = c[i]; break end + end +end + +--------------------------------------------------------------------- 资源偏移 +-- UI 图片 { resource.bin 内偏移, 长度, 名字 } —— 由 _tools/gen_ui_lua.py 回填 +local UIIMG = { +__UIIMG__ +} + +-- 角色:'replace' = 只负责替换(写 q63 的帧);'restore' = 只负责还原(写回原厂字节) +-- 两份盘共用同一个 watchface id,所以设备上只会占一份空间(见 README) +local ROLE = '__ROLE__' + +-- 负载表 { resource 内偏移, ROMFS 目标偏移, 长度 } —— 由脚本回填 +-- pay = 本盘要写进去的字节;rb = **另一侧字节的 64 B 指纹**(只给版本校验用) +local PAYTBL = { +__PAY__ +__RB__ +} + +local function prepImages() + local paths = {} + if not RES then return paths end + local f = io.open(RES, "rb") + if not f then return paths end + for i = 1, #UIIMG do + local e = UIIMG[i] + f:seek("set", e[1]) + local d = f:read(e[2]) + if d and #d == e[2] then + -- 直接写到 /tmp 根下:不能写子目录(目录不存在,且这台机器的 Lua 没有 mkdir) + local p = "/tmp/ui_" .. e[3] .. ".bin" + local o = io.open(p, "wb") + if o then o:write(d); o:close(); paths[e[3]] = p end + end + end + f:close() + return paths +end + +local IMG = prepImages() + +--------------------------------------------------------------------- 颜色与字体 +local C_GRAY, C_WHITE = '#999999', '#ffffff' +local C_OK, C_ERR = '#43cf7c', '#d43030' +local C_PLATE, C_CIRC = 0x002927, 0x4f4f4f + +local function getFont(name, size) + local f = nil + pcall(function() f = lvgl.Font(name, size) end) + return f +end + +local F_CLOCK = getFont('MiSans-Semibold', 28) +local F_TITLE = getFont('MiSans-Medium', 30) +local F_ITEM = getFont('MiSans-Medium', 28) +local F_BODY = getFont('MiSans-Medium', 24) +local F_VER = getFont('MiSans-Medium', 20) + +--------------------------------------------------------------------- 按下高亮 +-- onPressed / onClicked 是真实的 LVGL 对象方法(就在 onevent 旁边,属性表里可查) +local HL = 0x005149 -- 按下高亮色(比底板 0x002927 明显亮) +local pressed = nil -- { obj = , t = } +local tickCount = 0 + +local function setBg(obj, c) + pcall(function() obj:set { bg_color = c } end) +end + +local function pressRelease() + if pressed then + setBg(pressed.obj, C_PLATE) + pressed = nil + end +end + +--------------------------------------------------------------------- 基础 +local ui = {} +local built = false +local pages = {} +local clocks = {} +local job = { run = false, list = nil, i = 0, errs = 0, variant = 'replace' } +-- 从哪个入口进"开始替换"确认页,确认按钮就执行对应操作: +-- 首页「开始替换」 -> 'replace' +-- 帮助页「回滚修改」 -> 'rollback'(也要先过一遍警告确认,不直接开跑) +local pendingVariant = 'replace' +-- 确认页是从哪进来的,返回就回哪: +-- 首页「开始替换」 -> 'home' ; 帮助页「回滚修改」 -> 'help' +local confirmFrom = 'home' + +local function newPage() + local p = nil + pcall(function() + p = lvgl.Object(nil, { x = 0, y = 0, w = W, h = H, bg_color = 0x000000, + bg_opa = 255, border_width = 0, pad_all = 0 }) + p:clear_flag(lvgl.FLAG.SCROLLABLE) + p:add_flag(lvgl.FLAG.EVENT_BUBBLE) + end) + return p +end + +local function show(p) + if not p then return end + pressRelease() -- 切页时清掉可能残留的高亮 + for k, v in pairs(pages) do + pcall(function() v:add_flag(lvgl.FLAG.HIDDEN) end) + end + pcall(function() p:clear_flag(lvgl.FLAG.HIDDEN) end) +end + +local function bind(obj, fn) + if not obj then return end + -- 成品样本用的是 onevent(lvgl.EVENT.SHORT_CLICKED, fn)(CLICKED 这个常量 + -- 在这台机器上不存在),所以按 SHORT_CLICKED -> onClicked -> CLICKED 依次试 + local ev = lvgl.EVENT + if ev and ev.SHORT_CLICKED then + if pcall(function() obj:onevent(ev.SHORT_CLICKED, function() fn() end) end) then return end + end + if pcall(function() obj:onClicked(function() fn() end) end) then return end + pcall(function() obj:onevent(ev and ev.CLICKED, fn) end) +end + +--------------------------------------------------------------------- 点按 +-- 点按 = 按下高亮 + 抬起恢复 + 执行动作;带超时兜底(手指滑出时 click 不触发) +local function bindTap(obj, onTap) + if not obj then return end + local ok = pcall(function() + obj:onPressed(function() + setBg(obj, HL) + pressed = { obj = obj, t = tickCount } + end) + obj:onClicked(function() + pressRelease() + if onTap then onTap() end + end) + end) + if not ok then bind(obj, onTap) end +end + +local function setClickable(o) + if not o then return end + pcall(function() o:add_flag(lvgl.FLAG.CLICKABLE) end) +end + +local function lab(parent, o) + o.text_font = o.text_font or F_BODY + local l = nil + pcall(function() l = parent:Label(o) end) + return l +end + +local function clockLabel(parent) + -- 居中不用 text_align(它要的是 lv_text_align_t:LEFT=0/CENTER=1/RIGHT=2, + -- 传 ALIGN.CENTER=9 越界会被忽略 -> 实测变成左对齐)。 + -- 改为按真字体量出的文本宽度手工算 x,并把 w 收紧,这样对齐方式就不影响结果。 + local l = lab(parent, { x = 195, y = 3, w = 86, h = 45, text = '--:--', + text_color = C_GRAY, font_size = 28, text_font = F_CLOCK }) + clocks[#clocks + 1] = l + return l +end + +local function pageTitle(parent, text, x, w) + -- 标题 y:框 y=34 h=45,字号 30 行高约 41 -> 34 + (45-41)/2 = 36 + return lab(parent, { x = x, y = 36, w = w, h = 45, text = text, + text_color = C_WHITE, font_size = 30, text_font = F_TITLE }) +end + +local function backArrow(parent, x, onTap) + local im = nil + if IMG.back then + pcall(function() im = parent:Image { src = IMG.back, x = x or 160, y = 43 } end) + setClickable(im) + bind(im, onTap) + end + return im +end + +-- 列表项:底板(整块可点) + 圆形遮罩 + 图标 + 标题 +-- pad_all=0 是关键:LVGL 主题会给 Object 加内边距,不清零的话子元素会整体右下偏移 +local function listItem(parent, y, iconkey, title, onTap) + local plate = nil + pcall(function() + plate = parent:Object { x = 49, y = y, w = 368, h = 88, pad_all = 0, + bg_color = C_PLATE, border_width = 0, radius = 34 } + end) + if plate then + if onTap then + setClickable(plate) + bindTap(plate, onTap) + end + pcall(function() + plate:Object { x = 20, y = 19, w = 50, h = 50, + bg_color = C_CIRC, border_width = 0, radius = 25 } + end) + if IMG[iconkey] then + pcall(function() plate:Image { src = IMG[iconkey], x = 30, y = 29 } end) + end + -- 标题框 h=54,字号 28 行高约 38 -> 垂直居中 y = 17 + 8 = 25(左对齐,不用 text_align) + lab(plate, { x = 80, y = 25, w = 208, h = 54, text = title, + text_color = C_WHITE, font_size = 28, text_font = F_ITEM }) + end + return plate +end + +local function bigButton(parent, text) + local b = nil + pcall(function() + b = parent:Object { x = 108, y = 365, w = 250, h = 80, pad_all = 0, + bg_color = C_PLATE, border_width = 0, radius = 40 } + end) + if b then setClickable(b) end + -- 按钮文字:框 (65,13,120,54) 相对底板;字号 28 行高约 38 -> y = 13 + 8 = 21 + -- "确认替换"/"重启手表" 量得 112px -> 居中 x = 65 + (120-112)/2 = 69 + lab(b, { x = 69, y = 21, w = 122, h = 54, text = text, + text_color = C_WHITE, font_size = 28, text_font = F_ITEM }) + return b +end + +local function clockTick() + local t = '--:--' + if os and type(os.date) == "function" then + local ok, s = pcall(os.date, "%H:%M") + if ok and type(s) == "string" then t = s end + end + for i = 1, #clocks do + pcall(function() clocks[i]:set { text = t } end) + end +end + +--------------------------------------------------------------------- 外壳 +local function slurp(p, n) + local f = io.open(p, "rb") + if not f then return nil end + local d = f:read(n or "*a") + f:close() + return d +end + +local function sh(cmd) + if type(os.execute) ~= "function" then return -1 end + local a, b, c = os.execute(cmd) + if type(a) == "number" then return a end + if a == true then return 0 end + if type(c) == "number" then return c end + if type(b) == "number" then return b end + return -1 +end + +--------------------------------------------------------------------- 预检查 +local desc1 = nil + +local function setDesc1(txt, color) + if not desc1 then return end + pcall(function() desc1:set { text = txt, text_color = color } end) +end + +local function doCheck() + if not RES then + setDesc1('找不到 resource.bin,无法操作。', C_ERR) + return false + end + if sh("dd if=" .. DEV .. " of=/tmp/os4_hdr.bin bs=1 count=48") ~= 0 then + setDesc1('读取分区失败(dd 不可用或无权限)。', C_ERR) + return false + end + local h = slurp("/tmp/os4_hdr.bin", 48) + if not h or #h < 48 then + setDesc1('读不到分区头。', C_ERR) + return false + end + if h:sub(1, 8) ~= "-rom1fs-" then + setDesc1('预检查失败:分区头不是 ROMFS。', C_ERR) + return false + end + local z = h:find("\0", 17, true) + local vol = h:sub(17, (z or 33) - 1) + if vol ~= "app" then + setDesc1('预检查失败:卷名是 ' .. vol .. '。', C_ERR) + return false + end + local size = string.unpack(" size then bad = bad + 1 end + total = total + r[3] + end + if bad > 0 then + setDesc1(string.format('预检查失败:%d 项越界或未对齐。', bad), C_ERR) + return false + end + -- 顺带确认 resource.bin 第一条能读出来 + local f = io.open(RES, "rb") + if f then + f:seek("set", list[1][1]) + local probe = f:read(list[1][3]) + f:close() + if not probe or #probe ~= list[1][3] then + setDesc1('预检查失败:resource.bin 读取异常。', C_ERR) + return false + end + end + setDesc1(string.format('预检查通过 | %d 条 | %.2fMB\n您可以返回主页开始修改。', + #list, total / 1048576), C_OK) + return true +end + +--------------------------------------------------------------------- 替换 +local logLabel = nil +local rebootBtn = nil +local logLines = {} + +local function logOut(s) + logLines[#logLines + 1] = s + while #logLines > 8 do table.remove(logLines, 1) end + if logLabel then + pcall(function() logLabel:set { text = table.concat(logLines, "\n") } end) + end +end + +local function showReboot() + if rebootBtn then pcall(function() rebootBtn:clear_flag(lvgl.FLAG.HIDDEN) end) end +end + +local function readChunk(off, len, path, bsz) + if os.remove then pcall(os.remove, path) end + sh(string.format("dd if=%s of=%s bs=%d skip=%d count=%d", + DEV, path, bsz, off // bsz, len // bsz)) + return slurp(path, len) +end + +-- 大块 + 尾块:偏移都是 512 对齐,RLE 素材的长度却常常不是 4/512 的倍数(4485 / 196005 …)。 +-- 老写法对这种长度只能 bs=1 -> 一条 19 万字节要 38 万次单字节读写,慢到偶发失败。 +-- 这里把它切成 ≤3 段:bs=512 的大块 + bs=4 的次块 + 最后 1~3 字节 bs=1,syscall 直接降两个数量级。 +local BIG = 512 + +local function segs(off, len) + local out = {} + if off % BIG == 0 then + local head = len - (len % BIG) + if head > 0 then out[#out + 1] = { BIG, 0, head } end + local rest = len - head + local t4 = rest - (rest % 4) + if t4 > 0 then out[#out + 1] = { 4, head, t4 } end + if rest - t4 > 0 then out[#out + 1] = { 1, head + t4, rest - t4 } end + else + out[1] = { 1, 0, len } + end + return out +end + +local function ddWrite(off, len, data) + for _, s in ipairs(segs(off, len)) do + local b, rel, n = s[1], s[2], s[3] + -- 每段单独一个临时文件:这样 dd 只需要 seek(输出偏移),不依赖输入侧的 skip + local o = io.open(TMPB, "wb") + if not o then return false end + o:write(data:sub(rel + 1, rel + n)) + o:close() + sh(string.format("dd if=%s of=%s bs=%d seek=%d count=%d", + TMPB, DEV, b, (off + rel) // b, n // b)) + end + return true +end + +local function ddRead(off, len) + local parts = {} + for _, s in ipairs(segs(off, len)) do + local b, rel, n = s[1], s[2], s[3] + if os.remove then pcall(os.remove, TMPR) end + sh(string.format("dd if=%s of=%s bs=%d skip=%d count=%d", + DEV, TMPR, b, (off + rel) // b, n // b)) + local d = slurp(TMPR, n) + if not d or #d ~= n then return nil end + parts[#parts + 1] = d + end + return table.concat(parts) +end + +-- 每条三步:① 版本校验(设备上那段必须等于「原厂」或「替换后」)② 写入 ③ 整段回读比对 +local function writeOne(rec, want) + local f = io.open(RES, "rb") + if not f then return 'err' end + f:seek("set", rec[1]) + local data = f:read(rec[3]) + f:close() + if not data or #data ~= rec[3] then return 'err' end + local mismatch = false + if want then + local n = math.min(64, rec[3], want[3]) + local wf = io.open(RES, "rb") + wf:seek("set", want[1]) + local wdata = wf:read(n) + wf:close() + local cur = readChunk(rec[2], n, TMPH, 1) + if not cur or #cur ~= n or not wdata or #wdata ~= n then return 'err' end + -- 设备上现在是「原厂」或「已改」都算正常;都不是(比如上次只写了一半)也**照样写**, + -- 只记一笔 —— 跳过去反而修不好那种半截状态。 + if cur ~= data:sub(1, n) and cur ~= wdata then mismatch = true end + end + local ok, lastBack = false, nil + for attempt = 1, 3 do + if not ddWrite(rec[2], rec[3], data) then return 'err' end + sh("sync") + lastBack = ddRead(rec[2], rec[3]) + if lastBack and #lastBack == rec[3] and lastBack == data then ok = true break end + logOut(string.format('> 第 %d 条校验不过,重试 %d/3', job.i, attempt)) + end + if not ok then + -- 计划 B:常规「分段写」3 次不过时,改成整条一次 dd(bs=4 或 bs=1)。 + -- 有些驱动对「同一区域被拆成多次小写」比「一次大写」更敏感,换个写法常常就过了。 + local b = (rec[3] % 4 == 0) and 4 or 1 + local o = io.open(TMPB, "wb") + if o then + o:write(data) + o:close() + sh(string.format("dd if=%s of=%s bs=%d seek=%d count=%d", + TMPB, DEV, b, rec[2] // b, rec[3] // b)) + sh("sync") + local back2 = ddRead(rec[2], rec[3]) + if back2 and #back2 == rec[3] and back2 == data then + ok = true + logOut(string.format('> 第 %d 条改用整条 bs=%d 写入后校验通过', job.i, b)) + end + end + end + if not ok then + -- 诊断:把「哪一条 / 目标偏移 / 长度 / 读回多少字节 / 第一个不同的字节」打出来 + local why = '读回 nil' + if lastBack then + if #lastBack ~= rec[3] then + why = string.format('读回 %d 字节', #lastBack) + else + why = '内容全同?' + for i = 1, #lastBack do + if lastBack:byte(i) ~= data:byte(i) then + why = string.format('首差@%d 得%02X 期%02X', i - 1, lastBack:byte(i), data:byte(i)) + break + end + end + end + end + logOut(string.format('> 失败 #%d off=0x%X len=%d %s', job.i, rec[2], rec[3], why)) + return 'err' + end + return mismatch and 'warn' or 'ok' +end + +local function startJob(variant) + if job.run then return end + local list = (variant == 'rollback') and PAYTBL.rb or PAYTBL.pay + if not list or #list == 0 then + setDesc1('负载表为空,已中止。', C_ERR) + return + end + job.run, job.list, job.i, job.errs, job.variant = true, list, 0, 0, variant + logLines = {} + logOut(variant == 'rollback' and '> 开始回滚' or '> 开始替换') + logOut('> 读取 resource.bin') + if rebootBtn then pcall(function() rebootBtn:add_flag(lvgl.FLAG.HIDDEN) end) end + show(pages.running) +end + +local function tick() + tickCount = tickCount + 1 + -- 兜底:按下超过 1.6s 没收到 click(手指滑出等)就恢复高亮 + if pressed and (tickCount - pressed.t) > 40 then pressRelease() end + if not job.run or not job.list then return end + job.i = job.i + 1 + local n = #job.list + if job.i > n then + job.run = false + local s = (job.variant == 'rollback') + and '> 回滚完毕,请重启手表。' + or '> 替换完毕,请重启手表。若发现问题,请回滚修改。' + logOut(s) + logOut(string.format('> 共 %d 条,失败 %d 条%s', n, job.errs, + (job.vers or 0) > 0 and string.format(',写入前状态异常 %d 条(已照样写入)', job.vers) or '')) + showReboot() + return + end + local rb = (job.variant == 'rollback') and PAYTBL.pay or PAYTBL.rb + local r = writeOne(job.list[job.i], rb and rb[job.i]) + if r == 'warn' then + job.vers = (job.vers or 0) + 1 + elseif r ~= 'ok' then + job.errs = job.errs + 1 + end + if job.i % 4 == 0 or job.i == n then + logOut(string.format('> 写入 %d/%d 失败 %d', job.i, n, job.errs)) + end +end + +--------------------------------------------------------------------- 页面 +local function buildHome() + local p = newPage() + if not p then return nil end + clockLabel(p) + pageTitle(p, ROLE == 'restore' and 'S4PAnim还原' or 'S4PAnim', 159, 158) + listItem(p, 85, 'ic_help', '快速帮助', function() show(pages.help) end) + listItem(p, 181, 'ic_replace', ROLE == 'restore' and '开始还原' or '开始替换', function() + pendingVariant = 'replace' + confirmFrom = 'home' + show(pages.confirm) + end) + listItem(p, 277, 'ic_about', '关于', function() show(pages.about) end) + return p +end + +local function buildHelp() + local p = newPage() + if not p then return nil end + clockLabel(p) + backArrow(p, 160, function() show(pages.home) end) + local t = pageTitle(p, '快速帮助', 183, 130) + setClickable(t) + bind(t, function() show(pages.home) end) + + listItem(p, 85, 'ic_check', '环境预检查', function() doCheck() end) + desc1 = lab(p, { x = 65, y = 176, w = 338, h = 72, + text = '检查原始资源状态、分区可写情况等。', + text_color = C_GRAY, font_size = 24, text_font = F_BODY }) + + -- 拆盘之后:本盘只装一份负载,「回滚/恢复」由另一份盘负责 + listItem(p, 256, 'ic_rollback', ROLE == 'restore' and '恢复修改' or '回滚修改', function() + if ROLE == 'restore' then + setDesc1('本盘是还原盘。要重新换回动画,请安装 S4PAnim.face。', C_GRAY) + else + setDesc1('本盘只负责替换。要还原原厂动画,请安装 S4PAnimRestore.face。', C_GRAY) + end + end) + lab(p, { x = 65, y = 344, w = 334, h = 72, + text = ROLE == 'restore' and '把压力检测动画写回原厂样式。' + or '本盘不含回滚数据(体积减半),还原请装 S4PAnimRestore.face。', + text_color = C_GRAY, font_size = 24, text_font = F_BODY }) + return p +end + +local function buildConfirm() + local p = newPage() + if not p then return nil end + clockLabel(p) + -- 确认页的返回:回到进来时的那个页面(首页 或 快速帮助) + local function goBack() show(pages[confirmFrom] or pages.home) end + backArrow(p, 160, goBack) + local t = pageTitle(p, '开始替换', 183, 130) + setClickable(t) + bind(t, goBack) + + -- 多色文本**不能**用 recolor:AP 里那几处 recolor 是图片重着色(bg_img_recolor) + -- 和表盘编辑器的主题重着色,不是文本属性;实测标记会被原样显示出来。 + -- 改为拆成多个标签 + 手工算 x(宽度用真 MiSans 24px 量出)。 + -- 折行也显式写死:因为"替换图标包之前,请先确认自己"量得 336px,几乎等于 338px 框宽, + -- 且型号那行 500px 必然折行,只有写死才能让多色片段位置确定。 + lab(p, { x = 64, y = 90, w = 338, h = 70, + text = '替换压力检测动画之前,请先确认自己\n的系统版本号为 ', + text_color = C_GRAY, font_size = 24, text_font = F_BODY }) + lab(p, { x = 241, y = 123, w = 104, h = 33, text = '3.202.79', + text_color = C_WHITE, font_size = 24, text_font = F_BODY }) + lab(p, { x = 340, y = 123, w = 30, h = 33, text = '!', + text_color = C_GRAY, font_size = 24, text_font = F_BODY }) + lab(p, { x = 64, y = 189, w = 338, h = 33, text = '请确认自己的手表型号为', + text_color = C_GRAY, font_size = 24, text_font = F_BODY }) + lab(p, { x = 64, y = 222, w = 338, h = 70, + text = 'Xiaomi Watch S4 eSIM(非\n15 周年纪念版)', + text_color = C_WHITE, font_size = 24, text_font = F_BODY }) + lab(p, { x = 242, y = 255, w = 30, h = 33, text = '!', + text_color = C_GRAY, font_size = 24, text_font = F_BODY }) + lab(p, { x = 64, y = 321, w = 338, h = 33, text = '搞机有风险,变砖后果自负。', + text_color = C_GRAY, font_size = 24, text_font = F_BODY }) + + local b = bigButton(p, '确认替换') + bindTap(b, function() startJob(pendingVariant) end) + return p +end + +local function buildAbout() + local p = newPage() + if not p then return nil end + clockLabel(p) + backArrow(p, 191, function() show(pages.home) end) + local t = pageTitle(p, '关于', 214, 64) + setClickable(t) + bind(t, function() show(pages.home) end) + + -- 信息卡:底板 + 圆形 logo + 大标题 + 版本号 + pcall(function() + p:Object { x = 52, y = 79, w = 362, h = 204, pad_all = 0, + bg_color = C_PLATE, border_width = 0, radius = 30 } + end) + if IMG.logo then + pcall(function() p:Image { src = IMG.logo, x = 194, y = 108 } end) + end + -- "OS4 Icons" 28px 量得 138px -> 居中 x = 66 + (334-138)/2 = 164 + lab(p, { x = 164, y = 192, w = 138, h = 38, text = ROLE == 'restore' and 'S4PAnim还原' or 'S4PAnim', + text_color = C_WHITE, font_size = 28, text_font = F_ITEM }) + -- "1.0.0" 20px 量得 45px -> 居中 x = 66 + (334-45)/2 = 210 + lab(p, { x = 210, y = 227, w = 45, h = 27, text = '1.0.0', + text_color = C_GRAY, font_size = 20, text_font = F_VER }) + + -- QQ 联系方式:结构与列表项一致(无动作,所以不给它绑点击) + listItem(p, 291, 'ic_qq', '732681995', nil) + return p +end + +local function buildRunning() + local p = newPage() + if not p then return nil end + clockLabel(p) + -- 本页没有返回图标(规格:顶栏同首页),所以标题要屏幕居中: + -- "正在替换" 30px 量得 120px -> x = 233 - 120/2 = 173(不是帮助页的 183) + pageTitle(p, '正在替换', 173, 130) + logLabel = lab(p, { x = 64, y = 90, w = 338, h = 269, text = '', + text_color = C_WHITE, font_size = 24, text_font = F_BODY }) + rebootBtn = bigButton(p, '重启手表') + -- 替换过程中不显示,替换完再显示 + if rebootBtn then pcall(function() rebootBtn:add_flag(lvgl.FLAG.HIDDEN) end) end + bindTap(rebootBtn, function() + logOut('> 正在重启…') + sh("sync") + sh("reboot") + end) + return p +end + +--------------------------------------------------------------------- 组装 +local function build() + if built then return end + built = true + pages.home = buildHome() + pages.help = buildHelp() + pages.confirm = buildConfirm() + pages.about = buildAbout() + pages.running = buildRunning() + if pages.home then show(pages.home) end + clockTick() + pcall(function() + ui.timer = lvgl.Timer { period = 40, repeat_count = -1, cb = function() tick() end } + if ui.timer and ui.timer.resume then ui.timer:resume() end + end) + pcall(function() + ui.clockTimer = lvgl.Timer { period = 1000, repeat_count = -1, + cb = function() clockTick() end } + if ui.clockTimer and ui.clockTimer.resume then ui.clockTimer:resume() end + end) +end + +local okbuild, errbuild = pcall(build) +if not okbuild then + pcall(function() + local r = lvgl.Object(nil, { x = 0, y = 0, w = W, h = H, bg_color = 0x000000, + bg_opa = 255, border_width = 0 }) + r:Label { x = 20, y = 20, w = W - 40, h = H - 40, + text = 'BUILD FAILED:\n' .. tostring(errbuild), + text_color = '#ffffff', font_size = 13, text_font = F_BODY } + end) +end + +ui.init = function() + pcall(build) + return ui.root +end + +function pageOnPause() + if ui.timer then pcall(function() ui.timer:pause() end) end + if ui.clockTimer then pcall(function() ui.clockTimer:pause() end) end +end + +function pageOnResume() + if ui.timer then pcall(function() ui.timer:resume() end) end + if ui.clockTimer then pcall(function() ui.clockTimer:resume() end) end +end + +return ui diff --git a/s4e-pressure-anim/payload_rollback/payload.lua b/s4e-pressure-anim/payload_rollback/payload.lua new file mode 100644 index 0000000..eec1273 --- /dev/null +++ b/s4e-pressure-anim/payload_rollback/payload.lua @@ -0,0 +1,121 @@ +-- generated by _tools/make_dd_payload.py -- do not hand-edit +-- dd: -if / -of /dev/app bs=4 seek=offset/4 count=length/4 +return { + { name = "canary_superblock", file = "000_canary_superblock.bin", offset = 0, length = 32 }, + { name = "003_Measuring4_4", file = "001_003_Measuring4_4.bin", offset = 121341952, length = 5078 }, + { name = "004_Measuring5_5", file = "002_004_Measuring5_5.bin", offset = 119374336, length = 6327 }, + { name = "005_Measuring6_6", file = "003_005_Measuring6_6.bin", offset = 117413376, length = 8436 }, + { name = "006_Measuring7_7", file = "004_006_Measuring7_7.bin", offset = 115506688, length = 11785 }, + { name = "007_Measuring8_8", file = "005_007_Measuring8_8.bin", offset = 113678336, length = 17510 }, + { name = "008_Measuring9_9", file = "006_008_Measuring9_9.bin", offset = 112016384, length = 23011 }, + { name = "009_Measuring10_10", file = "007_009_Measuring10_10.bin", offset = 126875136, length = 31377 }, + { name = "010_Measuring11_11", file = "008_010_Measuring11_11.bin", offset = 126014464, length = 43104 }, + { name = "011_Measuring12_12", file = "009_011_Measuring12_12.bin", offset = 125605888, length = 54415 }, + { name = "012_Measuring13_13", file = "010_012_Measuring13_13.bin", offset = 125539840, length = 65693 }, + { name = "013_Measuring14_14", file = "011_013_Measuring14_14.bin", offset = 125463552, length = 75926 }, + { name = "014_Measuring15_15", file = "012_014_Measuring15_15.bin", offset = 125372928, length = 90514 }, + { name = "015_Measuring16_16", file = "013_015_Measuring16_16.bin", offset = 125251584, length = 120902 }, + { name = "016_Measuring17_17", file = "014_016_Measuring17_17.bin", offset = 125123584, length = 127580 }, + { name = "017_Measuring18_18", file = "015_017_Measuring18_18.bin", offset = 124991488, length = 131691 }, + { name = "018_Measuring19_19", file = "016_018_Measuring19_19.bin", offset = 124854784, length = 136658 }, + { name = "019_Measuring20_20", file = "017_019_Measuring20_20.bin", offset = 124708352, length = 141610 }, + { name = "020_Measuring21_21", file = "018_020_Measuring21_21.bin", offset = 124561920, length = 145939 }, + { name = "021_Measuring22_22", file = "019_021_Measuring22_22.bin", offset = 124407296, length = 154315 }, + { name = "022_Measuring23_23", file = "020_022_Measuring23_23.bin", offset = 124248064, length = 158922 }, + { name = "023_Measuring24_24", file = "021_023_Measuring24_24.bin", offset = 124083712, length = 164011 }, + { name = "024_Measuring25_25", file = "022_024_Measuring25_25.bin", offset = 123916288, length = 166983 }, + { name = "025_Measuring26_26", file = "023_025_Measuring26_26.bin", offset = 123746816, length = 169431 }, + { name = "026_Measuring27_27", file = "024_026_Measuring27_27.bin", offset = 123574784, length = 171709 }, + { name = "027_Measuring28_28", file = "025_027_Measuring28_28.bin", offset = 123400704, length = 173905 }, + { name = "028_Measuring29_29", file = "026_028_Measuring29_29.bin", offset = 123225088, length = 175146 }, + { name = "029_Measuring30_30", file = "027_029_Measuring30_30.bin", offset = 123042816, length = 176699 }, + { name = "030_Measuring31_31", file = "028_030_Measuring31_31.bin", offset = 122864128, length = 178248 }, + { name = "031_Measuring32_32", file = "029_031_Measuring32_32.bin", offset = 122683392, length = 180668 }, + { name = "032_Measuring33_33", file = "030_032_Measuring33_33.bin", offset = 122497024, length = 186322 }, + { name = "033_Measuring34_34", file = "031_033_Measuring34_34.bin", offset = 122308608, length = 188260 }, + { name = "034_Measuring35_35", file = "032_034_Measuring35_35.bin", offset = 122118656, length = 189862 }, + { name = "035_Measuring36_36", file = "033_035_Measuring36_36.bin", offset = 121927680, length = 190691 }, + { name = "036_Measuring37_37", file = "034_036_Measuring37_37.bin", offset = 121735680, length = 191837 }, + { name = "037_Measuring38_38", file = "035_037_Measuring38_38.bin", offset = 121542144, length = 193189 }, + { name = "038_Measuring39_39", file = "036_038_Measuring39_39.bin", offset = 121347072, length = 194939 }, + { name = "039_Measuring40_40", file = "037_039_Measuring40_40.bin", offset = 121146368, length = 195239 }, + { name = "040_Measuring41_41", file = "038_040_Measuring41_41.bin", offset = 120950272, length = 195793 }, + { name = "041_Measuring42_42", file = "039_041_Measuring42_42.bin", offset = 120754176, length = 196046 }, + { name = "042_Measuring43_43", file = "040_042_Measuring43_43.bin", offset = 120558080, length = 196021 }, + { name = "043_Measuring44_44", file = "041_043_Measuring44_44.bin", offset = 120361472, length = 196086 }, + { name = "044_Measuring45_45", file = "042_044_Measuring45_45.bin", offset = 120165376, length = 195652 }, + { name = "045_Measuring46_46", file = "043_045_Measuring46_46.bin", offset = 119969280, length = 195974 }, + { name = "046_Measuring47_47", file = "044_046_Measuring47_47.bin", offset = 119773184, length = 195999 }, + { name = "047_Measuring48_48", file = "045_047_Measuring48_48.bin", offset = 119577088, length = 195847 }, + { name = "048_Measuring49_49", file = "046_048_Measuring49_49.bin", offset = 119380992, length = 195899 }, + { name = "049_Measuring50_50", file = "047_049_Measuring50_50.bin", offset = 119177728, length = 196154 }, + { name = "050_Measuring51_51", file = "048_050_Measuring51_51.bin", offset = 118981120, length = 196246 }, + { name = "051_Measuring52_52", file = "049_051_Measuring52_52.bin", offset = 118785024, length = 196042 }, + { name = "052_Measuring53_53", file = "050_052_Measuring53_53.bin", offset = 118588928, length = 195985 }, + { name = "053_Measuring54_54", file = "051_053_Measuring54_54.bin", offset = 118392320, length = 196538 }, + { name = "054_Measuring55_55", file = "052_054_Measuring55_55.bin", offset = 118198272, length = 193546 }, + { name = "055_Measuring56_56", file = "053_055_Measuring56_56.bin", offset = 118004224, length = 193984 }, + { name = "056_Measuring57_57", file = "054_056_Measuring57_57.bin", offset = 117810176, length = 193954 }, + { name = "057_Measuring58_58", file = "055_057_Measuring58_58.bin", offset = 117616128, length = 193929 }, + { name = "058_Measuring59_59", file = "056_058_Measuring59_59.bin", offset = 117422080, length = 193761 }, + { name = "059_Measuring60_60", file = "057_059_Measuring60_60.bin", offset = 117218816, length = 194029 }, + { name = "060_Measuring61_61", file = "058_060_Measuring61_61.bin", offset = 117029376, length = 189089 }, + { name = "061_Measuring62_62", file = "059_061_Measuring62_62.bin", offset = 116838400, length = 190474 }, + { name = "062_Measuring63_63", file = "060_062_Measuring63_63.bin", offset = 116647424, length = 190748 }, + { name = "063_Measuring64_64", file = "061_063_Measuring64_64.bin", offset = 116455936, length = 190975 }, + { name = "064_Measuring65_65", file = "062_064_Measuring65_65.bin", offset = 116263424, length = 192023 }, + { name = "065_Measuring66_66", file = "063_065_Measuring66_66.bin", offset = 116071424, length = 191903 }, + { name = "066_Measuring67_67", file = "064_066_Measuring67_67.bin", offset = 115888640, length = 182358 }, + { name = "067_Measuring68_68", file = "065_067_Measuring68_68.bin", offset = 115704320, length = 183906 }, + { name = "068_Measuring69_69", file = "066_068_Measuring69_69.bin", offset = 115518976, length = 185033 }, + { name = "069_Measuring70_70", file = "067_069_Measuring70_70.bin", offset = 115321856, length = 184662 }, + { name = "070_Measuring71_71", file = "068_070_Measuring71_71.bin", offset = 115136512, length = 184877 }, + { name = "071_Measuring72_72", file = "069_071_Measuring72_72.bin", offset = 114951680, length = 184574 }, + { name = "072_Measuring73_73", file = "070_072_Measuring73_73.bin", offset = 114770432, length = 180773 }, + { name = "073_Measuring74_74", file = "071_073_Measuring74_74.bin", offset = 114589696, length = 180628 }, + { name = "074_Measuring75_75", file = "072_074_Measuring75_75.bin", offset = 114409472, length = 179919 }, + { name = "075_Measuring76_76", file = "073_075_Measuring76_76.bin", offset = 114228736, length = 180517 }, + { name = "076_Measuring77_77", file = "074_076_Measuring77_77.bin", offset = 114048000, length = 180677 }, + { name = "077_Measuring78_78", file = "075_077_Measuring78_78.bin", offset = 113867264, length = 180537 }, + { name = "078_Measuring79_79", file = "076_078_Measuring79_79.bin", offset = 113696256, length = 170518 }, + { name = "079_Measuring80_80", file = "077_079_Measuring80_80.bin", offset = 113507840, length = 170059 }, + { name = "080_Measuring81_81", file = "078_080_Measuring81_81.bin", offset = 113336320, length = 171247 }, + { name = "081_Measuring82_82", file = "079_081_Measuring82_82.bin", offset = 113165312, length = 170939 }, + { name = "082_Measuring83_83", file = "080_082_Measuring83_83.bin", offset = 112994816, length = 170193 }, + { name = "083_Measuring84_84", file = "081_083_Measuring84_84.bin", offset = 112840704, length = 153998 }, + { name = "084_Measuring85_85", file = "082_084_Measuring85_85.bin", offset = 112683520, length = 157044 }, + { name = "085_Measuring86_86", file = "083_085_Measuring86_86.bin", offset = 112524800, length = 158180 }, + { name = "086_Measuring87_87", file = "084_086_Measuring87_87.bin", offset = 112364544, length = 160025 }, + { name = "087_Measuring88_88", file = "085_087_Measuring88_88.bin", offset = 112203264, length = 160963 }, + { name = "088_Measuring89_89", file = "086_088_Measuring89_89.bin", offset = 112039936, length = 163153 }, + { name = "089_Measuring90_90", file = "087_089_Measuring90_90.bin", offset = 111863808, length = 152329 }, + { name = "090_Measuring91_91", file = "088_090_Measuring91_91.bin", offset = 111712256, length = 151129 }, + { name = "091_Measuring92_92", file = "089_091_Measuring92_92.bin", offset = 111562240, length = 149917 }, + { name = "092_Measuring93_93", file = "090_092_Measuring93_93.bin", offset = 111413248, length = 148930 }, + { name = "093_Measuring94_94", file = "091_093_Measuring94_94.bin", offset = 111264768, length = 148040 }, + { name = "094_Measuring95_95", file = "092_094_Measuring95_95.bin", offset = 111118848, length = 145756 }, + { name = "095_Measuring96_96", file = "093_095_Measuring96_96.bin", offset = 110998016, length = 120568 }, + { name = "096_Measuring97_97", file = "094_096_Measuring97_97.bin", offset = 110875648, length = 121902 }, + { name = "097_Measuring98_98", file = "095_097_Measuring98_98.bin", offset = 110754304, length = 121294 }, + { name = "098_Measuring99_99", file = "096_098_Measuring99_99.bin", offset = 110638592, length = 115589 }, + { name = "099_Measuring100_100", file = "097_099_Measuring100_100.bin", offset = 126764032, length = 110723 }, + { name = "100_Measuring101_101", file = "098_100_Measuring101_101.bin", offset = 126656512, length = 107240 }, + { name = "101_Measuring102_102", file = "099_101_Measuring102_102.bin", offset = 126569984, length = 86055 }, + { name = "102_Measuring103_103", file = "100_102_Measuring103_103.bin", offset = 126484480, length = 85118 }, + { name = "103_Measuring104_104", file = "101_103_Measuring104_104.bin", offset = 126400000, length = 84012 }, + { name = "104_Measuring105_105", file = "102_104_Measuring105_105.bin", offset = 126318592, length = 81068 }, + { name = "105_Measuring106_106", file = "103_105_Measuring106_106.bin", offset = 126240256, length = 77823 }, + { name = "106_Measuring107_107", file = "104_106_Measuring107_107.bin", offset = 126164480, length = 75410 }, + { name = "107_Measuring108_108", file = "105_107_Measuring108_108.bin", offset = 126111744, length = 52464 }, + { name = "108_Measuring109_109", file = "106_108_Measuring109_109.bin", offset = 126057984, length = 53592 }, + { name = "109_Measuring110_110", file = "107_109_Measuring110_110.bin", offset = 125961728, length = 52559 }, + { name = "110_Measuring111_111", file = "108_110_Measuring111_111.bin", offset = 125914624, length = 46569 }, + { name = "111_Measuring112_112", file = "109_111_Measuring112_112.bin", offset = 125874176, length = 40243 }, + { name = "112_Measuring113_113", file = "110_112_Measuring113_113.bin", offset = 125828608, length = 45031 }, + { name = "113_Measuring114_114", file = "111_113_Measuring114_114.bin", offset = 125788160, length = 40136 }, + { name = "114_Measuring115_115", file = "112_114_Measuring115_115.bin", offset = 125754880, length = 32924 }, + { name = "115_Measuring116_116", file = "113_115_Measuring116_116.bin", offset = 125727744, length = 27031 }, + { name = "116_Measuring117_117", file = "114_116_Measuring117_117.bin", offset = 125703168, length = 24325 }, + { name = "117_Measuring118_118", file = "115_117_Measuring118_118.bin", offset = 125680128, length = 22811 }, + { name = "118_Measuring119_119", file = "116_118_Measuring119_119.bin", offset = 125660672, length = 19025 }, +} diff --git a/s4e-pressure-anim/restore.sh b/s4e-pressure-anim/restore.sh new file mode 100644 index 0000000..5d0bbb6 --- /dev/null +++ b/s4e-pressure-anim/restore.sh @@ -0,0 +1,606 @@ +#!/bin/sh +# 把 S4 eSIM 的压力检测动画写回原厂(还原盘) + +DEV=/dev/app +BASE=/data/s4panim/payload_restore + +echo "target check" +ls -l $DEV + +echo "== 000_Measuring1_1 == +dd if=$BASE/000_000_Measuring1_1.bin of=$DEV bs=1 seek=126906880 count=4485 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=126906880 count=4485 +cmp /tmp/chk.bin $BASE/000_000_Measuring1_1.bin + +echo "== 001_Measuring2_2 == +dd if=$BASE/001_001_Measuring2_2.bin of=$DEV bs=1 seek=124850176 count=4530 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=124850176 count=4530 +cmp /tmp/chk.bin $BASE/001_001_Measuring2_2.bin + +echo "== 002_Measuring3_3 == +dd if=$BASE/002_002_Measuring3_3.bin of=$DEV bs=1 seek=123219968 count=4695 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=123219968 count=4695 +cmp /tmp/chk.bin $BASE/002_002_Measuring3_3.bin + +echo "== 003_Measuring4_4 == +dd if=$BASE/003_003_Measuring4_4.bin of=$DEV bs=1 seek=121341952 count=5078 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=121341952 count=5078 +cmp /tmp/chk.bin $BASE/003_003_Measuring4_4.bin + +echo "== 004_Measuring5_5 == +dd if=$BASE/004_004_Measuring5_5.bin of=$DEV bs=1 seek=119374336 count=6327 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=119374336 count=6327 +cmp /tmp/chk.bin $BASE/004_004_Measuring5_5.bin + +echo "== 005_Measuring6_6 == +dd if=$BASE/005_005_Measuring6_6.bin of=$DEV bs=4 seek=29353344 count=2109 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=29353344 count=2109 +cmp /tmp/chk.bin $BASE/005_005_Measuring6_6.bin + +echo "== 006_Measuring7_7 == +dd if=$BASE/006_006_Measuring7_7.bin of=$DEV bs=1 seek=115506688 count=11785 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=115506688 count=11785 +cmp /tmp/chk.bin $BASE/006_006_Measuring7_7.bin + +echo "== 007_Measuring8_8 == +dd if=$BASE/007_007_Measuring8_8.bin of=$DEV bs=1 seek=113678336 count=17510 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=113678336 count=17510 +cmp /tmp/chk.bin $BASE/007_007_Measuring8_8.bin + +echo "== 008_Measuring9_9 == +dd if=$BASE/008_008_Measuring9_9.bin of=$DEV bs=1 seek=112016384 count=23011 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=112016384 count=23011 +cmp /tmp/chk.bin $BASE/008_008_Measuring9_9.bin + +echo "== 009_Measuring10_10 == +dd if=$BASE/009_009_Measuring10_10.bin of=$DEV bs=1 seek=126875136 count=31377 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=126875136 count=31377 +cmp /tmp/chk.bin $BASE/009_009_Measuring10_10.bin + +echo "== 010_Measuring11_11 == +dd if=$BASE/010_010_Measuring11_11.bin of=$DEV bs=4 seek=31503616 count=10776 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=31503616 count=10776 +cmp /tmp/chk.bin $BASE/010_010_Measuring11_11.bin + +echo "== 011_Measuring12_12 == +dd if=$BASE/011_011_Measuring12_12.bin of=$DEV bs=1 seek=125605888 count=54415 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=125605888 count=54415 +cmp /tmp/chk.bin $BASE/011_011_Measuring12_12.bin + +echo "== 012_Measuring13_13 == +dd if=$BASE/012_012_Measuring13_13.bin of=$DEV bs=1 seek=125539840 count=65693 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=125539840 count=65693 +cmp /tmp/chk.bin $BASE/012_012_Measuring13_13.bin + +echo "== 013_Measuring14_14 == +dd if=$BASE/013_013_Measuring14_14.bin of=$DEV bs=1 seek=125463552 count=75926 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=125463552 count=75926 +cmp /tmp/chk.bin $BASE/013_013_Measuring14_14.bin + +echo "== 014_Measuring15_15 == +dd if=$BASE/014_014_Measuring15_15.bin of=$DEV bs=1 seek=125372928 count=90514 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=125372928 count=90514 +cmp /tmp/chk.bin $BASE/014_014_Measuring15_15.bin + +echo "== 015_Measuring16_16 == +dd if=$BASE/015_015_Measuring16_16.bin of=$DEV bs=1 seek=125251584 count=120902 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=125251584 count=120902 +cmp /tmp/chk.bin $BASE/015_015_Measuring16_16.bin + +echo "== 016_Measuring17_17 == +dd if=$BASE/016_016_Measuring17_17.bin of=$DEV bs=4 seek=31280896 count=31895 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=31280896 count=31895 +cmp /tmp/chk.bin $BASE/016_016_Measuring17_17.bin + +echo "== 017_Measuring18_18 == +dd if=$BASE/017_017_Measuring18_18.bin of=$DEV bs=1 seek=124991488 count=131691 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=124991488 count=131691 +cmp /tmp/chk.bin $BASE/017_017_Measuring18_18.bin + +echo "== 018_Measuring19_19 == +dd if=$BASE/018_018_Measuring19_19.bin of=$DEV bs=1 seek=124854784 count=136658 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=124854784 count=136658 +cmp /tmp/chk.bin $BASE/018_018_Measuring19_19.bin + +echo "== 019_Measuring20_20 == +dd if=$BASE/019_019_Measuring20_20.bin of=$DEV bs=1 seek=124708352 count=141610 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=124708352 count=141610 +cmp /tmp/chk.bin $BASE/019_019_Measuring20_20.bin + +echo "== 020_Measuring21_21 == +dd if=$BASE/020_020_Measuring21_21.bin of=$DEV bs=1 seek=124561920 count=145939 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=124561920 count=145939 +cmp /tmp/chk.bin $BASE/020_020_Measuring21_21.bin + +echo "== 021_Measuring22_22 == +dd if=$BASE/021_021_Measuring22_22.bin of=$DEV bs=1 seek=124407296 count=154315 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=124407296 count=154315 +cmp /tmp/chk.bin $BASE/021_021_Measuring22_22.bin + +echo "== 022_Measuring23_23 == +dd if=$BASE/022_022_Measuring23_23.bin of=$DEV bs=1 seek=124248064 count=158922 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=124248064 count=158922 +cmp /tmp/chk.bin $BASE/022_022_Measuring23_23.bin + +echo "== 023_Measuring24_24 == +dd if=$BASE/023_023_Measuring24_24.bin of=$DEV bs=1 seek=124083712 count=164011 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=124083712 count=164011 +cmp /tmp/chk.bin $BASE/023_023_Measuring24_24.bin + +echo "== 024_Measuring25_25 == +dd if=$BASE/024_024_Measuring25_25.bin of=$DEV bs=1 seek=123916288 count=166983 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=123916288 count=166983 +cmp /tmp/chk.bin $BASE/024_024_Measuring25_25.bin + +echo "== 025_Measuring26_26 == +dd if=$BASE/025_025_Measuring26_26.bin of=$DEV bs=1 seek=123746816 count=169431 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=123746816 count=169431 +cmp /tmp/chk.bin $BASE/025_025_Measuring26_26.bin + +echo "== 026_Measuring27_27 == +dd if=$BASE/026_026_Measuring27_27.bin of=$DEV bs=1 seek=123574784 count=171709 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=123574784 count=171709 +cmp /tmp/chk.bin $BASE/026_026_Measuring27_27.bin + +echo "== 027_Measuring28_28 == +dd if=$BASE/027_027_Measuring28_28.bin of=$DEV bs=1 seek=123400704 count=173905 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=123400704 count=173905 +cmp /tmp/chk.bin $BASE/027_027_Measuring28_28.bin + +echo "== 028_Measuring29_29 == +dd if=$BASE/028_028_Measuring29_29.bin of=$DEV bs=1 seek=123225088 count=175146 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=123225088 count=175146 +cmp /tmp/chk.bin $BASE/028_028_Measuring29_29.bin + +echo "== 029_Measuring30_30 == +dd if=$BASE/029_029_Measuring30_30.bin of=$DEV bs=1 seek=123042816 count=176699 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=123042816 count=176699 +cmp /tmp/chk.bin $BASE/029_029_Measuring30_30.bin + +echo "== 030_Measuring31_31 == +dd if=$BASE/030_030_Measuring31_31.bin of=$DEV bs=4 seek=30716032 count=44562 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=30716032 count=44562 +cmp /tmp/chk.bin $BASE/030_030_Measuring31_31.bin + +echo "== 031_Measuring32_32 == +dd if=$BASE/031_031_Measuring32_32.bin of=$DEV bs=4 seek=30670848 count=45167 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=30670848 count=45167 +cmp /tmp/chk.bin $BASE/031_031_Measuring32_32.bin + +echo "== 032_Measuring33_33 == +dd if=$BASE/032_032_Measuring33_33.bin of=$DEV bs=1 seek=122497024 count=186322 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=122497024 count=186322 +cmp /tmp/chk.bin $BASE/032_032_Measuring33_33.bin + +echo "== 033_Measuring34_34 == +dd if=$BASE/033_033_Measuring34_34.bin of=$DEV bs=4 seek=30577152 count=47065 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=30577152 count=47065 +cmp /tmp/chk.bin $BASE/033_033_Measuring34_34.bin + +echo "== 034_Measuring35_35 == +dd if=$BASE/034_034_Measuring35_35.bin of=$DEV bs=1 seek=122118656 count=189862 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=122118656 count=189862 +cmp /tmp/chk.bin $BASE/034_034_Measuring35_35.bin + +echo "== 035_Measuring36_36 == +dd if=$BASE/035_035_Measuring36_36.bin of=$DEV bs=1 seek=121927680 count=190691 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=121927680 count=190691 +cmp /tmp/chk.bin $BASE/035_035_Measuring36_36.bin + +echo "== 036_Measuring37_37 == +dd if=$BASE/036_036_Measuring37_37.bin of=$DEV bs=1 seek=121735680 count=191837 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=121735680 count=191837 +cmp /tmp/chk.bin $BASE/036_036_Measuring37_37.bin + +echo "== 037_Measuring38_38 == +dd if=$BASE/037_037_Measuring38_38.bin of=$DEV bs=1 seek=121542144 count=193189 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=121542144 count=193189 +cmp /tmp/chk.bin $BASE/037_037_Measuring38_38.bin + +echo "== 038_Measuring39_39 == +dd if=$BASE/038_038_Measuring39_39.bin of=$DEV bs=1 seek=121347072 count=194939 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=121347072 count=194939 +cmp /tmp/chk.bin $BASE/038_038_Measuring39_39.bin + +echo "== 039_Measuring40_40 == +dd if=$BASE/039_039_Measuring40_40.bin of=$DEV bs=1 seek=121146368 count=195239 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=121146368 count=195239 +cmp /tmp/chk.bin $BASE/039_039_Measuring40_40.bin + +echo "== 040_Measuring41_41 == +dd if=$BASE/040_040_Measuring41_41.bin of=$DEV bs=1 seek=120950272 count=195793 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=120950272 count=195793 +cmp /tmp/chk.bin $BASE/040_040_Measuring41_41.bin + +echo "== 041_Measuring42_42 == +dd if=$BASE/041_041_Measuring42_42.bin of=$DEV bs=1 seek=120754176 count=196046 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=120754176 count=196046 +cmp /tmp/chk.bin $BASE/041_041_Measuring42_42.bin + +echo "== 042_Measuring43_43 == +dd if=$BASE/042_042_Measuring43_43.bin of=$DEV bs=1 seek=120558080 count=196021 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=120558080 count=196021 +cmp /tmp/chk.bin $BASE/042_042_Measuring43_43.bin + +echo "== 043_Measuring44_44 == +dd if=$BASE/043_043_Measuring44_44.bin of=$DEV bs=1 seek=120361472 count=196086 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=120361472 count=196086 +cmp /tmp/chk.bin $BASE/043_043_Measuring44_44.bin + +echo "== 044_Measuring45_45 == +dd if=$BASE/044_044_Measuring45_45.bin of=$DEV bs=4 seek=30041344 count=48913 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=30041344 count=48913 +cmp /tmp/chk.bin $BASE/044_044_Measuring45_45.bin + +echo "== 045_Measuring46_46 == +dd if=$BASE/045_045_Measuring46_46.bin of=$DEV bs=1 seek=119969280 count=195974 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=119969280 count=195974 +cmp /tmp/chk.bin $BASE/045_045_Measuring46_46.bin + +echo "== 046_Measuring47_47 == +dd if=$BASE/046_046_Measuring47_47.bin of=$DEV bs=1 seek=119773184 count=195999 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=119773184 count=195999 +cmp /tmp/chk.bin $BASE/046_046_Measuring47_47.bin + +echo "== 047_Measuring48_48 == +dd if=$BASE/047_047_Measuring48_48.bin of=$DEV bs=1 seek=119577088 count=195847 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=119577088 count=195847 +cmp /tmp/chk.bin $BASE/047_047_Measuring48_48.bin + +echo "== 048_Measuring49_49 == +dd if=$BASE/048_048_Measuring49_49.bin of=$DEV bs=1 seek=119380992 count=195899 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=119380992 count=195899 +cmp /tmp/chk.bin $BASE/048_048_Measuring49_49.bin + +echo "== 049_Measuring50_50 == +dd if=$BASE/049_049_Measuring50_50.bin of=$DEV bs=1 seek=119177728 count=196154 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=119177728 count=196154 +cmp /tmp/chk.bin $BASE/049_049_Measuring50_50.bin + +echo "== 050_Measuring51_51 == +dd if=$BASE/050_050_Measuring51_51.bin of=$DEV bs=1 seek=118981120 count=196246 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=118981120 count=196246 +cmp /tmp/chk.bin $BASE/050_050_Measuring51_51.bin + +echo "== 051_Measuring52_52 == +dd if=$BASE/051_051_Measuring52_52.bin of=$DEV bs=1 seek=118785024 count=196042 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=118785024 count=196042 +cmp /tmp/chk.bin $BASE/051_051_Measuring52_52.bin + +echo "== 052_Measuring53_53 == +dd if=$BASE/052_052_Measuring53_53.bin of=$DEV bs=1 seek=118588928 count=195985 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=118588928 count=195985 +cmp /tmp/chk.bin $BASE/052_052_Measuring53_53.bin + +echo "== 053_Measuring54_54 == +dd if=$BASE/053_053_Measuring54_54.bin of=$DEV bs=1 seek=118392320 count=196538 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=118392320 count=196538 +cmp /tmp/chk.bin $BASE/053_053_Measuring54_54.bin + +echo "== 054_Measuring55_55 == +dd if=$BASE/054_054_Measuring55_55.bin of=$DEV bs=1 seek=118198272 count=193546 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=118198272 count=193546 +cmp /tmp/chk.bin $BASE/054_054_Measuring55_55.bin + +echo "== 055_Measuring56_56 == +dd if=$BASE/055_055_Measuring56_56.bin of=$DEV bs=4 seek=29501056 count=48496 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=29501056 count=48496 +cmp /tmp/chk.bin $BASE/055_055_Measuring56_56.bin + +echo "== 056_Measuring57_57 == +dd if=$BASE/056_056_Measuring57_57.bin of=$DEV bs=1 seek=117810176 count=193954 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=117810176 count=193954 +cmp /tmp/chk.bin $BASE/056_056_Measuring57_57.bin + +echo "== 057_Measuring58_58 == +dd if=$BASE/057_057_Measuring58_58.bin of=$DEV bs=1 seek=117616128 count=193929 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=117616128 count=193929 +cmp /tmp/chk.bin $BASE/057_057_Measuring58_58.bin + +echo "== 058_Measuring59_59 == +dd if=$BASE/058_058_Measuring59_59.bin of=$DEV bs=1 seek=117422080 count=193761 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=117422080 count=193761 +cmp /tmp/chk.bin $BASE/058_058_Measuring59_59.bin + +echo "== 059_Measuring60_60 == +dd if=$BASE/059_059_Measuring60_60.bin of=$DEV bs=1 seek=117218816 count=194029 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=117218816 count=194029 +cmp /tmp/chk.bin $BASE/059_059_Measuring60_60.bin + +echo "== 060_Measuring61_61 == +dd if=$BASE/060_060_Measuring61_61.bin of=$DEV bs=1 seek=117029376 count=189089 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=117029376 count=189089 +cmp /tmp/chk.bin $BASE/060_060_Measuring61_61.bin + +echo "== 061_Measuring62_62 == +dd if=$BASE/061_061_Measuring62_62.bin of=$DEV bs=1 seek=116838400 count=190474 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=116838400 count=190474 +cmp /tmp/chk.bin $BASE/061_061_Measuring62_62.bin + +echo "== 062_Measuring63_63 == +dd if=$BASE/062_062_Measuring63_63.bin of=$DEV bs=4 seek=29161856 count=47687 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=29161856 count=47687 +cmp /tmp/chk.bin $BASE/062_062_Measuring63_63.bin + +echo "== 063_Measuring64_64 == +dd if=$BASE/063_063_Measuring64_64.bin of=$DEV bs=1 seek=116455936 count=190975 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=116455936 count=190975 +cmp /tmp/chk.bin $BASE/063_063_Measuring64_64.bin + +echo "== 064_Measuring65_65 == +dd if=$BASE/064_064_Measuring65_65.bin of=$DEV bs=1 seek=116263424 count=192023 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=116263424 count=192023 +cmp /tmp/chk.bin $BASE/064_064_Measuring65_65.bin + +echo "== 065_Measuring66_66 == +dd if=$BASE/065_065_Measuring66_66.bin of=$DEV bs=1 seek=116071424 count=191903 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=116071424 count=191903 +cmp /tmp/chk.bin $BASE/065_065_Measuring66_66.bin + +echo "== 066_Measuring67_67 == +dd if=$BASE/066_066_Measuring67_67.bin of=$DEV bs=1 seek=115888640 count=182358 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=115888640 count=182358 +cmp /tmp/chk.bin $BASE/066_066_Measuring67_67.bin + +echo "== 067_Measuring68_68 == +dd if=$BASE/067_067_Measuring68_68.bin of=$DEV bs=1 seek=115704320 count=183906 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=115704320 count=183906 +cmp /tmp/chk.bin $BASE/067_067_Measuring68_68.bin + +echo "== 068_Measuring69_69 == +dd if=$BASE/068_068_Measuring69_69.bin of=$DEV bs=1 seek=115518976 count=185033 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=115518976 count=185033 +cmp /tmp/chk.bin $BASE/068_068_Measuring69_69.bin + +echo "== 069_Measuring70_70 == +dd if=$BASE/069_069_Measuring70_70.bin of=$DEV bs=1 seek=115321856 count=184662 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=115321856 count=184662 +cmp /tmp/chk.bin $BASE/069_069_Measuring70_70.bin + +echo "== 070_Measuring71_71 == +dd if=$BASE/070_070_Measuring71_71.bin of=$DEV bs=1 seek=115136512 count=184877 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=115136512 count=184877 +cmp /tmp/chk.bin $BASE/070_070_Measuring71_71.bin + +echo "== 071_Measuring72_72 == +dd if=$BASE/071_071_Measuring72_72.bin of=$DEV bs=1 seek=114951680 count=184574 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=114951680 count=184574 +cmp /tmp/chk.bin $BASE/071_071_Measuring72_72.bin + +echo "== 072_Measuring73_73 == +dd if=$BASE/072_072_Measuring73_73.bin of=$DEV bs=1 seek=114770432 count=180773 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=114770432 count=180773 +cmp /tmp/chk.bin $BASE/072_072_Measuring73_73.bin + +echo "== 073_Measuring74_74 == +dd if=$BASE/073_073_Measuring74_74.bin of=$DEV bs=4 seek=28647424 count=45157 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=28647424 count=45157 +cmp /tmp/chk.bin $BASE/073_073_Measuring74_74.bin + +echo "== 074_Measuring75_75 == +dd if=$BASE/074_074_Measuring75_75.bin of=$DEV bs=1 seek=114409472 count=179919 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=114409472 count=179919 +cmp /tmp/chk.bin $BASE/074_074_Measuring75_75.bin + +echo "== 075_Measuring76_76 == +dd if=$BASE/075_075_Measuring76_76.bin of=$DEV bs=1 seek=114228736 count=180517 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=114228736 count=180517 +cmp /tmp/chk.bin $BASE/075_075_Measuring76_76.bin + +echo "== 076_Measuring77_77 == +dd if=$BASE/076_076_Measuring77_77.bin of=$DEV bs=1 seek=114048000 count=180677 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=114048000 count=180677 +cmp /tmp/chk.bin $BASE/076_076_Measuring77_77.bin + +echo "== 077_Measuring78_78 == +dd if=$BASE/077_077_Measuring78_78.bin of=$DEV bs=1 seek=113867264 count=180537 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=113867264 count=180537 +cmp /tmp/chk.bin $BASE/077_077_Measuring78_78.bin + +echo "== 078_Measuring79_79 == +dd if=$BASE/078_078_Measuring79_79.bin of=$DEV bs=1 seek=113696256 count=170518 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=113696256 count=170518 +cmp /tmp/chk.bin $BASE/078_078_Measuring79_79.bin + +echo "== 079_Measuring80_80 == +dd if=$BASE/079_079_Measuring80_80.bin of=$DEV bs=1 seek=113507840 count=170059 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=113507840 count=170059 +cmp /tmp/chk.bin $BASE/079_079_Measuring80_80.bin + +echo "== 080_Measuring81_81 == +dd if=$BASE/080_080_Measuring81_81.bin of=$DEV bs=1 seek=113336320 count=171247 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=113336320 count=171247 +cmp /tmp/chk.bin $BASE/080_080_Measuring81_81.bin + +echo "== 081_Measuring82_82 == +dd if=$BASE/081_081_Measuring82_82.bin of=$DEV bs=1 seek=113165312 count=170939 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=113165312 count=170939 +cmp /tmp/chk.bin $BASE/081_081_Measuring82_82.bin + +echo "== 082_Measuring83_83 == +dd if=$BASE/082_082_Measuring83_83.bin of=$DEV bs=1 seek=112994816 count=170193 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=112994816 count=170193 +cmp /tmp/chk.bin $BASE/082_082_Measuring83_83.bin + +echo "== 083_Measuring84_84 == +dd if=$BASE/083_083_Measuring84_84.bin of=$DEV bs=1 seek=112840704 count=153998 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=112840704 count=153998 +cmp /tmp/chk.bin $BASE/083_083_Measuring84_84.bin + +echo "== 084_Measuring85_85 == +dd if=$BASE/084_084_Measuring85_85.bin of=$DEV bs=4 seek=28170880 count=39261 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=28170880 count=39261 +cmp /tmp/chk.bin $BASE/084_084_Measuring85_85.bin + +echo "== 085_Measuring86_86 == +dd if=$BASE/085_085_Measuring86_86.bin of=$DEV bs=4 seek=28131200 count=39545 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=28131200 count=39545 +cmp /tmp/chk.bin $BASE/085_085_Measuring86_86.bin + +echo "== 086_Measuring87_87 == +dd if=$BASE/086_086_Measuring87_87.bin of=$DEV bs=1 seek=112364544 count=160025 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=112364544 count=160025 +cmp /tmp/chk.bin $BASE/086_086_Measuring87_87.bin + +echo "== 087_Measuring88_88 == +dd if=$BASE/087_087_Measuring88_88.bin of=$DEV bs=1 seek=112203264 count=160963 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=112203264 count=160963 +cmp /tmp/chk.bin $BASE/087_087_Measuring88_88.bin + +echo "== 088_Measuring89_89 == +dd if=$BASE/088_088_Measuring89_89.bin of=$DEV bs=1 seek=112039936 count=163153 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=112039936 count=163153 +cmp /tmp/chk.bin $BASE/088_088_Measuring89_89.bin + +echo "== 089_Measuring90_90 == +dd if=$BASE/089_089_Measuring90_90.bin of=$DEV bs=1 seek=111863808 count=152329 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=111863808 count=152329 +cmp /tmp/chk.bin $BASE/089_089_Measuring90_90.bin + +echo "== 090_Measuring91_91 == +dd if=$BASE/090_090_Measuring91_91.bin of=$DEV bs=1 seek=111712256 count=151129 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=111712256 count=151129 +cmp /tmp/chk.bin $BASE/090_090_Measuring91_91.bin + +echo "== 091_Measuring92_92 == +dd if=$BASE/091_091_Measuring92_92.bin of=$DEV bs=1 seek=111562240 count=149917 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=111562240 count=149917 +cmp /tmp/chk.bin $BASE/091_091_Measuring92_92.bin + +echo "== 092_Measuring93_93 == +dd if=$BASE/092_092_Measuring93_93.bin of=$DEV bs=1 seek=111413248 count=148930 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=111413248 count=148930 +cmp /tmp/chk.bin $BASE/092_092_Measuring93_93.bin + +echo "== 093_Measuring94_94 == +dd if=$BASE/093_093_Measuring94_94.bin of=$DEV bs=4 seek=27816192 count=37010 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=27816192 count=37010 +cmp /tmp/chk.bin $BASE/093_093_Measuring94_94.bin + +echo "== 094_Measuring95_95 == +dd if=$BASE/094_094_Measuring95_95.bin of=$DEV bs=4 seek=27779712 count=36439 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=27779712 count=36439 +cmp /tmp/chk.bin $BASE/094_094_Measuring95_95.bin + +echo "== 095_Measuring96_96 == +dd if=$BASE/095_095_Measuring96_96.bin of=$DEV bs=4 seek=27749504 count=30142 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=27749504 count=30142 +cmp /tmp/chk.bin $BASE/095_095_Measuring96_96.bin + +echo "== 096_Measuring97_97 == +dd if=$BASE/096_096_Measuring97_97.bin of=$DEV bs=1 seek=110875648 count=121902 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=110875648 count=121902 +cmp /tmp/chk.bin $BASE/096_096_Measuring97_97.bin + +echo "== 097_Measuring98_98 == +dd if=$BASE/097_097_Measuring98_98.bin of=$DEV bs=1 seek=110754304 count=121294 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=110754304 count=121294 +cmp /tmp/chk.bin $BASE/097_097_Measuring98_98.bin + +echo "== 098_Measuring99_99 == +dd if=$BASE/098_098_Measuring99_99.bin of=$DEV bs=1 seek=110638592 count=115589 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=110638592 count=115589 +cmp /tmp/chk.bin $BASE/098_098_Measuring99_99.bin + +echo "== 099_Measuring100_100 == +dd if=$BASE/099_099_Measuring100_100.bin of=$DEV bs=1 seek=126764032 count=110723 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=126764032 count=110723 +cmp /tmp/chk.bin $BASE/099_099_Measuring100_100.bin + +echo "== 100_Measuring101_101 == +dd if=$BASE/100_100_Measuring101_101.bin of=$DEV bs=4 seek=31664128 count=26810 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=31664128 count=26810 +cmp /tmp/chk.bin $BASE/100_100_Measuring101_101.bin + +echo "== 101_Measuring102_102 == +dd if=$BASE/101_101_Measuring102_102.bin of=$DEV bs=1 seek=126569984 count=86055 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=126569984 count=86055 +cmp /tmp/chk.bin $BASE/101_101_Measuring102_102.bin + +echo "== 102_Measuring103_103 == +dd if=$BASE/102_102_Measuring103_103.bin of=$DEV bs=1 seek=126484480 count=85118 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=126484480 count=85118 +cmp /tmp/chk.bin $BASE/102_102_Measuring103_103.bin + +echo "== 103_Measuring104_104 == +dd if=$BASE/103_103_Measuring104_104.bin of=$DEV bs=4 seek=31600000 count=21003 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=31600000 count=21003 +cmp /tmp/chk.bin $BASE/103_103_Measuring104_104.bin + +echo "== 104_Measuring105_105 == +dd if=$BASE/104_104_Measuring105_105.bin of=$DEV bs=4 seek=31579648 count=20267 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=31579648 count=20267 +cmp /tmp/chk.bin $BASE/104_104_Measuring105_105.bin + +echo "== 105_Measuring106_106 == +dd if=$BASE/105_105_Measuring106_106.bin of=$DEV bs=1 seek=126240256 count=77823 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=126240256 count=77823 +cmp /tmp/chk.bin $BASE/105_105_Measuring106_106.bin + +echo "== 106_Measuring107_107 == +dd if=$BASE/106_106_Measuring107_107.bin of=$DEV bs=1 seek=126164480 count=75410 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=126164480 count=75410 +cmp /tmp/chk.bin $BASE/106_106_Measuring107_107.bin + +echo "== 107_Measuring108_108 == +dd if=$BASE/107_107_Measuring108_108.bin of=$DEV bs=4 seek=31527936 count=13116 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=31527936 count=13116 +cmp /tmp/chk.bin $BASE/107_107_Measuring108_108.bin + +echo "== 108_Measuring109_109 == +dd if=$BASE/108_108_Measuring109_109.bin of=$DEV bs=4 seek=31514496 count=13398 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=31514496 count=13398 +cmp /tmp/chk.bin $BASE/108_108_Measuring109_109.bin + +echo "== 109_Measuring110_110 == +dd if=$BASE/109_109_Measuring110_110.bin of=$DEV bs=1 seek=125961728 count=52559 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=125961728 count=52559 +cmp /tmp/chk.bin $BASE/109_109_Measuring110_110.bin + +echo "== 110_Measuring111_111 == +dd if=$BASE/110_110_Measuring111_111.bin of=$DEV bs=1 seek=125914624 count=46569 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=125914624 count=46569 +cmp /tmp/chk.bin $BASE/110_110_Measuring111_111.bin + +echo "== 111_Measuring112_112 == +dd if=$BASE/111_111_Measuring112_112.bin of=$DEV bs=1 seek=125874176 count=40243 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=125874176 count=40243 +cmp /tmp/chk.bin $BASE/111_111_Measuring112_112.bin + +echo "== 112_Measuring113_113 == +dd if=$BASE/112_112_Measuring113_113.bin of=$DEV bs=1 seek=125828608 count=45031 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=125828608 count=45031 +cmp /tmp/chk.bin $BASE/112_112_Measuring113_113.bin + +echo "== 113_Measuring114_114 == +dd if=$BASE/113_113_Measuring114_114.bin of=$DEV bs=4 seek=31447040 count=10034 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=31447040 count=10034 +cmp /tmp/chk.bin $BASE/113_113_Measuring114_114.bin + +echo "== 114_Measuring115_115 == +dd if=$BASE/114_114_Measuring115_115.bin of=$DEV bs=4 seek=31438720 count=8231 +dd if=$DEV of=/tmp/chk.bin bs=4 skip=31438720 count=8231 +cmp /tmp/chk.bin $BASE/114_114_Measuring115_115.bin + +echo "== 115_Measuring116_116 == +dd if=$BASE/115_115_Measuring116_116.bin of=$DEV bs=1 seek=125727744 count=27031 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=125727744 count=27031 +cmp /tmp/chk.bin $BASE/115_115_Measuring116_116.bin + +echo "== 116_Measuring117_117 == +dd if=$BASE/116_116_Measuring117_117.bin of=$DEV bs=1 seek=125703168 count=24325 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=125703168 count=24325 +cmp /tmp/chk.bin $BASE/116_116_Measuring117_117.bin + +echo "== 117_Measuring118_118 == +dd if=$BASE/117_117_Measuring118_118.bin of=$DEV bs=1 seek=125680128 count=22811 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=125680128 count=22811 +cmp /tmp/chk.bin $BASE/117_117_Measuring118_118.bin + +echo "== 118_Measuring119_119 == +dd if=$BASE/118_118_Measuring119_119.bin of=$DEV bs=1 seek=125660672 count=19025 +dd if=$DEV of=/tmp/chk.bin bs=1 skip=125660672 count=19025 +cmp /tmp/chk.bin $BASE/118_118_Measuring119_119.bin + +rm /tmp/chk.bin +echo "restore done - run reboot to apply" diff --git a/s4e-pressure-anim/rollback.sh b/s4e-pressure-anim/rollback.sh new file mode 100644 index 0000000..cfcb764 --- /dev/null +++ b/s4e-pressure-anim/rollback.sh @@ -0,0 +1,244 @@ +#!/bin/sh +# 写回 S4 原厂图标 + +DEV=/dev/app +BASE=/data/s5icons/payload_rollback + +echo "target check" +ls -l $DEV + +echo "== canary_superblock == +dd if=$BASE/000_canary_superblock.bin of=$DEV bs=4 seek=0 count=8 +echo "== 003_Measuring4_4 == +dd if=$BASE/001_003_Measuring4_4.bin of=$DEV bs=1 seek=121341952 count=5078 +echo "== 004_Measuring5_5 == +dd if=$BASE/002_004_Measuring5_5.bin of=$DEV bs=1 seek=119374336 count=6327 +echo "== 005_Measuring6_6 == +dd if=$BASE/003_005_Measuring6_6.bin of=$DEV bs=4 seek=29353344 count=2109 +echo "== 006_Measuring7_7 == +dd if=$BASE/004_006_Measuring7_7.bin of=$DEV bs=1 seek=115506688 count=11785 +echo "== 007_Measuring8_8 == +dd if=$BASE/005_007_Measuring8_8.bin of=$DEV bs=1 seek=113678336 count=17510 +echo "== 008_Measuring9_9 == +dd if=$BASE/006_008_Measuring9_9.bin of=$DEV bs=1 seek=112016384 count=23011 +echo "== 009_Measuring10_10 == +dd if=$BASE/007_009_Measuring10_10.bin of=$DEV bs=1 seek=126875136 count=31377 +echo "== 010_Measuring11_11 == +dd if=$BASE/008_010_Measuring11_11.bin of=$DEV bs=4 seek=31503616 count=10776 +echo "== 011_Measuring12_12 == +dd if=$BASE/009_011_Measuring12_12.bin of=$DEV bs=1 seek=125605888 count=54415 +echo "== 012_Measuring13_13 == +dd if=$BASE/010_012_Measuring13_13.bin of=$DEV bs=1 seek=125539840 count=65693 +echo "== 013_Measuring14_14 == +dd if=$BASE/011_013_Measuring14_14.bin of=$DEV bs=1 seek=125463552 count=75926 +echo "== 014_Measuring15_15 == +dd if=$BASE/012_014_Measuring15_15.bin of=$DEV bs=1 seek=125372928 count=90514 +echo "== 015_Measuring16_16 == +dd if=$BASE/013_015_Measuring16_16.bin of=$DEV bs=1 seek=125251584 count=120902 +echo "== 016_Measuring17_17 == +dd if=$BASE/014_016_Measuring17_17.bin of=$DEV bs=4 seek=31280896 count=31895 +echo "== 017_Measuring18_18 == +dd if=$BASE/015_017_Measuring18_18.bin of=$DEV bs=1 seek=124991488 count=131691 +echo "== 018_Measuring19_19 == +dd if=$BASE/016_018_Measuring19_19.bin of=$DEV bs=1 seek=124854784 count=136658 +echo "== 019_Measuring20_20 == +dd if=$BASE/017_019_Measuring20_20.bin of=$DEV bs=1 seek=124708352 count=141610 +echo "== 020_Measuring21_21 == +dd if=$BASE/018_020_Measuring21_21.bin of=$DEV bs=1 seek=124561920 count=145939 +echo "== 021_Measuring22_22 == +dd if=$BASE/019_021_Measuring22_22.bin of=$DEV bs=1 seek=124407296 count=154315 +echo "== 022_Measuring23_23 == +dd if=$BASE/020_022_Measuring23_23.bin of=$DEV bs=1 seek=124248064 count=158922 +echo "== 023_Measuring24_24 == +dd if=$BASE/021_023_Measuring24_24.bin of=$DEV bs=1 seek=124083712 count=164011 +echo "== 024_Measuring25_25 == +dd if=$BASE/022_024_Measuring25_25.bin of=$DEV bs=1 seek=123916288 count=166983 +echo "== 025_Measuring26_26 == +dd if=$BASE/023_025_Measuring26_26.bin of=$DEV bs=1 seek=123746816 count=169431 +echo "== 026_Measuring27_27 == +dd if=$BASE/024_026_Measuring27_27.bin of=$DEV bs=1 seek=123574784 count=171709 +echo "== 027_Measuring28_28 == +dd if=$BASE/025_027_Measuring28_28.bin of=$DEV bs=1 seek=123400704 count=173905 +echo "== 028_Measuring29_29 == +dd if=$BASE/026_028_Measuring29_29.bin of=$DEV bs=1 seek=123225088 count=175146 +echo "== 029_Measuring30_30 == +dd if=$BASE/027_029_Measuring30_30.bin of=$DEV bs=1 seek=123042816 count=176699 +echo "== 030_Measuring31_31 == +dd if=$BASE/028_030_Measuring31_31.bin of=$DEV bs=4 seek=30716032 count=44562 +echo "== 031_Measuring32_32 == +dd if=$BASE/029_031_Measuring32_32.bin of=$DEV bs=4 seek=30670848 count=45167 +echo "== 032_Measuring33_33 == +dd if=$BASE/030_032_Measuring33_33.bin of=$DEV bs=1 seek=122497024 count=186322 +echo "== 033_Measuring34_34 == +dd if=$BASE/031_033_Measuring34_34.bin of=$DEV bs=4 seek=30577152 count=47065 +echo "== 034_Measuring35_35 == +dd if=$BASE/032_034_Measuring35_35.bin of=$DEV bs=1 seek=122118656 count=189862 +echo "== 035_Measuring36_36 == +dd if=$BASE/033_035_Measuring36_36.bin of=$DEV bs=1 seek=121927680 count=190691 +echo "== 036_Measuring37_37 == +dd if=$BASE/034_036_Measuring37_37.bin of=$DEV bs=1 seek=121735680 count=191837 +echo "== 037_Measuring38_38 == +dd if=$BASE/035_037_Measuring38_38.bin of=$DEV bs=1 seek=121542144 count=193189 +echo "== 038_Measuring39_39 == +dd if=$BASE/036_038_Measuring39_39.bin of=$DEV bs=1 seek=121347072 count=194939 +echo "== 039_Measuring40_40 == +dd if=$BASE/037_039_Measuring40_40.bin of=$DEV bs=1 seek=121146368 count=195239 +echo "== 040_Measuring41_41 == +dd if=$BASE/038_040_Measuring41_41.bin of=$DEV bs=1 seek=120950272 count=195793 +echo "== 041_Measuring42_42 == +dd if=$BASE/039_041_Measuring42_42.bin of=$DEV bs=1 seek=120754176 count=196046 +echo "== 042_Measuring43_43 == +dd if=$BASE/040_042_Measuring43_43.bin of=$DEV bs=1 seek=120558080 count=196021 +echo "== 043_Measuring44_44 == +dd if=$BASE/041_043_Measuring44_44.bin of=$DEV bs=1 seek=120361472 count=196086 +echo "== 044_Measuring45_45 == +dd if=$BASE/042_044_Measuring45_45.bin of=$DEV bs=4 seek=30041344 count=48913 +echo "== 045_Measuring46_46 == +dd if=$BASE/043_045_Measuring46_46.bin of=$DEV bs=1 seek=119969280 count=195974 +echo "== 046_Measuring47_47 == +dd if=$BASE/044_046_Measuring47_47.bin of=$DEV bs=1 seek=119773184 count=195999 +echo "== 047_Measuring48_48 == +dd if=$BASE/045_047_Measuring48_48.bin of=$DEV bs=1 seek=119577088 count=195847 +echo "== 048_Measuring49_49 == +dd if=$BASE/046_048_Measuring49_49.bin of=$DEV bs=1 seek=119380992 count=195899 +echo "== 049_Measuring50_50 == +dd if=$BASE/047_049_Measuring50_50.bin of=$DEV bs=1 seek=119177728 count=196154 +echo "== 050_Measuring51_51 == +dd if=$BASE/048_050_Measuring51_51.bin of=$DEV bs=1 seek=118981120 count=196246 +echo "== 051_Measuring52_52 == +dd if=$BASE/049_051_Measuring52_52.bin of=$DEV bs=1 seek=118785024 count=196042 +echo "== 052_Measuring53_53 == +dd if=$BASE/050_052_Measuring53_53.bin of=$DEV bs=1 seek=118588928 count=195985 +echo "== 053_Measuring54_54 == +dd if=$BASE/051_053_Measuring54_54.bin of=$DEV bs=1 seek=118392320 count=196538 +echo "== 054_Measuring55_55 == +dd if=$BASE/052_054_Measuring55_55.bin of=$DEV bs=1 seek=118198272 count=193546 +echo "== 055_Measuring56_56 == +dd if=$BASE/053_055_Measuring56_56.bin of=$DEV bs=4 seek=29501056 count=48496 +echo "== 056_Measuring57_57 == +dd if=$BASE/054_056_Measuring57_57.bin of=$DEV bs=1 seek=117810176 count=193954 +echo "== 057_Measuring58_58 == +dd if=$BASE/055_057_Measuring58_58.bin of=$DEV bs=1 seek=117616128 count=193929 +echo "== 058_Measuring59_59 == +dd if=$BASE/056_058_Measuring59_59.bin of=$DEV bs=1 seek=117422080 count=193761 +echo "== 059_Measuring60_60 == +dd if=$BASE/057_059_Measuring60_60.bin of=$DEV bs=1 seek=117218816 count=194029 +echo "== 060_Measuring61_61 == +dd if=$BASE/058_060_Measuring61_61.bin of=$DEV bs=1 seek=117029376 count=189089 +echo "== 061_Measuring62_62 == +dd if=$BASE/059_061_Measuring62_62.bin of=$DEV bs=1 seek=116838400 count=190474 +echo "== 062_Measuring63_63 == +dd if=$BASE/060_062_Measuring63_63.bin of=$DEV bs=4 seek=29161856 count=47687 +echo "== 063_Measuring64_64 == +dd if=$BASE/061_063_Measuring64_64.bin of=$DEV bs=1 seek=116455936 count=190975 +echo "== 064_Measuring65_65 == +dd if=$BASE/062_064_Measuring65_65.bin of=$DEV bs=1 seek=116263424 count=192023 +echo "== 065_Measuring66_66 == +dd if=$BASE/063_065_Measuring66_66.bin of=$DEV bs=1 seek=116071424 count=191903 +echo "== 066_Measuring67_67 == +dd if=$BASE/064_066_Measuring67_67.bin of=$DEV bs=1 seek=115888640 count=182358 +echo "== 067_Measuring68_68 == +dd if=$BASE/065_067_Measuring68_68.bin of=$DEV bs=1 seek=115704320 count=183906 +echo "== 068_Measuring69_69 == +dd if=$BASE/066_068_Measuring69_69.bin of=$DEV bs=1 seek=115518976 count=185033 +echo "== 069_Measuring70_70 == +dd if=$BASE/067_069_Measuring70_70.bin of=$DEV bs=1 seek=115321856 count=184662 +echo "== 070_Measuring71_71 == +dd if=$BASE/068_070_Measuring71_71.bin of=$DEV bs=1 seek=115136512 count=184877 +echo "== 071_Measuring72_72 == +dd if=$BASE/069_071_Measuring72_72.bin of=$DEV bs=1 seek=114951680 count=184574 +echo "== 072_Measuring73_73 == +dd if=$BASE/070_072_Measuring73_73.bin of=$DEV bs=1 seek=114770432 count=180773 +echo "== 073_Measuring74_74 == +dd if=$BASE/071_073_Measuring74_74.bin of=$DEV bs=4 seek=28647424 count=45157 +echo "== 074_Measuring75_75 == +dd if=$BASE/072_074_Measuring75_75.bin of=$DEV bs=1 seek=114409472 count=179919 +echo "== 075_Measuring76_76 == +dd if=$BASE/073_075_Measuring76_76.bin of=$DEV bs=1 seek=114228736 count=180517 +echo "== 076_Measuring77_77 == +dd if=$BASE/074_076_Measuring77_77.bin of=$DEV bs=1 seek=114048000 count=180677 +echo "== 077_Measuring78_78 == +dd if=$BASE/075_077_Measuring78_78.bin of=$DEV bs=1 seek=113867264 count=180537 +echo "== 078_Measuring79_79 == +dd if=$BASE/076_078_Measuring79_79.bin of=$DEV bs=1 seek=113696256 count=170518 +echo "== 079_Measuring80_80 == +dd if=$BASE/077_079_Measuring80_80.bin of=$DEV bs=1 seek=113507840 count=170059 +echo "== 080_Measuring81_81 == +dd if=$BASE/078_080_Measuring81_81.bin of=$DEV bs=1 seek=113336320 count=171247 +echo "== 081_Measuring82_82 == +dd if=$BASE/079_081_Measuring82_82.bin of=$DEV bs=1 seek=113165312 count=170939 +echo "== 082_Measuring83_83 == +dd if=$BASE/080_082_Measuring83_83.bin of=$DEV bs=1 seek=112994816 count=170193 +echo "== 083_Measuring84_84 == +dd if=$BASE/081_083_Measuring84_84.bin of=$DEV bs=1 seek=112840704 count=153998 +echo "== 084_Measuring85_85 == +dd if=$BASE/082_084_Measuring85_85.bin of=$DEV bs=4 seek=28170880 count=39261 +echo "== 085_Measuring86_86 == +dd if=$BASE/083_085_Measuring86_86.bin of=$DEV bs=4 seek=28131200 count=39545 +echo "== 086_Measuring87_87 == +dd if=$BASE/084_086_Measuring87_87.bin of=$DEV bs=1 seek=112364544 count=160025 +echo "== 087_Measuring88_88 == +dd if=$BASE/085_087_Measuring88_88.bin of=$DEV bs=1 seek=112203264 count=160963 +echo "== 088_Measuring89_89 == +dd if=$BASE/086_088_Measuring89_89.bin of=$DEV bs=1 seek=112039936 count=163153 +echo "== 089_Measuring90_90 == +dd if=$BASE/087_089_Measuring90_90.bin of=$DEV bs=1 seek=111863808 count=152329 +echo "== 090_Measuring91_91 == +dd if=$BASE/088_090_Measuring91_91.bin of=$DEV bs=1 seek=111712256 count=151129 +echo "== 091_Measuring92_92 == +dd if=$BASE/089_091_Measuring92_92.bin of=$DEV bs=1 seek=111562240 count=149917 +echo "== 092_Measuring93_93 == +dd if=$BASE/090_092_Measuring93_93.bin of=$DEV bs=1 seek=111413248 count=148930 +echo "== 093_Measuring94_94 == +dd if=$BASE/091_093_Measuring94_94.bin of=$DEV bs=4 seek=27816192 count=37010 +echo "== 094_Measuring95_95 == +dd if=$BASE/092_094_Measuring95_95.bin of=$DEV bs=4 seek=27779712 count=36439 +echo "== 095_Measuring96_96 == +dd if=$BASE/093_095_Measuring96_96.bin of=$DEV bs=4 seek=27749504 count=30142 +echo "== 096_Measuring97_97 == +dd if=$BASE/094_096_Measuring97_97.bin of=$DEV bs=1 seek=110875648 count=121902 +echo "== 097_Measuring98_98 == +dd if=$BASE/095_097_Measuring98_98.bin of=$DEV bs=1 seek=110754304 count=121294 +echo "== 098_Measuring99_99 == +dd if=$BASE/096_098_Measuring99_99.bin of=$DEV bs=1 seek=110638592 count=115589 +echo "== 099_Measuring100_100 == +dd if=$BASE/097_099_Measuring100_100.bin of=$DEV bs=1 seek=126764032 count=110723 +echo "== 100_Measuring101_101 == +dd if=$BASE/098_100_Measuring101_101.bin of=$DEV bs=4 seek=31664128 count=26810 +echo "== 101_Measuring102_102 == +dd if=$BASE/099_101_Measuring102_102.bin of=$DEV bs=1 seek=126569984 count=86055 +echo "== 102_Measuring103_103 == +dd if=$BASE/100_102_Measuring103_103.bin of=$DEV bs=1 seek=126484480 count=85118 +echo "== 103_Measuring104_104 == +dd if=$BASE/101_103_Measuring104_104.bin of=$DEV bs=4 seek=31600000 count=21003 +echo "== 104_Measuring105_105 == +dd if=$BASE/102_104_Measuring105_105.bin of=$DEV bs=4 seek=31579648 count=20267 +echo "== 105_Measuring106_106 == +dd if=$BASE/103_105_Measuring106_106.bin of=$DEV bs=1 seek=126240256 count=77823 +echo "== 106_Measuring107_107 == +dd if=$BASE/104_106_Measuring107_107.bin of=$DEV bs=1 seek=126164480 count=75410 +echo "== 107_Measuring108_108 == +dd if=$BASE/105_107_Measuring108_108.bin of=$DEV bs=4 seek=31527936 count=13116 +echo "== 108_Measuring109_109 == +dd if=$BASE/106_108_Measuring109_109.bin of=$DEV bs=4 seek=31514496 count=13398 +echo "== 109_Measuring110_110 == +dd if=$BASE/107_109_Measuring110_110.bin of=$DEV bs=1 seek=125961728 count=52559 +echo "== 110_Measuring111_111 == +dd if=$BASE/108_110_Measuring111_111.bin of=$DEV bs=1 seek=125914624 count=46569 +echo "== 111_Measuring112_112 == +dd if=$BASE/109_111_Measuring112_112.bin of=$DEV bs=1 seek=125874176 count=40243 +echo "== 112_Measuring113_113 == +dd if=$BASE/110_112_Measuring113_113.bin of=$DEV bs=1 seek=125828608 count=45031 +echo "== 113_Measuring114_114 == +dd if=$BASE/111_113_Measuring114_114.bin of=$DEV bs=4 seek=31447040 count=10034 +echo "== 114_Measuring115_115 == +dd if=$BASE/112_114_Measuring115_115.bin of=$DEV bs=4 seek=31438720 count=8231 +echo "== 115_Measuring116_116 == +dd if=$BASE/113_115_Measuring116_116.bin of=$DEV bs=1 seek=125727744 count=27031 +echo "== 116_Measuring117_117 == +dd if=$BASE/114_116_Measuring117_117.bin of=$DEV bs=1 seek=125703168 count=24325 +echo "== 117_Measuring118_118 == +dd if=$BASE/115_117_Measuring118_118.bin of=$DEV bs=1 seek=125680128 count=22811 +echo "== 118_Measuring119_119 == +dd if=$BASE/116_118_Measuring119_119.bin of=$DEV bs=1 seek=125660672 count=19025 +echo "rollback done - run reboot to apply" diff --git a/s4e-pressure-anim/tools/build_faces.py b/s4e-pressure-anim/tools/build_faces.py new file mode 100644 index 0000000..e8122ee --- /dev/null +++ b/s4e-pressure-anim/tools/build_faces.py @@ -0,0 +1,81 @@ +"""overlay + 官方模板 -> Compiler.exe -> .face(两份盘共用同一个 watchface id) + +要点(都是踩过的坑): + * preview.png 必须是 326x326,Compiler.exe 会直接拒绝别的尺寸 + * watchfaceId 要带机型家族前缀:S4 家族 = 362150xxx + * 用 Python 的 copytree 拷目录,别用 PowerShell 的 Copy-Item -Recurse(会拍平) + * replace 盘与 restore 盘用**同一个 id**:装一份就顶掉另一份,设备上只占一份空间 +""" +import os, json, shutil, subprocess, sys +from paths import WORK, TEMPLATE + +TPL = TEMPLATE +OVER = os.path.join(WORK, 'overlay') +BUILD = os.path.join(WORK, 'build') + +TARGETS = {'S4PAnim': ('362150102', 'S5 41mm 压力检测动画 -> S4 eSIM(只写负载)'), + 'S4PAnimRestore': ('362150102', 'S4 eSIM 压力检测动画还原盘(只写原厂字节)')} + + +def prepare(name): + oid, _ = TARGETS[name] + ov = os.path.join(OVER, name) + dst = os.path.join(BUILD, name) + if not os.path.exists(TPL): + raise SystemExit('找不到官方模板:%s\n请把 FangAiden/LuaDevTemplate 放到 vendor/ 下(见 README)' % TPL) + if os.path.exists(dst): + shutil.rmtree(dst) + shutil.copytree(TPL, dst) + + old = os.path.join(dst, 'watchface', 'fprj', 'LuaDevTemplate.fprj') + if os.path.exists(old): + os.remove(old) + + cfg = json.load(open(os.path.join(ov, 'watchface.config.json'), encoding='utf-8')) + cfg['watchfaceId'] = oid + json.dump(cfg, open(os.path.join(dst, 'watchface.config.json'), 'w', encoding='utf-8'), + indent=4, ensure_ascii=False) + + shutil.copyfile(os.path.join(ov, 'watchface', 'fprj', name + '.fprj'), + os.path.join(dst, 'watchface', 'fprj', name + '.fprj')) + shutil.copyfile(os.path.join(ov, 'watchface', 'fprj', 'images', 'preview.png'), + os.path.join(dst, 'watchface', 'fprj', 'images', 'preview.png')) + shutil.copyfile(os.path.join(ov, 'watchface', 'fprj', 'app', 'lua', 'main.lua'), + os.path.join(dst, 'watchface', 'fprj', 'app', 'lua', 'main.lua')) + + img = os.path.join(dst, 'watchface', 'fprj', 'app', 'images') + os.makedirs(img, exist_ok=True) + ovimg = os.path.join(ov, 'watchface', 'fprj', 'app', 'images') + for d in sorted(x for x in os.listdir(ovimg) if os.path.isdir(os.path.join(ovimg, x))): + tgt = os.path.join(img, d) + if os.path.exists(tgt): + shutil.rmtree(tgt) + shutil.copytree(os.path.join(ovimg, d), tgt) + print(' %s: app/images/%s/ -> %d files' % (name, d, len(os.listdir(tgt)))) + return dst + + +def build(dst): + ps = os.path.join(dst, 'scripts', 'build_face.ps1') + r = subprocess.run(['powershell', '-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', ps], + capture_output=True, text=True, errors='replace') + for l in [x for x in (r.stdout or '').splitlines() if x.strip()][-6:]: + print(' |', l) + if r.returncode != 0: + print(' !! build failed, rc=', r.returncode) + return r.returncode == 0 + + +if __name__ == '__main__': + only = sys.argv[1] if len(sys.argv) > 1 else None + for name in TARGETS: + if only and name != only: + continue + print('== %s (id=%s) ==' % (name, TARGETS[name][0])) + dst = prepare(name) + build(dst) + face = os.path.join(dst, 'bin', name + '.face') + if os.path.exists(face): + print(' OK %s %d bytes' % (face, os.path.getsize(face))) + else: + print(' missing output') diff --git a/s4e-pressure-anim/tools/check_clip.py b/s4e-pressure-anim/tools/check_clip.py new file mode 100644 index 0000000..fc8ad4c --- /dev/null +++ b/s4e-pressure-anim/tools/check_clip.py @@ -0,0 +1,58 @@ +"""用真实渲染结果判"会不会被圆屏切掉"。 + +做法:渲染一张**不套圆形遮罩**的 RGBA,然后量所有非透明像素到圆心的距离。 +这样自动考虑字体实际字宽、折行、圆角、图片缩放 —— 比按元素外框估算准得多。 +""" +import os, sys, math +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from PIL import Image, ImageDraw +from render_ui import draw_label, W, H +from pages import PAGES + +CX = CY = 233.0 +R = 233.0 + + +def render_raw(items): + img = Image.new('RGBA', (W, None) if False else (W, H), (0, 0, 0, 0)) + d = ImageDraw.Draw(img) + from render_ui import _rgb, font + for it in items: + k = it.get('kind') + if k == 'rect': + box = [it['x'], it['y'], it['x'] + it['w'] - 1, it['y'] + it['h'] - 1] + r = it.get('radius', 0) + if r: + d.rounded_rectangle(box, radius=r, fill=_rgb(it['bg']) + (255,)) + else: + d.rectangle(box, fill=_rgb(it['bg']) + (255,)) + elif k == 'circle': + d.ellipse([it['x'], it['y'], it['x'] + it['w'] - 1, it['y'] + it['h'] - 1], + fill=_rgb(it['bg']) + (255,)) + elif k == 'image': + if os.path.exists(it['src']): + im = Image.open(it['src']).convert('RGBA').resize((it['w'], it['h']), Image.LANCZOS) + img.alpha_composite(im, (it['x'], it['y'])) + elif k == 'label': + draw_label(img, d, it) + return img + + +print(f'{"page":<10} {"max r":>7} {"outside px":>11} worst point / note') +print('-' * 78) +for name, items in PAGES.items(): + im = render_raw(items) + px = im.load() + worst, wpt, outside = 0.0, None, 0 + for y in range(H): + for x in range(W): + if px[x, y][3] > 8: + d = math.hypot(x - CX, y - CY) + if d > worst: + worst, wpt = d, (x, y) + if d > R: + outside += 1 + note = '全部在圆内' if worst <= R else f'会被切 {outside} px' + print(f'{name:<10} {worst:>7.1f} {outside:>11} {wpt} {note}') +print('-' * 78) +print(f'圆心 ({CX:.0f},{CY:.0f}) 半径 {R:.0f} 画布 {W}x{H}') diff --git a/s4e-pressure-anim/tools/finalize_faces.py b/s4e-pressure-anim/tools/finalize_faces.py new file mode 100644 index 0000000..cfb30b3 --- /dev/null +++ b/s4e-pressure-anim/tools/finalize_faces.py @@ -0,0 +1,72 @@ +"""产出最终 .face(两份盘各一份,都不带工具链变体)。 + + * Compiler.exe 写 byte5 = 0x00 + 自己的随机 id 到 40..49 + * 官方 set_face_id.ps1 会把 byte5 改成 10 并写 id;固件里 25 个真表盘 byte5 ∈ {0,1} + => 忠实产物 = Compiler.exe 原始输出,只替换 40..49(b5=0 那份) + 体积原因:本盘各 ~16 MB,不再额外复制工具链变体(要的话去掉下面的 SKIP_TOOLCHAIN 即可) +""" +import os, sys, shutil, subprocess, hashlib +from paths import WORK, DIST + +BUILD = os.path.join(WORK, 'build') +TARGETS = {'S4PAnim': '362150102', 'S4PAnimRestore': '362150102'} +SKIP_TOOLCHAIN = True + + +def compile_raw(name): + w = os.path.join(BUILD, name) + exe = os.path.join(w, 'watchface', 'tools', 'Compiler.exe') + if not os.path.exists(exe): + print(' %s: 跳过(没有构建树,先跑 build_faces.py %s)' % (name, name)) + return None + fprj = os.path.join(w, 'watchface', 'fprj', name + '.fprj') + out = os.path.join(WORK, 'raw_' + name) + if os.path.exists(out): + shutil.rmtree(out) + os.makedirs(out) + subprocess.run([exe, '-b', fprj, out, name + '.face', TARGETS[name]], + capture_output=True, text=True, errors='replace') + p = os.path.join(out, name + '.face') + return p if os.path.exists(p) else None + + +def patch_id(src, dst, wid): + d = bytearray(open(src, 'rb').read()) + if d[0:4] != b'\x5a\xa5\x34\x12': + raise SystemExit('bad magic') + if len(wid) > 10: + raise SystemExit('id too long') + d[40:50] = wid.encode() + b'\x00' * (10 - len(wid)) + open(dst, 'wb').write(bytes(d)) + return dst + + +def main(): + os.makedirs(DIST, exist_ok=True) + only = sys.argv[1] if len(sys.argv) > 1 else None + report = [] + for name, wid in TARGETS.items(): + if only and only != name: + continue + raw = compile_raw(name) + if not raw: + continue + faithful = patch_id(raw, os.path.join(DIST, name + '.face'), wid) + if not SKIP_TOOLCHAIN: + tc_src = os.path.join(BUILD, name, 'bin', name + '.face') + tc = os.path.join(DIST, name + '_toolchain.face') + if os.path.exists(tc_src): + shutil.copyfile(tc_src, tc) + for p in (faithful,): + if os.path.exists(p): + d = open(p, 'rb').read() + report.append((os.path.basename(p), len(d), d[4], d[5], d[40:50], + hashlib.sha256(d).hexdigest()[:16])) + print('%-28s%11s b4 b5 id sha256[:16]' % ('file', 'bytes')) + for f, n, b4, b5, sid, h in report: + print('%-28s%11d %-3d %-3d %-18r %s' % (f, n, b4, b5, sid, h)) + print('->', DIST) + + +if __name__ == '__main__': + main() diff --git a/s4e-pressure-anim/tools/gen_ui_lua.py b/s4e-pressure-anim/tools/gen_ui_lua.py new file mode 100644 index 0000000..4f54589 --- /dev/null +++ b/s4e-pressure-anim/tools/gen_ui_lua.py @@ -0,0 +1,127 @@ +"""规范化构建:表盘源码 + 资源 -> .face(两遍,自动回填 resource.bin 内的偏移) + + main.lua <- **源码**,含 __UIIMG__ / __PAY__ / __RB__ / __ROLE__ 占位符 + .work/overlay/ <- 中间产物(回填后的 Lua 落在这里) + .work/build/ <- 编译工作区 + dist/.face <- 交付物 + +两份盘(replace / restore)共用同一套流程,只是负载来源不同: + blob_replace/{payload,fp}.bin -> S4PAnim.face + blob_restore/{payload,fp}.bin -> S4PAnimRestore.face +rb 表里放的是**另一侧字节前 64 B 的指纹**(只给版本校验用),所以一个 .face 只装一份负载。 +""" +import os, re, sys, json, subprocess +from paths import PROJ, WORK + +ROLE = (sys.argv[1] if len(sys.argv) > 1 else 'replace').lower() +NAME = 'S4PAnim' if ROLE == 'replace' else 'S4PAnimRestore' +RECDIR = 'payload' if ROLE == 'replace' else 'payload_restore' +TOOLS = os.path.dirname(os.path.abspath(__file__)) +SRC = os.path.join(PROJ, 'main.lua') +OVERLAY_APP = os.path.join(WORK, 'overlay', NAME, 'watchface', 'fprj', 'app') +RESOLVED = os.path.join(OVERLAY_APP, 'lua', 'main.lua') +FACE = os.path.join(WORK, 'build', NAME, 'bin', NAME + '.face') +PY = sys.executable + +UINAMES = ['back', 'ic_help', 'ic_replace', 'ic_about', 'ic_check', 'ic_rollback', + 'ic_qq', 'logo'] + + +def run(script, *args): + r = subprocess.run([PY, os.path.join(TOOLS, script)] + list(args), + capture_output=True, text=True, encoding='utf-8', errors='replace') + if r.returncode != 0: + print(r.stdout[-2000:]); print(r.stderr[-2000:]) + raise SystemExit(f'{script} 执行失败') + return r.stdout + + +def find_blob(face, blob): + hits, start = [], 0 + while True: + i = face.find(blob, start) + if i < 0: + break + hits.append(i) + start = i + 1 + if len(hits) > 4: + break + return hits + + +def main(): + tpl = open(SRC, encoding='utf-8').read() + if '__UIIMG__' not in tpl: + raise SystemExit('main.lua 里没有占位符 —— 它应该是源码模板,不要把回填后的版本存回去') + print('=== %s(role=%s)===' % (NAME, ROLE)) + blobs = json.load(open(os.path.join(PROJ, 'blob_' + ROLE, 'offsets.json'), encoding='utf-8')) + pay_blob = open(os.path.join(PROJ, 'blob_' + ROLE, 'payload.bin'), 'rb').read() + fp_blob = open(os.path.join(PROJ, 'blob_' + ROLE, 'fp.bin'), 'rb').read() + ml, mlf = blobs['magic_len'], blobs['magic_fp_len'] + + print('第 1 遍:用占位符编一次,量出资源在 resource.bin 里的位置') + run('make_overlays.py', ROLE) + run('build_faces.py', NAME) + face = open(FACE, 'rb').read() + print(' face %d bytes' % len(face)) + + ui_rows, bad = [], 0 + for name in UINAMES: + blob = open(os.path.join(PROJ, 'ui_images', name + '.bin'), 'rb').read() + hits = find_blob(face, blob) + if len(hits) != 1: + print(' !! %s 命中 %d 次(应为 1)' % (name, len(hits))); bad += 1 + if hits: + ui_rows.append((hits[0], len(blob), name)) + if bad: + raise SystemExit('有资源无法唯一定位') + pay_base = face.find(pay_blob[:15]) + fp_base = face.find(fp_blob[:15]) + for tag, blob, base in (('payload', pay_blob, pay_base), ('fp', fp_blob, fp_base)): + if face.count(blob[:15]) != 1: + raise SystemExit('%s 魔数命中 %d 次(应为 1)' % (tag, face.count(blob[:15]))) + if face.find(blob) != base: + raise SystemExit('%s 不是原样连续存放' % tag) + pay = [(pay_base + ml + r['rel'], r['target'], r['length']) for r in blobs['records']] + rb = [(fp_base + mlf + r['fprel'], r['target'], r['fplen']) for r in blobs['records']] + print(' 单条目:payload @0x%x(%d B)/ fp @0x%x(%d B),共 %d 条记录' + % (pay_base, len(pay_blob), fp_base, len(fp_blob), len(pay))) + + ui_txt = '\n'.join(' {%d,%d,"%s"},' % (o, l, n) for o, l, n in ui_rows) + pay_txt = ' pay = {\n' + '\n'.join(' {%d,%d,%d},' % t for t in pay) + '\n },' + rb_txt = ' rb = {\n' + '\n'.join(' {%d,%d,%d},' % t for t in rb) + '\n },' + resolved = (tpl.replace('__UIIMG__', ui_txt).replace('__PAY__', pay_txt) + .replace('__RB__', rb_txt).replace('__ROLE__', ROLE)) + assert '__UIIMG__' not in resolved and '__ROLE__' not in resolved + os.makedirs(os.path.dirname(RESOLVED), exist_ok=True) + open(RESOLVED, 'w', encoding='utf-8', newline='\n').write(resolved) + print('第 2 遍:回填后的 Lua(%d bytes),重新编译' % len(resolved)) + run('build_faces.py', NAME) + print(run('finalize_faces.py', NAME).strip()) + + face2 = open(FACE, 'rb').read() + drift = 0 + for name in UINAMES: + blob = open(os.path.join(PROJ, 'ui_images', name + '.bin'), 'rb').read() + hits = find_blob(face2, blob) + row = [r for r in ui_rows if r[2] == name] + if not row or not hits or hits[0] != row[0][0]: + print(' !! 偏移漂移 %s' % name); drift += 1 + if face2.find(pay_blob) != pay_base or face2.find(fp_blob) != fp_base: + print(' !! 负载偏移漂移'); drift += 1 + files = [(f, open(os.path.join(PROJ, RECDIR, f), 'rb').read()) + for f in sorted(os.listdir(os.path.join(PROJ, RECDIR))) if f.endswith('.bin')] + ok = sum(1 for off, tgt, ln in pay + if any(len(x) == ln and face2[off:off + ln] == x for _, x in files)) + if ok != len(pay): + print(' !! pay 只有 %d/%d 条吻合' % (ok, len(pay))); drift += 1 + okf = sum(1 for (off, tgt, ln), r in zip(rb, blobs['records']) + if face2[off:off + ln] == fp_blob[mlf + r['fprel']:mlf + r['fprel'] + ln]) + if okf != len(rb): + print(' !! fp 只有 %d/%d 条吻合' % (okf, len(rb))); drift += 1 + print('偏移校验:%s' % ('全部吻合,0 漂移' if drift == 0 else '%d 处异常' % drift)) + print('交付物:', os.path.join(PROJ, 'dist', NAME + '.face')) + + +if __name__ == '__main__': + main() diff --git a/s4e-pressure-anim/tools/iconpack.py b/s4e-pressure-anim/tools/iconpack.py new file mode 100644 index 0000000..4012c6e --- /dev/null +++ b/s4e-pressure-anim/tools/iconpack.py @@ -0,0 +1,447 @@ +"""Build / apply a system-app launcher-icon replacement for a Vela `vela_app.bin`. + +Why "same length" is the whole ball game +---------------------------------------- +ROMFS packs superblock -> inode -> name -> file data strictly back to back, and +every `next` / `spec` pointer is an ABSOLUTE offset. If a replacement file +changes length by even one byte, every later offset shifts and the image is +corrupt. So a replacement must encode to EXACTLY the original byte length: + + * source PNG == original dimensions -> encode, lengths match, done. + * source PNG != original dimensions -> two options: + (default) resize to the original dimensions (icon renders identically) + --pad keep the source dimensions, zero-pad the container to the + original length (renders smaller unless the launcher scales) + +Usage: + iconpack.py list + iconpack.py make [--pad] [--patched] + iconpack.py apply +""" +import os, sys, json, struct, hashlib, shutil +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from romfs import Romfs, img_header +from lvgl_bin import rle_compress, pad_rle +from PIL import Image + +CF_ARGB8888, CF_I8, MAGIC = 0x10, 0x0a, 0x19 +FMT = {CF_ARGB8888: 'ARGB8888', CF_I8: 'I8'} +RESAMPLE = Image.LANCZOS + + +# ----------------------------------------------------------------- encoders +def encode_argb8888(img): + rgba = img.convert('RGBA') + w, h = rgba.size + px = rgba.load() + out = bytearray() + for y in range(h): + row = bytearray() + for x in range(w): + r, g, b, a = px[x, y] + row += bytes((b, g, r, a)) + out += row + return bytes(out) + + +def encode_i8(img, alpha_levels=16, sharpen_up=0.0): + """I8 encoder with an ALPHA-AWARE palette. + + The naive version quantised RGB only and then stored one *averaged* alpha + per palette entry. Any colour shared by an opaque body pixel and a + semi-transparent edge pixel collapsed to a mid alpha, and fully transparent + pixels inherited that same entry -- so the transparent margin became faintly + visible (measured: perpetual_calendar haze 0.000 -> 1.000, i.e. a grey veil + around the whole icon). + + Here the palette is built over (quantised RGB, quantised alpha) PAIRS, so a + colour can own several entries at different alphas and alpha 0 stays alpha 0. + """ + rgba = img.convert('RGBA') + w, h = rgba.size + src = rgba.load() + + # average colour of the visible pixels: transparent pixels must not invent + # their own colour clusters + rs = gs = bs = n = 0 + for y in range(h): + for x in range(w): + r, g, b, a = src[x, y] + if a > 0: + rs += r; gs += g; bs += b; n += 1 + avg = (rs // n, gs // n, bs // n) if n else (0, 0, 0) + + step = max(1, 256 // max(1, alpha_levels)) + flat = Image.new('RGB', (w, h)) + fp = flat.load() + qa = [[0] * w for _ in range(h)] + for y in range(h): + for x in range(w): + r, g, b, a = src[x, y] + fp[x, y] = (r, g, b) if a > 0 else avg + q = int(round(a / float(step))) * step + qa[y][x] = 255 if q > 255 else q + + p = flat.quantize(colors=256, method=Image.MEDIANCUT) + pal_rgb = p.getpalette()[:768] + idx = p.tobytes() + + cnt = {} + for y in range(h): + row = y * w + for x in range(w): + k = (idx[row + x], qa[y][x]) + cnt[k] = cnt.get(k, 0) + 1 + keep = sorted(cnt.items(), key=lambda kv: -kv[1])[:256] + + pal = bytearray() + remap = {} + by_rgb = {} + for slot, ((pi, al), _c) in enumerate(keep): + r = pal_rgb[pi * 3] if pi * 3 + 2 < len(pal_rgb) else 0 + g = pal_rgb[pi * 3 + 1] if pi * 3 + 2 < len(pal_rgb) else 0 + b = pal_rgb[pi * 3 + 2] if pi * 3 + 2 < len(pal_rgb) else 0 + pal += bytes((b, g, r, al)) + remap[(pi, al)] = slot + by_rgb.setdefault(pi, []).append((al, slot)) + removed = 256 - len(keep) + if removed > 0: + pal += b'\x00\x00\x00\x00' * removed + for pi in by_rgb: + by_rgb[pi].sort() + + out = bytearray(w * h) + for y in range(h): + row = y * w + for x in range(w): + pi = idx[row + x] + al = qa[y][x] + s = remap.get((pi, al)) + if s is None: + cand = by_rgb.get(pi) + if cand: + s = min(cand, key=lambda t: abs(t[0] - al))[1] + else: + s = 0 + out[row + x] = s + return bytes(pal) + bytes(out) + + +def encode_i8_palette(img, pal_bgra, alpha_tol=12): + """Quantise to an EXISTING 256-entry BGRA palette (as stored in an I8 blob). + + Reusing the source icon's own palette preserves the artwork's gradient shades + and its anti-aliased edge levels. A freshly clustered palette cannot: the + plate gradient eats the 256 entries, the edge collapses to a few alpha steps + and dropped pixels land on wrong entries -- measured on remote_camera as a + jagged plate rim plus stray coloured speckles. + + The search is constrained by alpha so that + * fully transparent source pixels can only pick transparent entries + (no veil, no speckles), and + * fully opaque source pixels can only pick opaque entries + (the body never goes translucent). + """ + rgba = img.convert('RGBA') + w, h = rgba.size + px = rgba.load() + + ent = [(pal_bgra[i * 4 + 2], pal_bgra[i * 4 + 1], + pal_bgra[i * 4], pal_bgra[i * 4 + 3]) for i in range(256)] + clear = [i for i, e in enumerate(ent) if e[3] == 0] + if not clear: # guarantee a transparent entry exists + worst = min(range(256), key=lambda i: ent[i][3]) + ent[worst] = (0, 0, 0, 0) + clear = [worst] + solid = [i for i, e in enumerate(ent) if e[3] >= 248] + by_alpha = {} + for i, e in enumerate(ent): + by_alpha.setdefault(e[3], []).append(i) + + def premul(e): + r, g, b, a = e + return (r * a // 255, g * a // 255, b * a // 255, a) + + P = [premul(e) for e in ent] + cache = {} + out = bytearray(w * h) + for y in range(h): + row = y * w + for x in range(w): + r, g, b, a = px[x, y] + key = (r >> 2, g >> 2, b >> 2, a >> 3) + s = cache.get(key) + if s is None: + if a == 0: + cand = clear + elif a >= 248 and solid: + cand = solid + else: + cand = [] + for da in range(0, alpha_tol + 1): + cand += by_alpha.get(a + da, ()) if a + da <= 255 else () + if da: + cand += by_alpha.get(a - da, ()) if a - da >= 0 else () + if not cand: + cand = range(256) + pr, pg, pb, pa = premul((r, g, b, a)) + best, bd = cand[0], None + for i in cand: + q = P[i] + d = ((pr - q[0]) ** 2 + (pg - q[1]) ** 2 + (pb - q[2]) ** 2 + + 2 * (pa - q[3]) ** 2) + if bd is None or d < bd: + bd, best = d, i + s = best + cache[key] = s + out[row + x] = s + + pal = bytearray() + for (r, g, b, a) in ent: + pal += bytes((b, g, r, a)) + return bytes(pal) + bytes(out) + + +def encode(img, cf, w, h, flags=0, pad_to=None, palette=None): + if cf == CF_ARGB8888: + body = encode_argb8888(img) + elif palette is not None: + body = encode_i8_palette(img, palette) + else: + body = encode_i8(img) + stride = w * (4 if cf == CF_ARGB8888 else 1) + blob = struct.pack(' pad_to: + raise ValueError(f'encoded {len(blob)} > slot {pad_to}') + blob += b'\x00' * (pad_to - len(blob)) + return blob + + +# ----------------------------------------------------------------- helpers +def raw_i8_strided(img, stride, colors=256): + """I8 像素流:1024 B 调色板(BGRA)+ stride*h 个索引(行尾按 header 的 stride 补 0)。 + + RLE 槽位专用:用 FASTOCTREE 自适应减色 + 关闭抖动 —— 颜色越少、相邻像素越容易出现 + 长游程,RLE 流就越短(这是能不能塞进原 csize 的关键)。 + """ + q = img.convert('RGBA').quantize(colors=max(2, min(256, colors)), + method=Image.FASTOCTREE, dither=Image.NONE) + if q.palette.mode == 'RGBA': + pal = list(q.getpalette('RGBA')) + else: + rgb = q.getpalette('RGB') + pal = [v for i in range(256) for v in (rgb[i * 3], rgb[i * 3 + 1], rgb[i * 3 + 2], 255)] + pal = (pal + [0] * 1024)[:1024] + pal_b = bytearray() + for i in range(256): # 存储序:B, G, R, A + r, g, b, a = pal[i * 4:i * 4 + 4] + pal_b += bytes((b, g, r, a)) + idx = q.tobytes() + w, h = img.size + rows = bytearray() + for y in range(h): + rows += idx[y * w:(y + 1) * w] + if stride > w: + rows += b'\x00' * (stride - w) + return bytes(pal_b) + bytes(rows) + + +# 从大到小试:优先保真,装不下再减色 +RLE_COLOR_STEPS = (256, 192, 160, 128, 96, 80, 64, 48, 40, 32, 24, 16, 12, 8, 6, 4) + + +def encode_into_slot(img, stock_blob, slot_len=None, colors=None): + """把 img 编成与 stock_blob **逐字节等长** 的 LVGL v9 blob(沿用原 header)。 + + 真实固件里这些 UI 素材有两种槽位形态,都必须支持,否则 ROMFS 后面所有偏移全平移: + * ARGB8888 未压缩(144x144 图标那种):像素流 + 零填充到原长度 + * I8 + RLE 压缩(walkie_talkie 里几乎全是):量化 -> RLE -> 用 ctrl=0 空指令补齐到原 csize + (颜色数从 256 逐级往下试,取第一个能塞进原 csize 的) + 返回 (blob, how);不支持 / 压缩后仍超长时返回 (None, 原因)。 + """ + h = img_header(stock_blob) + if h is None: + return None, '不是 LVGL bin' + w, ht, cf, flags = h['w'], h['h'], h['cf'], h['flags'] + if slot_len is None: + slot_len = len(stock_blob) + out_img = img.convert('RGBA') + if out_img.size != (w, ht): + # 约定:**长宽比不一致的素材直接不替换**(保持原厂图),而不是拉伸或裁切 —— + # 拉伸会把方块变长方形;裁切会切掉按钮两端。宁可保持原厂,也不给设备塞变形图。 + pw, ph = out_img.size + if abs(pw / ph - w / ht) / (w / ht) > 0.005: + return None, ('长宽比不一致(源 %dx%d = %.3f,槽位 %dx%d = %.3f)-> 按约定跳过,保持原厂' + % (pw, ph, pw / ph, w, ht, w / ht)) + out_img = out_img.resize((w, ht), RESAMPLE) + if cf == CF_ARGB8888 and not (flags & 0x08): + blob = stock_blob[:12] + encode_argb8888(out_img) + if len(blob) > slot_len: + return None, 'ARGB 编码 %d > 槽位 %d' % (len(blob), slot_len) + return blob + b'\x00' * (slot_len - len(blob)), 'ARGB(补 %d)' % (slot_len - len(blob)) + if cf == CF_I8 and (flags & 0x08): + method, csize, usize = struct.unpack(' csize %d(已减到 %d 色)' % (len(comp), csize, n_last) + n, raw, comp = best + comp = comp + bytes((1, raw[-1])) # 把流做到正好 usize 字节,和原厂一致 + padded = pad_rle(comp, csize) + if padded is None: + return None, '补齐失败' + blob = stock_blob[:12] + struct.pack(' inode for every launcher icon. + + alarm/launcher.bin -> alarm + sports/training/launcher.bin -> sports_training + phone/contacts_launcher.bin -> phone_contacts + """ + out = {} + for p, i in r.entries(): + if not p.rsplit('/', 1)[-1].endswith('launcher.bin'): + continue + stem = p[:-4] # drop '.bin' + if stem.endswith('_launcher'): + stem = stem[:-len('_launcher')] + elif stem.endswith('/launcher'): + stem = stem[:-len('/launcher')] + out[stem.replace('/', '_')] = i + return out + + +def describe(r, i): + h = img_header(r.read(i)) + if not h: + return '?', '?', '?' + return f'{h["w"]}x{h["h"]}', FMT.get(h['cf'], f'0x{h["cf"]:02x}'), h + + +def cmd_list(image): + r = Romfs(image) + print(f'{os.path.basename(image)} volume={r.volume} romfs_size={r.sb_size}') + for app, i in sorted(targets(r).items()): + dim, fmt, _ = describe(r, i) + print(f' {app:<26} data@0x{i.data_off:08x} len={i.size:<7} {dim} {fmt}') + + +def cmd_make(image, pngdir, outdir, pad=False, write_patched=False): + r = Romfs(image) + tg = targets(r) + os.makedirs(outdir, exist_ok=True) + payload_dir = os.path.join(outdir, 'payload') + os.makedirs(payload_dir, exist_ok=True) + patch = bytearray() + rollback = bytearray() + records, skipped = [], [] + for fn in sorted(os.listdir(pngdir)): + if not fn.lower().endswith('.png'): + continue + app = fn[:-4] + if app not in tg: + skipped.append((app, 'no such app')) + continue + ino = tg[app] + orig = r.read(ino) + _, fmt, h = describe(r, ino) + if h is None: + skipped.append((app, f'not LVGL image (len={ino.size})')) + continue + src = Image.open(os.path.join(pngdir, fn)).convert('RGBA') + src_dim = f'{src.size[0]}x{src.size[1]}' + resized = False + if src.size != (h['w'], h['h']): + if pad: + tw, th = src.size + blob = encode(src, h['cf'], tw, th, h['flags'], pad_to=len(orig)) + else: + src = src.resize((h['w'], h['h']), RESAMPLE) + tw, th = src.size + resized = True + blob = encode(src, h['cf'], tw, th, h['flags']) + else: + tw, th = src.size + blob = encode(src, h['cf'], tw, th, h['flags']) + if len(blob) != len(orig): + skipped.append((app, f'length {len(blob)} != {len(orig)}')) + continue + patch += struct.pack('12}{"bytes":>8} {"fmt":<10}{"stock":>9}{"source":>9} note') + for m in records: + note = 'resized' if m['resized'] else ('padded' if m['padded'] else '') + print(f'{m["app"]:<26}0x{m["offset"]:08x}{m["length"]:>8} {m["format"]:<10}' + f'{m["stock_dim"]:>9}{m["source_dim"]:>9} {note}') + for a, why in skipped: + print(f' SKIP {a}: {why}') + print(f'\n{len(records)} replacement(s)') + print(f' payload -> {payload_dir}') + print(f' patch -> {os.path.join(outdir, "icons.patch")} ({len(patch)} bytes)') + print(f' rollback -> {os.path.join(outdir, "icons.rollback")} ({len(rollback)} bytes)') + print(f' manifest -> {os.path.join(outdir, "manifest.json")}') + if write_patched: + out = os.path.join(outdir, 'vela_app_patched.bin') + cmd_apply(image, os.path.join(outdir, 'icons.patch'), out) + + +def cmd_apply(image, patch, out): + data = bytearray(open(image, 'rb').read()) + blob = open(patch, 'rb').read() + off = n = 0 + while off < len(blob): + addr, ln = struct.unpack(' {out}') + + +if __name__ == '__main__': + a = sys.argv[1:] + if not a: + print(__doc__) + elif a[0] == 'list': + cmd_list(a[1]) + elif a[0] == 'make': + cmd_make(a[1], a[2], a[3], '--pad' in a, '--patched' in a) + elif a[0] == 'apply': + cmd_apply(a[1], a[2], a[3]) + else: + print(__doc__) diff --git a/s4e-pressure-anim/tools/lua_balance.py b/s4e-pressure-anim/tools/lua_balance.py new file mode 100644 index 0000000..a33ebba --- /dev/null +++ b/s4e-pressure-anim/tools/lua_balance.py @@ -0,0 +1,72 @@ +"""Crude but effective Lua block-balance checker: strips comments and strings, +then counts block openers vs `end`.""" +import sys, re + +def strip(src): + out = [] + i, n = 0, len(src) + while i < n: + c = src[i] + # long bracket comment / string --[[ ]] , --[==[ ]==] + m = re.match(r'--\[(=*)\[', src[i:]) + if m: + close = ']' + m.group(1) + ']' + j = src.find(close, i) + i = n if j < 0 else j + len(close) + out.append(' ') + continue + if src.startswith('--', i): + j = src.find('\n', i) + i = n if j < 0 else j + out.append(' ') + continue + m = re.match(r'\[(=*)\[', src[i:]) + if m: + close = ']' + m.group(1) + ']' + j = src.find(close, i) + i = n if j < 0 else j + len(close) + out.append('""') + continue + if c in '"\'': + j = i + 1 + while j < n: + if src[j] == '\\': + j += 2 + continue + if src[j] == c: + break + j += 1 + i = j + 1 + out.append('""') + continue + out.append(c) + i += 1 + return ''.join(out) + + +def check(path): + src = open(path, encoding='utf-8').read() + code = strip(src) + def cnt(k): + return len(re.findall(r'(? n: + raise ValueError('truncated literal run') + out += data[i:i + nb] + i += nb + else: + cnt = ctrl + if i + blk > n: + raise ValueError('truncated repeat unit') + unit = data[i:i + blk] + i += blk + out += unit * cnt + if out_len is not None and len(out) >= out_len: + break + return bytes(out) + + +def rle_compress(raw, max_run=127): + """LVGL RLE 编码器(rle_decompress 的逆向,blk=1)。 + + 贪心:先找同字节游程(>=2 用「重复」指令),否则攒「原样」指令。 + 输出长度总是 <= 原图像素流,因为最坏情况每条 literal 只多 1 个控制字节。 + """ + out = bytearray() + i, n = 0, len(raw) + while i < n: + v = raw[i] + j = i + 1 + while j < n and raw[j] == v and j - i < max_run: + j += 1 + run = j - i + if run >= 2: + out.append(run); out.append(v); i = j + continue + lit = bytearray() + k = i + while k < n and len(lit) < max_run: + if k + 1 < n and raw[k] == raw[k + 1]: + break + lit.append(raw[k]); k += 1 + if not lit: # 理论上到不了这里 + lit.append(v); k = i + 1 + out.append(0x80 | len(lit)); out += lit + i = k + return bytes(out) + + +def pad_rle(comp, want): + """把 RLE 流补到 want 字节:用 ctrl=0(重复 0 次 = 产出 0 像素)的 2 字节空指令。 + + ctrl=0 是合法指令(`len = 0`,两个越界判断都不触发),所以设备端解码器读完 csize + 字节既不会越界、也不会返回 INVALID —— 这是「等长覆盖」能成立的关键。 + 差值为奇数时先把一条 literal 拆成两条(payload 不变、多 1 个控制字节)。 + """ + d = want - len(comp) + if d < 0: + return None + if d % 2: + b = bytearray(comp) + for idx in range(len(b) - 1, -1, -1): + if b[idx] & 0x80 and (b[idx] & 0x7F) >= 2: + cnt = b[idx] & 0x7F + lit = bytes(b[idx + 1: idx + 1 + cnt]) + half = cnt // 2 + b[idx: idx + 1 + cnt] = (bytes([0x80 | half]) + lit[:half] + + bytes([0x80 | (cnt - half)]) + lit[half:]) + break + else: + return None + comp = bytes(b) + d -= 1 + return comp + b'\x00\x00' * (d // 2) + + +def parse_header(blob): + if len(blob) < 12 or blob[0] != MAGIC: + return None + magic, cf, flags, w, h, stride, rsv = struct.unpack(' S4 eSIM 的 pressure/measure 槽位(等长覆盖)。 + +硬约束:ROMFS 里文件数据长度固定(后面所有文件的偏移都是绝对值),所以 119 个槽位 +必须**逐字节等长**替换;而目标槽位是 464x464 I8 且 **RLE 压缩**,源(q63)是 480x480。 +于是走三步: + 1) q63 帧缩放/量化:按 (颜色数, 缩放比例) 阶梯找一档,让编码结果 <= 槽位 csize + 2) 编码:LVGL RLE 语法(lvgl_bin.rle_compress) + 3) 补齐:ctrl=0 的空指令补到 csize(lvgl_bin.pad_rle)—— 解码器读完 csize 字节 + 既不会越界也不会多出像素 +文件总长 = 12(头) + 12(压缩头) + csize = 原槽位长度 ✓(头里的 csize/usize/method 原样沿用) +""" +import os, sys, json, struct, shutil, hashlib +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from paths import INPUTS, WORK +from romfs import Romfs +import lvgl_bin as L +from PIL import Image, ImageDraw +import numpy as np + +STOCK = os.path.join(INPUTS, 'target', 'vela_app.bin') # S4 eSIM 原厂 +SOURCE = os.path.join(INPUTS, 'source', 'vela_app.bin') # q63(S5 41mm) +OUTDIR = os.path.join(WORK, 'anim') +PATCHED = os.path.join(OUTDIR, 'vela_app_anim.bin') +DIRP = 'pressure/measure/' + +# 从「最清晰」往「最省字节」排;缩放 <1 时把画面缩小居中(四周纯黑,RLE 大幅变短) +# ------------------------------------------------------------------ 比例怎么定 +# 之前用「离散阶梯」逐档试:相邻两帧的预算只差一点,却可能一档跳到 0.6、下一帧跳到 0.22 —— 真机上就是 +# 「忽大忽小」。现在改成:**由槽位预算直接算出连续的比例**,球体大小随预算平滑变化。 +# +# 464x464 的 I8 图,光是全黑背景就要 FLOOR ≈ 3.7 KB(RLE 单游程上限 127 -> 一行至少 4 个 token); +# 球体本身的开销大致正比于「直径^1.57」(实测拟合),满尺寸 32 色约 BALL ≈ 41.6 KB。 +# 于是 scale = ((csize - FLOOR) / BALL) ^ (1/1.57),截到 [0.03, 1.0]。 +FLOOR = 3700.0 +BALL = 41600.0 +POW = 1.57 +TARGET_COLORS = (256, 192, 128, 96, 64, 48, 32) +MIN_COLORS = 32 +KEEP_STOCK_IF_TOO_TIGHT = False # 用户要求:一帧都不保留原厂 + + +def plan_scale(csize): + b = max(0.0, csize - FLOOR) + return min(1.0, max(0.03, (b / BALL) ** (1.0 / POW))) + + +def num_of(path): + return int(path.rsplit('Measuring', 1)[-1].replace('.bin', '')) + + +def collect(rom, prefix): + d = {} + for p, i in rom.entries(): + if i.type == 1 or not p.startswith(prefix) or not p.endswith('.bin'): + continue + d[num_of(p)] = (p, i, rom.read(i)) + return d + + +def usize_tail(comp, raw_len, usize): + """补「重复 N 次」token 把解码长度凑到 usize(原厂流解出的正是 usize,少一字节设备就不画)。""" + short = usize - raw_len + if short <= 0: + return comp + ex = bytearray() + while short > 0: + n = min(short, 127) + ex += bytes((n, 0)) + short -= n + return comp + bytes(ex) + + +def pal4(q): + pal = list(q.getpalette() or []) + pal += [0] * (768 - len(pal)) + out = bytearray() + for k in range(256): + out += bytes((pal[k * 3 + 2], pal[k * 3 + 1], pal[k * 3], 255)) + return bytes(out) + + +def encode(img, w, h, ncol, scale): + im = img + if scale != 1.0: + s = max(8, int(round(w * scale))) + small = img.convert('RGBA').resize((s, s), Image.LANCZOS) + cv = Image.new('RGBA', (w, h), (0, 0, 0, 255)) + cv.paste(small, ((w - s) // 2, (h - s) // 2)) + im = cv + q = im.convert('RGB').quantize(colors=max(1, ncol), method=Image.MEDIANCUT, dither=Image.NONE) + return L.rle_compress(pal4(q) + q.tobytes()), q + + +def main(): + os.makedirs(OUTDIR, exist_ok=True) + stock, src = Romfs(STOCK), Romfs(SOURCE) + slots, srcs = collect(stock, DIRP), collect(src, DIRP) + tl, sl = sorted(slots), sorted(srcs) + print('目标槽位 %d 个(编号 %d..%d,合计 %.1f MB)' % ( + len(tl), tl[0], tl[-1], sum(len(slots[n][2]) for n in tl) / 1048576)) + print('源帧 %d 个(编号 %d..%d,合计 %.1f MB)' % ( + len(sl), sl[0], sl[-1], sum(len(srcs[n][2]) for n in sl) / 1048576)) + h0 = L.parse_header(slots[tl[0]][2]) + print('槽位格式 %dx%d cf=0x%02x flags=0x%02x method=%d' % ( + h0['w'], h0['h'], h0['cf'], h0['flags'], + struct.unpack('= 0.995 else ' 缩放%.2f' % sc) + _, back = L.decode_lvgl_bin(new) + raw_len = len(L.rle_decompress(new[24:24 + csize], 1, None)) + if q is not None: + same = list(back.convert('RGB').getdata()) == list(q.convert('RGB').getdata()) + else: + same = all(pp[:3] == (0, 0, 0) for pp in back.convert('RGB').getdata()) + if same and raw_len == usize: + decode_ck += 1 + else: + print(' !! 槽 %d:解码长度 %d(应 %d)或像素不一致' % (n, raw_len, usize)) + records.append(dict(name='%03d_%s_%d' % (len(records), path.rsplit('/', 1)[-1][:-4], n), + src='measure%d' % qn, offset=ino.data_off, length=len(blob), + data=new, csize=csize, how=tag)) + print('\n阶梯使用情况(颜色数, 缩放 -> 帧数):') + for kk, v in sorted(used.items(), key=lambda kv: -kv[1]): + print(' %3d 色 %s -> %3d 帧' % (kk[0], 'x1.00' if kk[1] == 1.0 else 'x%.2f' % kk[1], v)) + if placeholder: + print(' 全透明兜底(也绝不留原厂):%d 帧' % placeholder) + if kept: + print(' !! 仍保留原厂:%d 帧 %s' % (len(kept), kept[:8])) + print(' 回解逐像素一致:%d / %d' % (decode_ck, len(records) - len(kept))) + + shutil.copyfile(STOCK, PATCHED) + with open(PATCHED, 'r+b') as f: + for r in records: + f.seek(r['offset']) + f.write(r['data']) + manifest = dict( + device='Xiaomi Watch S4 eSIM (o62lte) vela_app.bin', + source='Xiaomi Watch S5 41mm (q63) v4.101.020 pressure/measure frames', + target_dir=DIRP, + mapping='第 i 个槽位(1..%d) <- q63 第 round((i-1)/(n-1)*(m-1))+1 帧(1..%d)' % (len(tl), len(sl)), + image_sha256=hashlib.sha256(stock.data).hexdigest(), + records=[dict(name=r['name'], offset=r['offset'], length=r['length'], + src=r['src'], how=r['how']) for r in records]) + json.dump(manifest, open(os.path.join(OUTDIR, 'manifest.json'), 'w', encoding='utf-8'), + indent=1, ensure_ascii=False) + + # 自证:inode 表逐位不变 + 回放一致 + r2 = Romfs(PATCHED) + a = [(p, i.off, i.next, i.spec, i.size, i.cksum) for p, i in stock.entries()] + b = [(p, i.off, i.next, i.spec, i.size, i.cksum) for p, i in r2.entries()] + print('\ninode 表逐位不变:%s(%d 条)' % (a == b, len(a))) + print('superblock 一致:%s' % (stock.sb_size == r2.sb_size and stock.sb_cksum == r2.sb_cksum)) + sh = open(PATCHED, 'rb').read() + replay = all(sh[r['offset']:r['offset'] + r['length']] == r['data'] for r in records) + print('回放(改后镜像 == 计划字节):%s' % replay) + print('负载合计 %.2f MB;写出 %s' % ( + sum(r['length'] for r in records) / 1048576, os.path.join(OUTDIR, 'manifest.json'))) + + +if __name__ == '__main__': + main() diff --git a/s4e-pressure-anim/tools/make_dd_payload.py b/s4e-pressure-anim/tools/make_dd_payload.py new file mode 100644 index 0000000..ee4d814 --- /dev/null +++ b/s4e-pressure-anim/tools/make_dd_payload.py @@ -0,0 +1,136 @@ +"""按「角色」切负载:replace(只负责替换)/ restore(只负责还原)。 + +拆盘的要点:一个 .face 只装一份负载,体积减半。 + * replace 盘:pay = q63 的帧(新字节);rb = **原厂字节前 64 B 的指纹**(只用于版本校验) + * restore 盘:pay = 原厂字节; rb = **新字节前 64 B 的指纹** +指纹表让「写入前版本校验 + 写入后回读比对」两道保险都还在,代价只有 117*64 ≈ 7.5 KB。 +两份盘用同一个 watchface id,装其中一份就会顶掉另一份,设备上始终只占一份空间。 + +产出: + /payload[_restore]/ NNN_.bin + payload.lua + /blob_/payload.bin 单条目:魔数 + 顺序拼接 + /blob_/fp.bin 指纹单条目 + /flash.sh | restore.sh 线性 nsh 脚本(replace 盘还有 restore.sh 的对应物) + /payload.json 机器可读清单 +""" +import os, sys, json, hashlib +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from paths import PROJ, INPUTS, WORK + +ROLE = (sys.argv[1] if len(sys.argv) > 1 else 'replace').lower() +assert ROLE in ('replace', 'restore'), ROLE +STOCK = os.path.join(INPUTS, 'target', 'vela_app.bin') +PATCHED = os.path.join(WORK, 'anim', 'vela_app_anim.bin') +OUT = PROJ + +recs_in = json.load(open(os.path.join(WORK, 'anim', 'manifest.json'), encoding='utf-8')) +stock = open(STOCK, 'rb').read() +patched = open(PATCHED, 'rb').read() +assert len(stock) == len(patched) + +SRC = patched if ROLE == 'replace' else stock # 要写进设备的字节 +OTHER = stock if ROLE == 'replace' else patched # 另一侧的字节(做指纹) +FPDIR = os.path.join(OUT, 'payload' if ROLE == 'replace' else 'payload_restore') +BLOBDIR = os.path.join(OUT, 'blob_' + ROLE) +FP_LEN = 64 + +# 不再往偏移 0 写那 32 字节的「canary」:写 superblock 头没有意义, +# 而且它极可能就是真机上「总是失败 1」的那一条(偏移 0 的写/读回最容易出岔子)。 +assert stock[:8] == b'-rom1fs-' +records = [] +for r in recs_in['records']: + off, ln = r['offset'], r['length'] + records.append(dict(name=r['name'], offset=off, length=ln, data=SRC[off:off + ln], + changed=(stock[off:off + ln] != patched[off:off + ln]) if ROLE == 'replace' else True, + fp=OTHER[off:off + FP_LEN])) +emitted = [r for r in records if r['changed']] +dropped = [r for r in records if not r['changed']] + + +def write_pack(d): + os.makedirs(d, exist_ok=True) + for f in os.listdir(d): + os.remove(os.path.join(d, f)) + rows = [] + for i, r in enumerate(emitted): + fn = '%03d_%s.bin' % (i, r['name']) + open(os.path.join(d, fn), 'wb').write(r['data']) + assert r['offset'] % 4 == 0, r['name'] + rows.append(dict(name=r['name'], file=fn, offset=r['offset'], length=r['length'])) + with open(os.path.join(d, 'payload.lua'), 'w', encoding='utf-8', newline='\n') as f: + f.write('-- generated by tools/make_dd_payload.py (%s) -- do not hand-edit\n' % ROLE) + f.write('-- dd: -if / -of /dev/app bs=4 seek=offset/4 count=length/4\n') + f.write('return {\n') + for r in rows: + f.write(' { name = "%s", file = "%s", offset = %d, length = %d },\n' + % (r['name'], r['file'], r['offset'], r['length'])) + f.write('}\n') + return rows + + +lua_rows = write_pack(FPDIR) + +MAGIC_PAY = b'\x00S4P-PAYv1\x00\xa5\x5a\xc3\x3c' # 15 B,必须全局唯一 +MAGIC_FP = b'\x00S4P-FPv1\x00\x5a\xa5\x3c\xc3' +os.makedirs(BLOBDIR, exist_ok=True) +for f in os.listdir(BLOBDIR): + os.remove(os.path.join(BLOBDIR, f)) +pay_blob = MAGIC_PAY + b''.join(r['data'] for r in emitted) +fp_blob = MAGIC_FP + b''.join(r['fp'] for r in emitted) +open(os.path.join(BLOBDIR, 'payload.bin'), 'wb').write(pay_blob) +open(os.path.join(BLOBDIR, 'fp.bin'), 'wb').write(fp_blob) +rel, off, fpoff = [], 0, 0 +for r in emitted: + rel.append(dict(name=r['name'], rel=off, fprel=fpoff, target=r['offset'], + length=r['length'], fplen=len(r['fp']))) + off += r['length'] + fpoff += len(r['fp']) +assert fpoff == len(fp_blob) - len(MAGIC_FP), (fpoff, len(fp_blob)) +json.dump(dict(role=ROLE, magic_len=len(MAGIC_PAY), magic_fp_len=len(MAGIC_FP), fp_len=FP_LEN, + records=rel, pay_bytes=len(pay_blob), fp_bytes=len(fp_blob)), + open(os.path.join(BLOBDIR, 'offsets.json'), 'w', encoding='utf-8'), indent=1) +print('blob_%s/ 单条目: payload.bin %d B + fp.bin %d B(%d 条记录)' + % (ROLE, len(pay_blob), len(fp_blob), len(rel))) + + +def script(fname, rows, header, base, verify, tail): + with open(os.path.join(OUT, fname), 'w', encoding='utf-8', newline='\n') as f: + f.write('#!/bin/sh\n') + for line in header: + f.write('# ' + line + '\n') + f.write('\nDEV=/dev/app\nBASE=' + base + '\n\n') + f.write('echo "target check"\nls -l $DEV\n\n') + for r in rows: + b = 4 if (r['offset'] % 4 == 0 and r['length'] % 4 == 0) else 1 + f.write('echo "== ' + r['name'] + ' ==\n') + f.write('dd if=$BASE/%s of=$DEV bs=%d seek=%d count=%d\n' + % (r['file'], b, r['offset'] // b, r['length'] // b)) + if verify: + f.write('dd if=$DEV of=/tmp/chk.bin bs=%d skip=%d count=%d\n' + % (b, r['offset'] // b, r['length'] // b)) + f.write('cmp /tmp/chk.bin $BASE/%s\n\n' % r['file']) + f.write(tail + '\n') + + +if ROLE == 'replace': + script('flash.sh', lua_rows, + ['S5 41mm 压力检测动画 -> S4 eSIM(只写负载)', + '把 flash.sh 与 payload/ 放到同一个目录,例如 /data/s4panim/', + '然后: sh /data/s4panim/flash.sh 最后: reboot', + '还原用 restore.sh 或安装还原盘 S4PAnimRestore.face'], + '/data/s4panim/payload', True, + 'rm /tmp/chk.bin\necho "all done - run reboot to apply"') +else: + script('restore.sh', lua_rows, + ['把 S4 eSIM 的压力检测动画写回原厂(还原盘)'], + '/data/s4panim/payload_restore', True, + 'rm /tmp/chk.bin\necho "restore done - run reboot to apply"') + +json.dump(dict(device='Xiaomi Watch S4 eSIM (o62lte)', target='/dev/app', + role=ROLE, kind='pressure-measure-anim', + dd='dd if=/ of=/dev/app bs=4 seek= count=', + block_size=4, records=lua_rows, + dropped_identical=[r['name'] for r in dropped]), + open(os.path.join(OUT, 'payload.json'), 'w', encoding='utf-8'), indent=1, ensure_ascii=False) +total = sum(r['length'] for r in lua_rows) +print('role=%s: emitted %d records, %.2f MB; dropped %d' % (ROLE, len(lua_rows), total / 1048576, len(dropped))) diff --git a/s4e-pressure-anim/tools/make_overlays.py b/s4e-pressure-anim/tools/make_overlays.py new file mode 100644 index 0000000..5c2adf2 --- /dev/null +++ b/s4e-pressure-anim/tools/make_overlays.py @@ -0,0 +1,75 @@ +"""为官方 LuaDevTemplate 生成可编译的 overlay。两份盘:S4PAnim(替换)/ S4PAnimRestore(还原)。""" +import os, sys, json, shutil +from PIL import Image, ImageFont +from paths import PROJ, WORK + +ROLE = (sys.argv[1] if len(sys.argv) > 1 else 'replace').lower() +NAME = 'S4PAnim' if ROLE == 'replace' else 'S4PAnimRestore' +OUT = os.path.join(WORK, 'overlay') +ASSETS = os.path.join(PROJ, 'assets') + +DEVICE_TYPE = "362" # 与官方模板一致(362 = S4 家族) +ENTRY_WIDGET = "app_lua%2Fmain.lua" +PREVIEW_SIZE = 326 # Compiler.exe 对 362 设备强制 326x326 +SCREEN_SIZE = 466 +WF_ID = "362150102" # 两份盘共用同一个 id:装一份就顶掉另一份 +FONTS = [r'C:\Windows\Fonts\msyhbd.ttc', r'C:\Windows\Fonts\msyh.ttc', + r'C:\Windows\Fonts\arialbd.ttf', r'C:\Windows\Fonts\arial.ttf'] + + +def font(size): + for p in FONTS: + try: + return ImageFont.truetype(p, size) + except OSError: + continue + return ImageFont.load_default() + + +def make_preview(path): + Image.open(os.path.join(ASSETS, 'cover.png')).convert('RGBA') \ + .resize((PREVIEW_SIZE, PREVIEW_SIZE), Image.LANCZOS).save(path) + + +def write_fprj(path, title): + xml = ('\r\n' + f'\r\n' + f' \r\n' + f' \r\n' + ' \r\n\r\n') + open(path, 'w', encoding='utf-16', newline='').write(xml) + + +def build(name, role): + base = os.path.join(OUT, name) + if os.path.exists(base): + shutil.rmtree(base) + fprj = os.path.join(base, 'watchface', 'fprj') + os.makedirs(os.path.join(fprj, 'images'), exist_ok=True) + os.makedirs(os.path.join(fprj, 'app', 'lua'), exist_ok=True) + + cfg = {"projectName": name, "watchfaceId": WF_ID, "power_consumption": "3", + "resourceBin": {"lvglVersion": 9, "colorFormat": "I8", "compress": "NONE", + "input": "watchface/fprj/images/preview.png", "name": "preview"}} + json.dump(cfg, open(os.path.join(base, 'watchface.config.json'), 'w', encoding='utf-8'), + indent=4, ensure_ascii=False) + write_fprj(os.path.join(fprj, f'{name}.fprj'), name) + make_preview(os.path.join(fprj, 'images', 'preview.png')) + shutil.copyfile(os.path.join(PROJ, 'main.lua'), + os.path.join(fprj, 'app', 'lua', 'main.lua')) + # 只放「合并后的单条目」(blob_/) —— .face 里每个文件 = 一个资源条目 + blob = os.path.join(PROJ, 'blob_' + role) + if not os.path.isdir(blob): + raise SystemExit('缺少 blob_%s/ —— 先跑 tools/make_dd_payload.py %s' % (role, role)) + for src, rel in ((blob, 'pay'), (os.path.join(PROJ, 'ui_images'), 'ui')): + dst = os.path.join(fprj, 'app', 'images', rel) + if os.path.exists(dst): + shutil.rmtree(dst) + shutil.copytree(src, dst) + return base + + +if __name__ == '__main__': + build(NAME, ROLE) + print('overlay written to', os.path.join(OUT, NAME)) diff --git a/s4e-pressure-anim/tools/make_ui_images.py b/s4e-pressure-anim/tools/make_ui_images.py new file mode 100644 index 0000000..bcdcb11 --- /dev/null +++ b/s4e-pressure-anim/tools/make_ui_images.py @@ -0,0 +1,58 @@ +"""把 assets/ 里的 UI 素材转成 LVGL v9 二进制图片(.bin),供表盘运行时加载。 + +格式(已在真机验证): + header 12B: magic=0x19 | cf | flags | w | h | stride | reserved + cf=0x10 ARGB8888 -> 数据为 BGRA,长度 = w*h*4 +编码器用 iconpack.encode(与图标包同一套,字节级验证过)。 +""" +import os, sys +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +import iconpack as ip +from PIL import Image, ImageDraw + +from paths import PROJ, DOCS +ASSETS = os.path.join(PROJ, 'assets') +OUT = os.path.join(PROJ, 'ui_images') + +# 显示尺寸取自 CSS:列表图标 30x30,返回箭头 17x28 +SPEC = [ + ('back', 'backarrow.png', 17, 28), + ('ic_help', 'MdiLightbulbOutline.png', 30, 30), + ('ic_replace', 'MdiBandage.png', 30, 30), + ('ic_about', 'MdiInformationOutline.png', 30, 30), + ('ic_check', 'MdiClipboardTextSearchOutline.png', 30, 30), + ('ic_rollback', 'MdiReplyAllOutline.png', 30, 30), + ('ic_qq', 'qq.png', 30, 30), +] + +os.makedirs(OUT, exist_ok=True) +for name, png, w, h in SPEC: + p = os.path.join(ASSETS, png) + if not os.path.exists(p): + print(f' 缺少 {png}'); continue + img = Image.open(p).convert('RGBA') + if img.size != (w, h): + img = img.resize((w, h), Image.LANCZOS) + blob = ip.encode(img, ip.CF_ARGB8888, w, h, 0) + dst = os.path.join(OUT, name + '.bin') + open(dst, 'wb').write(blob) + print(f' {name:<12} <- {png:<34} {w}x{h} {len(blob)} bytes') + +# 关于页的 logo:封面图缩到 78x78,并加圆形遮罩 +# (封面是圆形图案 + 黑色方角,不裁成圆的话会在墨绿卡片上露出黑方块) +logo_src = os.path.join(ASSETS, 'cover.png') +if os.path.exists(logo_src): + L = 78 + img = Image.open(logo_src).convert('RGBA').resize((L, L), Image.LANCZOS) + mask = Image.new('L', (L, L), 0) + ImageDraw.Draw(mask).ellipse([0, 0, L - 1, L - 1], fill=255) + img.putalpha(mask) + blob = ip.encode(img, ip.CF_ARGB8888, L, L, 0) + open(os.path.join(OUT, 'logo.bin'), 'wb').write(blob) + # 顺便存一份 PNG 给本机预览用(注意别放进打包目录,会一起被打进去) + prev = os.path.join(DOCS, 'logo_preview.png') + os.makedirs(os.path.dirname(prev), exist_ok=True) + img.save(prev) + print(f' {"logo":<12} <- cover.png(圆形裁切) {L}x{L} {len(blob)} bytes') + +print('written to', OUT) diff --git a/s4e-pressure-anim/tools/pages.py b/s4e-pressure-anim/tools/pages.py new file mode 100644 index 0000000..cb8f259 --- /dev/null +++ b/s4e-pressure-anim/tools/pages.py @@ -0,0 +1,180 @@ +"""页面规格 + 本机渲染器:按 CSS 逐条落地,用真 MiSans 字体和真素材渲染预览。 + +CSS -> 实际能力 的映射(已在真机验证过): + left/top/width/height -> x / y / w / h(466 画布绝对坐标) + background -> bg_color + border-radius -> radius(50x50 圆的 radius=25) + color (alpha 1) -> text_color '#rrggbb' + color (alpha 0.6) -> 白 60% 叠在纯黑底上 = #999999(页面底色是黑,见下) + font-weight 600 -> MiSans-Semibold + font-weight 500 -> MiSans-Medium + vertical-align middle -> vcenter=True + 多色文本 -> LVGL recolor: "灰#ffffff 白#灰" +""" +import os + +from paths import PROJ, DOCS +ROOT = PROJ +ASSETS = os.path.join(ROOT, 'assets') +W = H = 466 +BG = (0, 0, 0) # 封面四角纯黑 -> 页面底色 +GRAY = '999999' # rgba(255,255,255,0.6) 叠黑 +WHITE = 'ffffff' +PLATE = '002927' # rgba(0,41,39,1) +CIRCLE = '4f4f4f' # rgba(79,79,79,1) +OK = '43cf7c' # rgba(67,207,124,1) +ERR = 'd43030' # rgba(212,48,48,1) + +S600, S500 = 'semibold', 'medium' + +# 列表项几何(来自第一个表项的 CSS,其余按同一相对关系排布) +ITEM_X, ITEM_W, ITEM_H, ITEM_R = 49, 368, 88, 34 +PITCH = 96 # 88 + 8px 间距 +ICONC_DX, ICONC_DY, ICONC_SZ = 20, 19, 50 # 圆形遮罩 69-49 / 104-85 +ICON_DX, ICON_DY, ICON_SZ = 30, 29, 30 # 图标图片 79-49 / 114-85 +TITLE_DX, TITLE_DY, TITLE_W, TITLE_H = 80, 17, 208, 54 # 129-49 / 102-85 + + +def item(y, icon, title): + """一个列表项:底板 + 圆形遮罩 + 图标 + 标题""" + return [ + dict(kind='rect', x=ITEM_X, y=y, w=ITEM_W, h=ITEM_H, bg=PLATE, radius=ITEM_R), + dict(kind='circle', x=ITEM_X + ICONC_DX, y=y + ICONC_DY, + w=ICONC_SZ, h=ICONC_SZ, bg=CIRCLE), + dict(kind='image', x=ITEM_X + ICON_DX, y=y + ICON_DY, + w=ICON_SZ, h=ICON_SZ, src=os.path.join(ASSETS, icon)), + dict(kind='label', x=ITEM_X + TITLE_DX, y=y + TITLE_DY, + w=TITLE_W, h=TITLE_H, text=title, size=28, weight=S500, + color=WHITE, align='left', vcenter=True, lh=37.13), + ] + + +def clock(t='10:24'): + return dict(kind='label', x=168, y=0, w=130, h=45, text=t, size=28, + weight=S600, color=GRAY, align='center', vcenter=True, lh=33.6) + + +def title(text, x=106, w=255): + return dict(kind='label', x=x, y=34, w=w, h=45, text=text, size=30, + weight=S500, color=WHITE, align='center', vcenter=True, lh=44.8) + + +def back_arrow(): + return dict(kind='image', x=160, y=43, w=17, h=28, + src=os.path.join(ASSETS, 'backarrow.png')) + + +def big_button(y=365, text='确认替换'): + return [ + dict(kind='rect', x=108, y=y, w=250, h=80, bg=PLATE, radius=40), + dict(kind='label', x=173, y=y + 13, w=120, h=54, text=text, size=28, + weight=S500, color=WHITE, align='center', vcenter=True, lh=37.13), + ] + + +# ---------------------------------------------------------------- 1. 首页 +PAGE_HOME = [ + clock(), + title('OS4 Icons'), + *item(85, 'MdiLightbulbOutline.png', '快速帮助'), + *item(181, 'MdiBandage.png', '开始替换'), + *item(277, 'MdiInformationOutline.png', '关于'), +] + +# ---------------------------------------------------------------- 2. 快速帮助 +_HELP_HEAD = [clock(), back_arrow(), title('快速帮助', x=179, w=128)] +_HELP_I1 = item(85, 'MdiClipboardTextSearchOutline.png', '环境预检查') +_HELP_D1 = dict(kind='label', x=65, y=176, w=338, h=72, + text='检查原始资源状态、分区可写情况等。', size=24, weight=S500, + color=GRAY, align='left', vtop=True, lh=31.82) +_HELP_I2 = item(256, 'MdiReplyAllOutline.png', '回滚修改') +_HELP_D2 = dict(kind='label', x=65, y=344, w=334, h=72, + text='将图标恢复到系统原始样式。', size=24, weight=S500, + color=GRAY, align='left', vtop=True, lh=31.82) + +PAGE_HELP = _HELP_HEAD + _HELP_I1 + [_HELP_D1] + _HELP_I2 + [_HELP_D2] + +# 帮助页的两种结果状态(同一标签换色换字) +HELP_DESC_OK = dict(kind='label', x=65, y=176, w=338, h=72, + text='预检查通过 | 38 条 | 2.58MB\n您可以返回主页开始修改。', + size=24, weight=S500, color=OK, align='left', vtop=True, lh=31.82) +HELP_DESC_ERR = dict(kind='label', x=65, y=176, w=338, h=72, + text='预检查失败:分区头不是 ROMFS,位置 0x00。', + size=24, weight=S500, color=ERR, align='left', vtop=True, lh=31.82) + +PAGE_HELP_OK = _HELP_HEAD + _HELP_I1 + [HELP_DESC_OK] + _HELP_I2 + [_HELP_D2] +PAGE_HELP_ERR = _HELP_HEAD + _HELP_I1 + [HELP_DESC_ERR] + _HELP_I2 + [_HELP_D2] + +# ---------------------------------------------------------------- 3. 开始替换 +# 多色文本**不能**用 recolor(AP 里那几处是图片重着色,不是文本属性), +# 所以拆成多个标签 + 手工算 x。折行也写死,否则多色片段位置不确定。 +# 宽度用真 MiSans 24px 量得: +# "的系统版本号为 " = 177 "3.202.79" = 99 +# "Xiaomi Watch S4 eSIM(非" = 313 "15 周年纪念版)" = 178 +WARN_LABELS = [ + dict(kind='label', x=64, y=90, w=338, h=70, + text='替换图标包之前,请先确认自己\n的系统版本号为 ', + size=24, weight=S500, color=GRAY, align='left', vtop=True, lh=33), + dict(kind='label', x=241, y=123, w=104, h=33, text='3.202.79', + size=24, weight=S500, color=WHITE, align='left', vtop=True, lh=33), + dict(kind='label', x=340, y=123, w=30, h=33, text='!', + size=24, weight=S500, color=GRAY, align='left', vtop=True, lh=33), + dict(kind='label', x=64, y=189, w=338, h=33, text='请确认自己的手表型号为', + size=24, weight=S500, color=GRAY, align='left', vtop=True, lh=33), + dict(kind='label', x=64, y=222, w=338, h=70, + text='Xiaomi Watch S4 eSIM(非\n15 周年纪念版)', + size=24, weight=S500, color=WHITE, align='left', vtop=True, lh=33), + dict(kind='label', x=242, y=255, w=30, h=33, text='!', + size=24, weight=S500, color=GRAY, align='left', vtop=True, lh=33), + dict(kind='label', x=64, y=321, w=338, h=33, text='搞机有风险,变砖后果自负。', + size=24, weight=S500, color=GRAY, align='left', vtop=True, lh=33), +] + +PAGE_CONFIRM = [ + clock(), + back_arrow(), + title('开始替换', x=179, w=128), + *WARN_LABELS, + *big_button(365, '确认替换'), +] + +# ---------------------------------------------------------------- 4. 替换中 +LOG_MID = ('> 开始替换\n' + '> 读取 resource.bin\n' + '> 写入 12/38\n' + '> 写入 38/38 完成,0 失败\n' + '> 替换完毕,请重启手表。若发现问题,请回滚修改。') + +PAGE_RUNNING = [ + clock(), + title('正在替换', x=179, w=128), + dict(kind='label', x=64, y=90, w=338, h=269, text=LOG_MID, size=24, + weight=S500, color=WHITE, align='left', vtop=True, lh=31.82), + *big_button(365, '重启手表'), +] + +# ---------------------------------------------------------------- 5. 关于 +PAGE_ABOUT = [ + clock(), + dict(kind='image', x=191, y=43, w=17, h=28, + src=os.path.join(ASSETS, 'backarrow.png')), + title('关于', x=212, w=64), + dict(kind='rect', x=52, y=79, w=362, h=204, bg=PLATE, radius=30), + dict(kind='image', x=194, y=108, w=78, h=78, + src=os.path.join(DOCS, 'logo_preview.png')), + dict(kind='label', x=164, y=192, w=138, h=38, text='OS4 Icons', + size=28, weight=S500, color=WHITE, align='left', vtop=True, lh=38), + dict(kind='label', x=210, y=227, w=45, h=27, text='1.0.0', + size=20, weight=S500, color=GRAY, align='left', vtop=True, lh=27), + *item(291, 'qq.png', '732681995'), +] + +PAGES = { + 'home': PAGE_HOME, + 'help': PAGE_HELP, + 'help_ok': PAGE_HELP_OK, + 'help_err': PAGE_HELP_ERR, + 'confirm': PAGE_CONFIRM, + 'about': PAGE_ABOUT, + 'running': PAGE_RUNNING, +} diff --git a/s4e-pressure-anim/tools/paths.py b/s4e-pressure-anim/tools/paths.py new file mode 100644 index 0000000..a2da22a --- /dev/null +++ b/s4e-pressure-anim/tools/paths.py @@ -0,0 +1,35 @@ +"""项目里所有脚本的路径入口 —— 换机器 / 换目录只看这一个文件。 + +默认布局(都相对项目根): + inputs/target/ 目标固件的分区镜像(要自己放,见 README) + inputs/source/ 图标来源的分区镜像(要自己放) + inputs/fonts/ 可选:MiSans-Medium.ttf / MiSans-Semibold.ttf(渲染预览用) + vendor/ 第三方 LuaDevTemplate-main(里面有 Compiler.exe,要自己放) + .work/ 构建工作区(产物,可删) + dist/ 交付的 .face + docs/ 说明与预览图 + +环境变量可覆盖:ICON_EDIT_INPUTS / ICON_EDIT_VENDOR / ICON_EDIT_WORK / + ICON_EDIT_DIST / ICON_EDIT_DOCS / ICON_EDIT_FONTS +""" +import os + +PROJ = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + + +def _p(env, default): + return os.environ.get(env) or os.path.join(PROJ, default) + + +INPUTS = _p('ICON_EDIT_INPUTS', 'inputs') +VENDOR = _p('ICON_EDIT_VENDOR', 'vendor') +WORK = _p('ICON_EDIT_WORK', '.work') +DIST = _p('ICON_EDIT_DIST', 'dist') +DOCS = _p('ICON_EDIT_DOCS', 'docs') +FONTS = os.environ.get('ICON_EDIT_FONTS') or os.path.join(INPUTS, 'fonts') +TEMPLATE = os.path.join(VENDOR, 'LuaDevTemplate-main') +INPUT_TARGET = os.path.join(INPUTS, 'target') +INPUT_SOURCE = os.path.join(INPUTS, 'source') + +for _d in (WORK, DIST, DOCS, INPUT_TARGET, INPUT_SOURCE): + os.makedirs(_d, exist_ok=True) diff --git a/s4e-pressure-anim/tools/render_ui.py b/s4e-pressure-anim/tools/render_ui.py new file mode 100644 index 0000000..3c75dc5 --- /dev/null +++ b/s4e-pressure-anim/tools/render_ui.py @@ -0,0 +1,193 @@ +"""按页面规格渲染 466x466 预览图(真 MiSans 字体 + 真素材 + 圆形遮罩)。 + +支持 kind: rect / circle / image / label +label 支持 recolor 标记 "#RRGGBB 文字#"(LVGL 的写法),以及 align/vcenter/vtop/lh +""" +import os, re, sys +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from PIL import Image, ImageDraw, ImageFont + +from paths import PROJ, INPUTS, FONTS, WORK +ROOT = PROJ +FONT_FILES = {'semibold': 'MiSans-Semibold.ttf', 'medium': 'MiSans-Medium.ttf', + 'regular': 'MiSans-Regular.ttf'} +W = H = 466 +_fc = {} + + +def _font_from_firmware(name): + """从 inputs/target/vela_font.bin 里现取 MiSans(固件里就有这几个 ttf)。""" + p = os.path.join(INPUTS, 'target', 'vela_font.bin') + if not os.path.exists(p): + return None + try: + from romfs import Romfs + r = Romfs(p) + except Exception: + return None + for path, ino in r.entries(): + if path == name: + d = os.path.join(WORK, 'fonts') + os.makedirs(d, exist_ok=True) + out = os.path.join(d, name) + open(out, 'wb').write(r.read(ino)) + return out + return None + + +def font(weight, size): + k = (weight, size) + if k in _fc: + return _fc[k] + name = FONT_FILES.get(weight, FONT_FILES['medium']) + p = os.path.join(FONTS, name) + if not os.path.exists(p): + p = _font_from_firmware(name) or p + if not os.path.exists(p): + for alt in (r'C:\Windows\Fonts\msyh.ttc', r'C:\Windows\Fonts\msyhbd.ttc', + r'C:\Windows\Fonts\arial.ttf'): + if os.path.exists(alt): + p = alt + print(' [warn] %s 缺失,预览改用 %s(只影响预览缩略图)' % (name, os.path.basename(alt))) + break + _fc[k] = ImageFont.truetype(p, size) if os.path.exists(p) else ImageFont.load_default() + return _fc[k] + + +def _rgb(c): + return tuple(int(c[i:i + 2], 16) for i in (0, 2, 4)) + + +def split_recolor(text, base): + """LVGL 写法: '灰#ffffff 白#灰' + - # + 6 位十六进制 -> 切到该颜色(该字符不显示) + - 再次遇到单独的 # -> 结束着色,回到基础色(该字符不显示) + """ + runs, buf, cur, incol = [], '', base, False + i = 0 + while i < len(text): + if text[i] == '#': + if re.match(r'[0-9a-fA-F]{6}', text[i + 1:i + 7]): + runs.append((cur, buf)); buf = '' + cur = text[i + 1:i + 7].lower(); incol = True + i += 7 + continue + if incol: + runs.append((cur, buf)); buf = '' + cur = base; incol = False + i += 1 + continue + buf += text[i]; i += 1 + runs.append((cur, buf)) + return runs + + +def wrap_runs(d, runs, f, maxw): + """按像素宽度折行,返回 [[(color, text), ...], ...](尽量保持原换行)""" + lines = [[]] + for color, chunk in runs: + for k, seg in enumerate(chunk.split('\n')): + if k: + lines.append([]) + if not seg: + continue + cur = '' + for ch in seg: + if d.textlength(cur + ch, font=f) > maxw and cur: + lines[-1].append((color, cur)) + lines.append([]) + cur = ch + else: + cur += ch + if cur: + lines[-1].append((color, cur)) + return lines + + +OVERFLOW = [] # (标签文本前 12 字, css_lh, 行数, 块高, 框高) + + +def draw_label(img, d, it): + f = font(it.get('weight', 'medium'), it['size']) + runs = split_recolor(it['text'], it.get('color', 'ffffff')) \ + if it.get('recolor') else [(it.get('color', 'ffffff'), it['text'])] + asc, desc = f.getmetrics() + natural = asc + desc + lh = max(it.get('lh', 0), natural) # LVGL 用字体自身行高,取较大者预测 + lines = wrap_runs(d, runs, f, it['w']) + total = lh * len(lines) + if total > it['h'] + 0.5: + OVERFLOW.append((it['text'][:14].replace('\n', '/'), + round(it.get('lh', 0), 2), len(lines), + round(total, 1), it['h'], round(natural, 2))) + if it.get('vcenter'): + y = it['y'] + (it['h'] - total) / 2 + elif it.get('vtop'): + y = it['y'] + else: + y = it['y'] + (it['h'] - total) / 2 + for ln in lines: + wsum = sum(d.textlength(t, font=f) for _, t in ln) + if it.get('align') == 'center': + x = it['x'] + (it['w'] - wsum) / 2 + elif it.get('align') == 'right': + x = it['x'] + it['w'] - wsum + else: + x = it['x'] + for color, t in ln: + d.text((x, y), t, font=f, fill=_rgb(color) + (255,)) + x += d.textlength(t, font=f) + y += lh + + +def render(items, out, mask=True, ring=False): + img = Image.new('RGBA', (W, H), (0, 0, 0, 0)) + d = ImageDraw.Draw(img) + for it in items: + k = it.get('kind') + if k == 'rect': + box = [it['x'], it['y'], it['x'] + it['w'] - 1, it['y'] + it['h'] - 1] + r = it.get('radius', 0) + (d.rounded_rectangle if r else d.rectangle)( + box, radius=r, fill=_rgb(it['bg']) + (255,)) if r else d.rectangle( + box, fill=_rgb(it['bg']) + (255,)) + elif k == 'circle': + box = [it['x'], it['y'], it['x'] + it['w'] - 1, it['y'] + it['h'] - 1] + d.ellipse(box, fill=_rgb(it['bg']) + (255,)) + elif k == 'image': + p = it['src'] + if os.path.exists(p): + im = Image.open(p).convert('RGBA').resize((it['w'], it['h']), Image.LANCZOS) + img.alpha_composite(im, (it['x'], it['y'])) + elif k == 'label': + draw_label(img, d, it) + if mask: + m = Image.new('L', (W, H), 0) + ImageDraw.Draw(m).ellipse([0, 0, W - 1, H - 1], fill=255) + img.putalpha(m) + base = Image.new('RGB', (W, H), (0, 0, 0)) + base.paste(img, (0, 0), img) + if ring: + ImageDraw.Draw(base).ellipse([0, 0, W - 1, H - 1], outline=(80, 80, 90)) + base.save(out) + return out + + +if __name__ == '__main__': + from pages import PAGES + outs = [] + for name, items in PAGES.items(): + p = os.path.join(ROOT, '_out', f'ui_{name}.png') + render(items, p) + outs.append(p) + print('wrote', p) + # 拼一张总览图 + cols = 3 + rows = (len(outs) + cols - 1) // cols + sheet = Image.new('RGB', (cols * (W + 12) + 12, rows * (H + 12) + 12), (28, 28, 32)) + for i, p in enumerate(outs): + im = Image.open(p) + sheet.paste(im, (12 + (i % cols) * (W + 12), 12 + (i // cols) * (H + 12))) + sp = os.path.join(ROOT, '_out', 'ui_all.png') + sheet.save(sp) + print('wrote', sp, sheet.size) diff --git a/s4e-pressure-anim/tools/romfs.py b/s4e-pressure-anim/tools/romfs.py new file mode 100644 index 0000000..753dda2 --- /dev/null +++ b/s4e-pressure-anim/tools/romfs.py @@ -0,0 +1,112 @@ +"""Minimal reader for Xiaomi Vela ROMFS partition images (Linux 'rom1fs' variant). + +Layout (big-endian): + super : magic "-rom1fs-", u32 size, u32 checksum, name (NUL-term, padded to 16) + inode : u32 next (low 4 bits = ROMFH_ type flags), u32 spec, u32 size, u32 checksum, + name (NUL-term, padded to 16), then file data immediately after. + type : 0=hardlink (spec = target inode offset), 1=dir (spec = offset of first child), + 2=regular file, 8=exec bit OR-ed in. + For a directory, its children are the chain starting at `spec`, linked by + `next & ~0xF`, and they stay inside the directory's own block. +""" +import struct, os + +ROUND16 = lambda n: (n + 15) & ~15 + +class Inode: + __slots__ = ('off', 'next', 'spec', 'size', 'cksum', 'name', 'namelen', 'data_off', 'kind') + + @property + def type_flags(self): + return self.next & 0xF + + @property + def type(self): + return self.next & 0x7 + + @property + def exec_bit(self): + return bool(self.next & 0x8) + + @property + def next_off(self): + return self.next & ~0xF + + KIND = {0: 'HRD', 1: 'DIR', 2: 'REG', 3: 'SYM', 4: 'BLK', 5: 'CHR', 6: 'SCK', 7: 'FIF'} + + def __repr__(self): + return (f'') + + +class Romfs: + def __init__(self, path): + self.path = path + self.data = open(path, 'rb').read() + d = self.data + if d[0:8] != b'-rom1fs-': + raise ValueError('not a romfs image') + self.sb_size, self.sb_cksum = struct.unpack('>II', d[8:16]) + end = d.index(b'\x00', 16) + self.volume = d[16:end].decode('latin1') + self.root_off = 16 + ROUND16(end - 16 + 1) + + def inode(self, off): + i = Inode() + i.off = off + i.next, i.spec, i.size, i.cksum = struct.unpack('>IIII', self.data[off:off+16]) + p = off + 16 + end = self.data.find(b'\x00', p, p + 256) + if end < 0: + raise ValueError(f'bad name at 0x{off:x}') + i.name = self.data[p:end].decode('latin1') + i.namelen = end - p + i.data_off = off + 16 + ROUND16(i.namelen + 1) + return i + + def walk(self, first, limit, prefix=''): + """Yield (path, Inode) for every entry reachable from `first` below `limit`. + + `limit` bounds the sibling chain. The LAST entry of a chain has next==0, + but its own children still live after it: they inherit the parent's limit + instead of being bounded by that zero. + """ + off = first + seen = set() + while off and (limit == 0 or off < limit) and off not in seen: + seen.add(off) + i = self.inode(off) + path = prefix + i.name + yield path, i + if i.type == 1 and i.spec: # directory -> first child at spec + yield from self.walk(i.spec, i.next_off or limit, path + '/') + off = i.next_off + + def entries(self): + """All entries of the image, as (path, Inode).""" + return self.walk(self.root_off + 0x20, len(self.data)) + + def read(self, ino): + return self.data[ino.data_off:ino.data_off + ino.size] + + +def img_header(blob): + if len(blob) < 12 or blob[0] != 0x19: + return None + magic, cf, flags, w, h, stride, res = struct.unpack(' 2 and sys.argv[2] not in path: + continue + kind = ('DIR ' if i.type == 1 else 'LNK ' if i.type == 0 else 'FILE') + print(f'{kind} {i.size:>9} {path}') + print(f'-- total entries: {n}') diff --git a/s4e-pressure-anim/tools/verify_anim.py b/s4e-pressure-anim/tools/verify_anim.py new file mode 100644 index 0000000..0fbc485 --- /dev/null +++ b/s4e-pressure-anim/tools/verify_anim.py @@ -0,0 +1,106 @@ +# -*- coding: utf-8 -*- +"""独立校验交付的 .face(只看交付物 + 固件镜像,不看中间产物)。 + + rome: replace -> dist/S4PAnim.face pay 应等于**改后镜像**的字节,fp 应等于**原厂**前 64 B + restore -> dist/S4PAnimRestore.face pay 应等于**原厂**字节, fp 应等于**改后镜像**前 64 B + ① 从 .face 里抠出 Lua 表(pay / rb),按表里的偏移取字节 + ② 每条 pay 长度必须 == 原厂槽位长度(等长覆盖的底线),且能按 LVGL RLE 解出 464x464 + ③ rb 每条必须是 64 B 指纹,且等于另一侧字节的前 64 B(版本校验数据正确) + ④ 目标镜像的 inode 表 / superblock / 分区长度与原厂逐位一致 +""" +import os, re, sys +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from paths import PROJ, INPUTS, WORK +from romfs import Romfs +import lvgl_bin as L + +ROLE = (sys.argv[1] if len(sys.argv) > 1 else 'replace').lower() +NAME = 'S4PAnim' if ROLE == 'replace' else 'S4PAnimRestore' +FACE = os.path.join(PROJ, 'dist', NAME + '.face') +STOCK = os.path.join(INPUTS, 'target', 'vela_app.bin') +PATCHED = os.path.join(WORK, 'anim', 'vela_app_anim.bin') +DIRP = 'pressure/measure/' +FP_LEN = 64 +ok = fail = 0 + + +def ck(cond, msg): + global ok, fail + if cond: + ok += 1 + else: + fail += 1 + print(' !! ' + msg) + + +face = open(FACE, 'rb').read() +src = face.decode('latin1') +m = re.search(r'pay = \{(.*?)\n \},', src, re.S) +m2 = re.search(r'rb = \{(.*?)\n \},', src, re.S) +ck(m is not None and m2 is not None, '在 .face 里找不到 pay / rb 表') +pay = [tuple(int(x) for x in r) for r in re.findall(r'\{(\d+),(\d+),(\d+)\}', m.group(1))] +rb = [tuple(int(x) for x in r) for r in re.findall(r'\{(\d+),(\d+),(\d+)\}', m2.group(1))] +print('%s:pay %d 条,rb %d 条' % (NAME, len(pay), len(rb))) + +stock_b = open(STOCK, 'rb').read() +patch_b = open(PATCHED, 'rb').read() +c = re.search(r"local ROLE = '(\w+)'", src) +ck(c is not None and c.group(1) == ROLE, 'Lua 里的 ROLE 不是 %s' % ROLE) +EXPECT_PAY = patch_b if ROLE == 'replace' else stock_b +EXPECT_FP = stock_b if ROLE == 'replace' else patch_b +stock = Romfs(STOCK) +slots = {} +for p, i in stock.entries(): + if i.type == 1 or not p.startswith(DIRP) or not p.endswith('.bin') or i.size == 0: + continue + slots[i.data_off] = (p, i.size) + +canary = [r for r in pay if r[1] == 0 and r[2] == 32] +ck(len(canary) <= 1, 'canary 记录不止一条') +if canary: + ck(face[canary[0][0]:canary[0][0] + 32] == stock_b[:32], 'canary 不对') +data = [r for r in pay if not (r[1] == 0 and r[2] == 32)] +bad_len = bad_dec = bad_bytes = 0 +for o, t, ln in data: + blob = face[o:o + ln] + if t not in slots or slots[t][1] != ln: + bad_len += 1; continue + if EXPECT_PAY[t:t + ln] != blob: + bad_bytes += 1 + h = L.parse_header(blob) + if h is None or not (h['flags'] & L.FLAG_COMPRESSED) or h['w'] != 464 or h['h'] != 464: + bad_dec += 1; continue + # ★ 设备端解码器要求流解出**正好 usize 字节**(1024 + w*h + 1),少一字节就整帧不画 + import struct as _s + _m, _cs, _us = _s.unpack('0.5% 就跳过 —— 拉伸会把方块变长方形、裁切会切掉按钮两端。所以 `walkie_talkie/image/stop.bin`(176×96 vs 144×96)与 `walkie_talkie/guidance/finish_guidance.bin`(176×96 vs 160×96)**保持原厂**(本机逐字节确认) +- **对讲机那一组是 I8+RLE 压缩槽位**(固件里 `flags=0x08`,长度不是 4 的倍数)→ `iconpack.encode_into_slot()`:量化(颜色数从 256 逐级往下试)→ RLE → 用 `ctrl=0` 的 2 字节空指令补齐到原 `csize`,做到逐字节等长;设备端写入这类记录自动退化成 `dd bs=1` +- 界面按 **480×480** 重做(设计稿在 `tools/s5e_pages.py`,本机用真 MiSans 渲染 + 真实像素判圆屏切边,7 页 0 越界) +- **压力 App 小表情也搬了 14 张**(`pressure/icon/`):4 档情绪 48px(relax/mild/mid/severe)+ 32px 三张 + `press_grade1..4` 等级徽标 + `presure_icon` + 两张表情底版(`pressure_eomji_bg` / `_widbg`);96px 那四张两边本来就一样(自动跳过)。没搬的:整屏背景(`pressure_bg`/`success_bg`/`success_arc`/`press_widget_bg*`/`press_chart_bg`)、`reminder.bin`、`pressure/measure/*` 动画帧(体积大) +- **系统 UI 素材整套一起搬**:8 个锁屏/通知/免打扰小图标 + 22 张控制中心磁贴(q63 磁贴是 116×116 浅色主题,缩放到 S5 eSIM 的 128×128)。白盘放在深色控制中心上的观感已在 S4 eSIM 上实测,效果可以 +- 只读预检查 + 每条写入前后对版/回读校验 + 一键回滚 + +--- + +## 1. 交付物与安装 + +| 文件 | 说明 | +|---|---| +| `dist/S5eIcons.face` | **主交付物** 10,852,268 B(sha256 50da912734a5db48…),表盘 id `462150101`,b5=0(与固件自带表盘一致) | +| `dist/S5eIcons_toolchain.face` | 官方工具链原样产物(b5=10);上面那个装不上时换这个 | +| `.work/s5e_from_os4/vela_app_os4.bin` | 重建后得到的整分区镜像(可直接刷机,绕开设备侧脚本) | +| `.work/s5e_from_os4/vela_health_os4.bin` | 同上(health 分区) | +| `docs/说明.md` | 完整说明:固件事实、47 条偏移表、验证结果、待实测项 | +| `docs/s5e_before_after.png` | 47 个 launcher 图标:原厂 → 替换后 对照图 | +| `docs/s5e_sysicons.png` | 44 个系统 UI 素材(22 控制中心 + 14 压力表情 + 8 其他):原厂 → 从 S5 41mm 搬过来的 对照图 | +| `docs/s5e_pressure.png` | 14 张压力 App 小表情:原厂 → q63 对照图 | +| `docs/s5e_ui_all.png` | 480×480 七个页面预览 | + +装法与普通 Lua 表盘相同(`resource.bin` 推到 `/data/app/watchface/market//`)。装好后在表盘上操作: + +``` +首页 →「快速帮助」→「环境预检查」 只读,一个字节都不写 +首页 →「开始替换」→ 确认 → 自动写入 153 条 →「重启手表」 + 快速帮助 →「回滚修改」→ 确认 → 写回原厂图标 +``` + +输出怎么读(「正在替换」页):每 10 条一行 `> n/153 成 x 失 y`;结束时 `> 成功 x / 失败 y(共 153)`、必要时 `> 版本不符跳过 n 条`,以及 `> app/health 分区分条统计`。 + +**判断成功的唯一标准是 `失败 0`。**不是 0 就先别重启,点「回滚修改」。 + +--- + +## 2. 目录 + +``` +main.lua 表盘源码(含 __UIIMG__ / __PAY__ / __RB__ 占位符,构建时回填) +payload/ payload.bin = MAGIC + 91 条顺序拼接;payload.lua = 逐条偏移表 +payload_rollback/ 原厂字节(回滚用) +ui_images/ 界面素材(LVGL v9 .bin,8 张) +assets/ 设计素材 PNG(Mdi 图标、返回箭头、QQ、封面) +assets/override/ 自制图覆盖(activities.png = 跑道图标,优先级高于固件源) +tools/ 全部脚本(见下表) +dist/ 交付的 .face +docs/ 说明与预览图 +inputs/ 要你自己放固件分区镜像(见第 4 节,不进仓库) +vendor/ 放第三方 LuaDevTemplate-main(不进仓库) +.work/ 构建工作区(产物,可删) +``` + +| 脚本 | 作用 | +|---|---| +| `tools/paths.py` | 所有路径的唯一入口(inputs / vendor / .work / dist,支持环境变量覆盖) | +| `tools/s5e_payload.py` | 从两个分区镜像生成 91 条负载 + 回滚 + 整分区镜像(自带正反两个证明) | +| `tools/s5e_ui_images.py` | 设计素材 PNG → LVGL .bin + 圆形 logo | +| `tools/s5e_pages.py` | 480×480 页面规格(唯一的设计稿来源) | +| `tools/s5e_render_ui.py` | 用真 MiSans 渲染 7 页预览 | +| `tools/s5e_check_clip.py` | 用真实渲染像素判圆屏是否切边 | +| `tools/s5e_build.py` | 两遍构建 → `dist/S5eIcons.face`(自动回填偏移 + 0 漂移校验) | +| `tools/s5e_verify.py` | **独立校验**:从 .face 里反解 Lua 表与偏移,再与固件镜像逐字节比对 | +| `tools/romfs.py` `tools/lvgl_bin.py` `tools/iconpack.py` `tools/render_ui.py` `tools/lua_balance.py` | 共用底层(ROMFS 解析 / LVGL 图片编解码 / UI 渲染 / Lua 块平衡) | + +--- + +## 3. 目标固件的事实(从固件包里查出来的) + +| 项 | 结论 | 证据 | +|---|---|---| +| 机型 | Xiaomi Watch S5 eSIM 46mm | `vela_ap.bin` / `vela_cp_image.bin` 里的机型字符串 | +| 屏幕 | 480×480 | 自带表盘素材 `bigsun.bin`、`citymage1.bin` 都是 I8 480×480(231,436 B);`complexzone.lua` 建 480×480 根对象 | +| Lua 宿主 | 有,Lua 5.4 | `vela_ap.bin` 里 `Lua 5.4`、`SCRIPT_PATH`、`watchface.db`、`luavgl`,以及同样的 7 个 C 模块 `luaopen_activity/dataman/miwear/navigator/screen/topic/vibrator` | +| 自带 Lua 表盘 | 7 个 | outdooradv / daynight / corona / complexzone / clearheart / circle / asymmetryzone | +| 图标槽位 | `/dev/app` 34 个 + `/dev/health` 15 个 | ROMFS `app` / `health` 卷里的 `*/launcher.bin` | +| 图标格式 | 全部 144×144 ARGB8888 = 82,956 B | 逐个解码确认;这台**没有 I8 槽位**(S4 eSIM 有 6 个),不存在量化损失 | +| 原厂画法 | 内容 136×136 居中(4px 透明边);`launcher_*` 138@(3,3);`debug` / `lua` / `voice_alexa` / `sports_game` 满幅 144 | 逐像素量 alpha 边界 | + +## 4. 替换内容 + +- 能对上 **47** 个槽位(app 33 + health 14);OS4 里没有对应的 2 个槽位**保持原厂**:`walkie_talkie`、`sports_game` +- 其中 **8 个**槽位的 OS4 图标与原厂逐字节相同(debug / esimsms / interconnect / lua / sports / sports_course / sports_record / sports_training),照样写入,保证整套一致 +- **`activities` 用自制图覆盖**:`assets/override/activities.png`(112×112 红/绿/青同心环,即 S4 项目里那张跑道图标)优先于 q63 固件源。裁剪 = 取实心 bbox(100×100) → 放大到目标槽位画法(136×136,LANCZOS + 锐化) → 居中粘回 (4,4);实测成品外径与原厂图标**完全对齐**(都是 (4,4)-(139,139)) +- **`voice_aivs`(小爱同学)同样用自制图覆盖**:`assets/override/voice_aivs.png`(用户给的 144×144 PNG,圆形白盘直径 140)→ 裁到实心 → 缩放到槽位画法 136×136 → 居中;编码后 82,956 B 与原槽位等长 +- **尺寸归一化**:把 OS4 的满幅 144 图按目标槽位的画法缩放(多数 136、`launcher_*` 138)并居中到同一原点 —— 这样每条负载长度与原槽位完全相等 +- 负载 4.41 MB(替换 + 回滚各一份),.face 共 8.98 MB + +--- + +## 5. 重建 + +### 5.1 需要自己准备的输入(放进 `inputs/`,不进仓库) + +| 路径 | 是什么 | 大小 / sha256 前 16 位 | +|---|---|---| +| `inputs/target/vela_app.bin` | 目标固件(p62lte)app 分区 | 103,043,072 / 9b41c5f5eb17c112 | +| `inputs/target/vela_health.bin` | 目标固件 health 分区 | 206,924,800 / 489752c8a3fb24ff | +| `inputs/target/vela_font.bin` | 目标固件 font 分区(可选:渲染预览时从里面取 MiSans) | 43,379,712 / 16cb803ac1bd74c5 | +| `inputs/source/vela_app.bin` | 图标来源(S5 41mm q63)app 分区 | 206,000,128 / 161a8f7a5f0bd8af | +| `inputs/source/vela_health.bin` | 图标来源 health 分区 | 233,939,968 / 12a3bd2f8b62e969 | +| `inputs/fonts/MiSans-Medium.ttf`、`MiSans-Semibold.ttf` | 可选;不提供就自动从 `inputs/target/vela_font.bin` 里取 | — | +| `vendor/LuaDevTemplate-main/` | 官方模板(里面有 `Compiler.exe`) | — | + +固件分区镜像就是官方 OTA 包(ZIP)里的 `vela_*.bin` 条目,解压出来即可。 + +### 5.2 步骤 + +```powershell +# 需要 Python 3 + Pillow +python tools/s5e_payload.py # 负载 + 整分区镜像(自己证明 正放=改后镜像 / 反放=原厂镜像) +python tools/s5e_ui_images.py # 界面素材 +python tools/s5e_render_ui.py # 480 预览(可选) +python tools/s5e_check_clip.py # 圆屏切边检查(可选) +python tools/s5e_build.py # 两遍构建 -> dist/S5eIcons.face +python tools/s5e_verify.py # 独立校验 +``` + +只想改界面/坐标:改 `main.lua` 与 `tools/s5e_pages.py`,然后跑 `s5e_render_ui.py` + `s5e_check_clip.py` + `s5e_build.py` + `s5e_verify.py`。 + +--- + +## 6. 已知边界(踩过的坑) + +0. **对讲机素材默认不打包**(ICON_WALKIE=1 可打开);160 张 PNG 仍在 ssets/walkie_talkie/。 + +1. **偏移锁固件版本**:换固件要重跑 `s5e_payload.py` + 构建。用错版本时表盘会判「版本不符」整分区跳过,**不会把字节写进别人的文件里**。 +2. **Compiler.exe 的设备表里没有 480 档**(枚举 DeviceType 1..1500 只有 17 个已知值,圆表且预览 326×326 的只有 362 / 462)→ 这里用 462 + 326×326 预览。预览只是表盘列表里的缩略图。 +3. **表盘宿主能不能写 `/dev/app`、`/dev/health` 未在真机验证**(S4 eSIM 上写 `/dev/app` 是实测通过的,同源平台)。所以界面里第一步是只读的「环境预检查」。 +4. **负载文件必须原样连续放在 .face 里**,偏移才等于 `文件基址 + 魔数长度 + 相对偏移`;`s5e_build.py` 会断言这一点。魔数长度**不要手写常量**(曾经把 15 写成 16,全部偏移差 1 字节)。 +5. **独立校验不是摆设**:`tools/s5e_verify.py` 从 .face 里的 Lua 表反解偏移再与固件镜像比对,本项目开发时它抓出两个真 bug —— 魔数长度差 1;Lua 表里存的是 `/dev/app` 而不是运行时期望的 `app` 键(会让 dd 拼成 `if=nil`)。 +6. 负载写入走 `/tmp` 中转(resource.bin 内的偏移不保证 4 字节对齐,不能直接 `dd skip`)。 + +## 7. 第三方与风险 + +- 本仓库不含固件、OTA 包、字体;`.face` 由第三方 `Compiler.exe` 编译(见 [../README.md](../README.md)) +- 往分区节点写数据是危险操作:写错偏移可能损坏资源分区。表盘自带版本校验与回滚,但请自行确认设备所有权并承担后果 +- 官方 OTA 升级会整分区覆盖,图标会回到原厂(等于自动还原) diff --git a/s5e-icons/assets/MdiBandage.png b/s5e-icons/assets/MdiBandage.png new file mode 100644 index 0000000..26abc5b Binary files /dev/null and b/s5e-icons/assets/MdiBandage.png differ diff --git a/s5e-icons/assets/MdiClipboardTextSearchOutline.png b/s5e-icons/assets/MdiClipboardTextSearchOutline.png new file mode 100644 index 0000000..85ea2da Binary files /dev/null and b/s5e-icons/assets/MdiClipboardTextSearchOutline.png differ diff --git a/s5e-icons/assets/MdiInformationOutline.png b/s5e-icons/assets/MdiInformationOutline.png new file mode 100644 index 0000000..3af2a29 Binary files /dev/null and b/s5e-icons/assets/MdiInformationOutline.png differ diff --git a/s5e-icons/assets/MdiLightbulbOutline.png b/s5e-icons/assets/MdiLightbulbOutline.png new file mode 100644 index 0000000..7d7819c Binary files /dev/null and b/s5e-icons/assets/MdiLightbulbOutline.png differ diff --git a/s5e-icons/assets/MdiReplyAllOutline.png b/s5e-icons/assets/MdiReplyAllOutline.png new file mode 100644 index 0000000..f419de0 Binary files /dev/null and b/s5e-icons/assets/MdiReplyAllOutline.png differ diff --git a/s5e-icons/assets/backarrow.png b/s5e-icons/assets/backarrow.png new file mode 100644 index 0000000..9e55859 Binary files /dev/null and b/s5e-icons/assets/backarrow.png differ diff --git a/s5e-icons/assets/cover.png b/s5e-icons/assets/cover.png new file mode 100644 index 0000000..23c1cf8 Binary files /dev/null and b/s5e-icons/assets/cover.png differ diff --git a/s5e-icons/assets/override/activities.png b/s5e-icons/assets/override/activities.png new file mode 100644 index 0000000..ae5c552 Binary files /dev/null and b/s5e-icons/assets/override/activities.png differ diff --git a/s5e-icons/assets/override/voice_aivs.png b/s5e-icons/assets/override/voice_aivs.png new file mode 100644 index 0000000..228914f Binary files /dev/null and b/s5e-icons/assets/override/voice_aivs.png differ diff --git a/s5e-icons/assets/qq.png b/s5e-icons/assets/qq.png new file mode 100644 index 0000000..3982762 Binary files /dev/null and b/s5e-icons/assets/qq.png differ diff --git a/s5e-icons/assets/walkie_talkie/background.png b/s5e-icons/assets/walkie_talkie/background.png new file mode 100644 index 0000000..879c98f Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/background.png differ diff --git a/s5e-icons/assets/walkie_talkie/cancel_btn.png b/s5e-icons/assets/walkie_talkie/cancel_btn.png new file mode 100644 index 0000000..cc56188 Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/cancel_btn.png differ diff --git a/s5e-icons/assets/walkie_talkie/check.png b/s5e-icons/assets/walkie_talkie/check.png new file mode 100644 index 0000000..bc89d28 Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/check.png differ diff --git a/s5e-icons/assets/walkie_talkie/checked.png b/s5e-icons/assets/walkie_talkie/checked.png new file mode 100644 index 0000000..64bf2b9 Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/checked.png differ diff --git a/s5e-icons/assets/walkie_talkie/close_btn.png b/s5e-icons/assets/walkie_talkie/close_btn.png new file mode 100644 index 0000000..a872247 Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/close_btn.png differ diff --git a/s5e-icons/assets/walkie_talkie/confirm_btn.png b/s5e-icons/assets/walkie_talkie/confirm_btn.png new file mode 100644 index 0000000..932d711 Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/confirm_btn.png differ diff --git a/s5e-icons/assets/walkie_talkie/delete_icon.png b/s5e-icons/assets/walkie_talkie/delete_icon.png new file mode 100644 index 0000000..b3d948d Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/delete_icon.png differ diff --git a/s5e-icons/assets/walkie_talkie/guidance/finish_guidance.png b/s5e-icons/assets/walkie_talkie/guidance/finish_guidance.png new file mode 100644 index 0000000..932d711 Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/guidance/finish_guidance.png differ diff --git a/s5e-icons/assets/walkie_talkie/history_anim/anim0.png b/s5e-icons/assets/walkie_talkie/history_anim/anim0.png new file mode 100644 index 0000000..cdbf172 Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/history_anim/anim0.png differ diff --git a/s5e-icons/assets/walkie_talkie/history_anim/anim1.png b/s5e-icons/assets/walkie_talkie/history_anim/anim1.png new file mode 100644 index 0000000..9eb6f8b Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/history_anim/anim1.png differ diff --git a/s5e-icons/assets/walkie_talkie/history_anim/anim10.png b/s5e-icons/assets/walkie_talkie/history_anim/anim10.png new file mode 100644 index 0000000..927d24a Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/history_anim/anim10.png differ diff --git a/s5e-icons/assets/walkie_talkie/history_anim/anim11.png b/s5e-icons/assets/walkie_talkie/history_anim/anim11.png new file mode 100644 index 0000000..38d3985 Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/history_anim/anim11.png differ diff --git a/s5e-icons/assets/walkie_talkie/history_anim/anim12.png b/s5e-icons/assets/walkie_talkie/history_anim/anim12.png new file mode 100644 index 0000000..32c452c Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/history_anim/anim12.png differ diff --git a/s5e-icons/assets/walkie_talkie/history_anim/anim13.png b/s5e-icons/assets/walkie_talkie/history_anim/anim13.png new file mode 100644 index 0000000..b4338f7 Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/history_anim/anim13.png differ diff --git a/s5e-icons/assets/walkie_talkie/history_anim/anim14.png b/s5e-icons/assets/walkie_talkie/history_anim/anim14.png new file mode 100644 index 0000000..6dd42f1 Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/history_anim/anim14.png differ diff --git a/s5e-icons/assets/walkie_talkie/history_anim/anim15.png b/s5e-icons/assets/walkie_talkie/history_anim/anim15.png new file mode 100644 index 0000000..e4795f0 Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/history_anim/anim15.png differ diff --git a/s5e-icons/assets/walkie_talkie/history_anim/anim16.png b/s5e-icons/assets/walkie_talkie/history_anim/anim16.png new file mode 100644 index 0000000..5cdb8ad Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/history_anim/anim16.png differ diff --git a/s5e-icons/assets/walkie_talkie/history_anim/anim17.png b/s5e-icons/assets/walkie_talkie/history_anim/anim17.png new file mode 100644 index 0000000..9eb6f8b Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/history_anim/anim17.png differ diff --git a/s5e-icons/assets/walkie_talkie/history_anim/anim18.png b/s5e-icons/assets/walkie_talkie/history_anim/anim18.png new file mode 100644 index 0000000..cdbf172 Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/history_anim/anim18.png differ diff --git a/s5e-icons/assets/walkie_talkie/history_anim/anim2.png b/s5e-icons/assets/walkie_talkie/history_anim/anim2.png new file mode 100644 index 0000000..5cdb8ad Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/history_anim/anim2.png differ diff --git a/s5e-icons/assets/walkie_talkie/history_anim/anim3.png b/s5e-icons/assets/walkie_talkie/history_anim/anim3.png new file mode 100644 index 0000000..e4795f0 Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/history_anim/anim3.png differ diff --git a/s5e-icons/assets/walkie_talkie/history_anim/anim4.png b/s5e-icons/assets/walkie_talkie/history_anim/anim4.png new file mode 100644 index 0000000..6dd42f1 Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/history_anim/anim4.png differ diff --git a/s5e-icons/assets/walkie_talkie/history_anim/anim5.png b/s5e-icons/assets/walkie_talkie/history_anim/anim5.png new file mode 100644 index 0000000..b4338f7 Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/history_anim/anim5.png differ diff --git a/s5e-icons/assets/walkie_talkie/history_anim/anim6.png b/s5e-icons/assets/walkie_talkie/history_anim/anim6.png new file mode 100644 index 0000000..32c452c Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/history_anim/anim6.png differ diff --git a/s5e-icons/assets/walkie_talkie/history_anim/anim7.png b/s5e-icons/assets/walkie_talkie/history_anim/anim7.png new file mode 100644 index 0000000..38d3985 Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/history_anim/anim7.png differ diff --git a/s5e-icons/assets/walkie_talkie/history_anim/anim8.png b/s5e-icons/assets/walkie_talkie/history_anim/anim8.png new file mode 100644 index 0000000..927d24a Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/history_anim/anim8.png differ diff --git a/s5e-icons/assets/walkie_talkie/history_anim/anim9.png b/s5e-icons/assets/walkie_talkie/history_anim/anim9.png new file mode 100644 index 0000000..6d5503a Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/history_anim/anim9.png differ diff --git a/s5e-icons/assets/walkie_talkie/history_anim_right/anim0.png b/s5e-icons/assets/walkie_talkie/history_anim_right/anim0.png new file mode 100644 index 0000000..cdbf172 Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/history_anim_right/anim0.png differ diff --git a/s5e-icons/assets/walkie_talkie/history_anim_right/anim1.png b/s5e-icons/assets/walkie_talkie/history_anim_right/anim1.png new file mode 100644 index 0000000..5d99f04 Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/history_anim_right/anim1.png differ diff --git a/s5e-icons/assets/walkie_talkie/history_anim_right/anim10.png b/s5e-icons/assets/walkie_talkie/history_anim_right/anim10.png new file mode 100644 index 0000000..493b920 Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/history_anim_right/anim10.png differ diff --git a/s5e-icons/assets/walkie_talkie/history_anim_right/anim11.png b/s5e-icons/assets/walkie_talkie/history_anim_right/anim11.png new file mode 100644 index 0000000..e596207 Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/history_anim_right/anim11.png differ diff --git a/s5e-icons/assets/walkie_talkie/history_anim_right/anim12.png b/s5e-icons/assets/walkie_talkie/history_anim_right/anim12.png new file mode 100644 index 0000000..44ccabf Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/history_anim_right/anim12.png differ diff --git a/s5e-icons/assets/walkie_talkie/history_anim_right/anim13.png b/s5e-icons/assets/walkie_talkie/history_anim_right/anim13.png new file mode 100644 index 0000000..cc6d96c Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/history_anim_right/anim13.png differ diff --git a/s5e-icons/assets/walkie_talkie/history_anim_right/anim14.png b/s5e-icons/assets/walkie_talkie/history_anim_right/anim14.png new file mode 100644 index 0000000..709fe42 Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/history_anim_right/anim14.png differ diff --git a/s5e-icons/assets/walkie_talkie/history_anim_right/anim15.png b/s5e-icons/assets/walkie_talkie/history_anim_right/anim15.png new file mode 100644 index 0000000..08c909e Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/history_anim_right/anim15.png differ diff --git a/s5e-icons/assets/walkie_talkie/history_anim_right/anim16.png b/s5e-icons/assets/walkie_talkie/history_anim_right/anim16.png new file mode 100644 index 0000000..5b5a34a Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/history_anim_right/anim16.png differ diff --git a/s5e-icons/assets/walkie_talkie/history_anim_right/anim17.png b/s5e-icons/assets/walkie_talkie/history_anim_right/anim17.png new file mode 100644 index 0000000..5d99f04 Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/history_anim_right/anim17.png differ diff --git a/s5e-icons/assets/walkie_talkie/history_anim_right/anim18.png b/s5e-icons/assets/walkie_talkie/history_anim_right/anim18.png new file mode 100644 index 0000000..cdbf172 Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/history_anim_right/anim18.png differ diff --git a/s5e-icons/assets/walkie_talkie/history_anim_right/anim2.png b/s5e-icons/assets/walkie_talkie/history_anim_right/anim2.png new file mode 100644 index 0000000..5b5a34a Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/history_anim_right/anim2.png differ diff --git a/s5e-icons/assets/walkie_talkie/history_anim_right/anim3.png b/s5e-icons/assets/walkie_talkie/history_anim_right/anim3.png new file mode 100644 index 0000000..08c909e Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/history_anim_right/anim3.png differ diff --git a/s5e-icons/assets/walkie_talkie/history_anim_right/anim4.png b/s5e-icons/assets/walkie_talkie/history_anim_right/anim4.png new file mode 100644 index 0000000..709fe42 Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/history_anim_right/anim4.png differ diff --git a/s5e-icons/assets/walkie_talkie/history_anim_right/anim5.png b/s5e-icons/assets/walkie_talkie/history_anim_right/anim5.png new file mode 100644 index 0000000..cc6d96c Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/history_anim_right/anim5.png differ diff --git a/s5e-icons/assets/walkie_talkie/history_anim_right/anim6.png b/s5e-icons/assets/walkie_talkie/history_anim_right/anim6.png new file mode 100644 index 0000000..44ccabf Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/history_anim_right/anim6.png differ diff --git a/s5e-icons/assets/walkie_talkie/history_anim_right/anim7.png b/s5e-icons/assets/walkie_talkie/history_anim_right/anim7.png new file mode 100644 index 0000000..e596207 Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/history_anim_right/anim7.png differ diff --git a/s5e-icons/assets/walkie_talkie/history_anim_right/anim8.png b/s5e-icons/assets/walkie_talkie/history_anim_right/anim8.png new file mode 100644 index 0000000..493b920 Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/history_anim_right/anim8.png differ diff --git a/s5e-icons/assets/walkie_talkie/history_anim_right/anim9.png b/s5e-icons/assets/walkie_talkie/history_anim_right/anim9.png new file mode 100644 index 0000000..2286ae4 Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/history_anim_right/anim9.png differ diff --git a/s5e-icons/assets/walkie_talkie/icon_plus.png b/s5e-icons/assets/walkie_talkie/icon_plus.png new file mode 100644 index 0000000..a1117ab Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/icon_plus.png differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker0.png b/s5e-icons/assets/walkie_talkie/image/Speaker0.png new file mode 100644 index 0000000..fc91da7 Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/image/Speaker0.png differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker1.png b/s5e-icons/assets/walkie_talkie/image/Speaker1.png new file mode 100644 index 0000000..66fc559 Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/image/Speaker1.png differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker10.png b/s5e-icons/assets/walkie_talkie/image/Speaker10.png new file mode 100644 index 0000000..2bb9b59 Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/image/Speaker10.png differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker100.png b/s5e-icons/assets/walkie_talkie/image/Speaker100.png new file mode 100644 index 0000000..521ed3e Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/image/Speaker100.png differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker101.png b/s5e-icons/assets/walkie_talkie/image/Speaker101.png new file mode 100644 index 0000000..9b955dd Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/image/Speaker101.png differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker102.png b/s5e-icons/assets/walkie_talkie/image/Speaker102.png new file mode 100644 index 0000000..7825c03 Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/image/Speaker102.png differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker11.png b/s5e-icons/assets/walkie_talkie/image/Speaker11.png new file mode 100644 index 0000000..c4415dd Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/image/Speaker11.png differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker12.png b/s5e-icons/assets/walkie_talkie/image/Speaker12.png new file mode 100644 index 0000000..844818b Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/image/Speaker12.png differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker13.png b/s5e-icons/assets/walkie_talkie/image/Speaker13.png new file mode 100644 index 0000000..89029fd Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/image/Speaker13.png differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker14.png b/s5e-icons/assets/walkie_talkie/image/Speaker14.png new file mode 100644 index 0000000..221f19a Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/image/Speaker14.png differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker15.png b/s5e-icons/assets/walkie_talkie/image/Speaker15.png new file mode 100644 index 0000000..4423df7 Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/image/Speaker15.png differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker16.png b/s5e-icons/assets/walkie_talkie/image/Speaker16.png new file mode 100644 index 0000000..bd8aa8a Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/image/Speaker16.png differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker17.png b/s5e-icons/assets/walkie_talkie/image/Speaker17.png new file mode 100644 index 0000000..9ea2e8b Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/image/Speaker17.png differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker18.png b/s5e-icons/assets/walkie_talkie/image/Speaker18.png new file mode 100644 index 0000000..fb5a10b Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/image/Speaker18.png differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker19.png b/s5e-icons/assets/walkie_talkie/image/Speaker19.png new file mode 100644 index 0000000..3ad3bf7 Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/image/Speaker19.png differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker2.png b/s5e-icons/assets/walkie_talkie/image/Speaker2.png new file mode 100644 index 0000000..8601636 Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/image/Speaker2.png differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker20.png b/s5e-icons/assets/walkie_talkie/image/Speaker20.png new file mode 100644 index 0000000..b6ea25b Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/image/Speaker20.png differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker21.png b/s5e-icons/assets/walkie_talkie/image/Speaker21.png new file mode 100644 index 0000000..14200fd Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/image/Speaker21.png differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker22.png b/s5e-icons/assets/walkie_talkie/image/Speaker22.png new file mode 100644 index 0000000..3d9c48f Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/image/Speaker22.png differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker23.png b/s5e-icons/assets/walkie_talkie/image/Speaker23.png new file mode 100644 index 0000000..e91fd3d Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/image/Speaker23.png differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker24.png b/s5e-icons/assets/walkie_talkie/image/Speaker24.png new file mode 100644 index 0000000..393fc07 Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/image/Speaker24.png differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker25.png b/s5e-icons/assets/walkie_talkie/image/Speaker25.png new file mode 100644 index 0000000..fa9e68e Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/image/Speaker25.png differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker26.png b/s5e-icons/assets/walkie_talkie/image/Speaker26.png new file mode 100644 index 0000000..6d43f7d Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/image/Speaker26.png differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker27.png b/s5e-icons/assets/walkie_talkie/image/Speaker27.png new file mode 100644 index 0000000..0a65418 Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/image/Speaker27.png differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker28.png b/s5e-icons/assets/walkie_talkie/image/Speaker28.png new file mode 100644 index 0000000..b0160e4 Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/image/Speaker28.png differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker29.png b/s5e-icons/assets/walkie_talkie/image/Speaker29.png new file mode 100644 index 0000000..1a8c275 Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/image/Speaker29.png differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker3.png b/s5e-icons/assets/walkie_talkie/image/Speaker3.png new file mode 100644 index 0000000..142da0d Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/image/Speaker3.png differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker30.png b/s5e-icons/assets/walkie_talkie/image/Speaker30.png new file mode 100644 index 0000000..c07d68b Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/image/Speaker30.png differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker31.png b/s5e-icons/assets/walkie_talkie/image/Speaker31.png new file mode 100644 index 0000000..b5bf877 Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/image/Speaker31.png differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker32.png b/s5e-icons/assets/walkie_talkie/image/Speaker32.png new file mode 100644 index 0000000..c7635aa Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/image/Speaker32.png differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker33.png b/s5e-icons/assets/walkie_talkie/image/Speaker33.png new file mode 100644 index 0000000..55c5c23 Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/image/Speaker33.png differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker34.png b/s5e-icons/assets/walkie_talkie/image/Speaker34.png new file mode 100644 index 0000000..8291262 Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/image/Speaker34.png differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker35.png b/s5e-icons/assets/walkie_talkie/image/Speaker35.png new file mode 100644 index 0000000..7299baa Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/image/Speaker35.png differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker36.png b/s5e-icons/assets/walkie_talkie/image/Speaker36.png new file mode 100644 index 0000000..ab8b08e Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/image/Speaker36.png differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker37.png b/s5e-icons/assets/walkie_talkie/image/Speaker37.png new file mode 100644 index 0000000..5f76111 Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/image/Speaker37.png differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker38.png b/s5e-icons/assets/walkie_talkie/image/Speaker38.png new file mode 100644 index 0000000..41cb88e Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/image/Speaker38.png differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker39.png b/s5e-icons/assets/walkie_talkie/image/Speaker39.png new file mode 100644 index 0000000..72a5863 Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/image/Speaker39.png differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker4.png b/s5e-icons/assets/walkie_talkie/image/Speaker4.png new file mode 100644 index 0000000..c7ad479 Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/image/Speaker4.png differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker40.png b/s5e-icons/assets/walkie_talkie/image/Speaker40.png new file mode 100644 index 0000000..81c7391 Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/image/Speaker40.png differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker41.png b/s5e-icons/assets/walkie_talkie/image/Speaker41.png new file mode 100644 index 0000000..9881ad6 Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/image/Speaker41.png differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker42.png b/s5e-icons/assets/walkie_talkie/image/Speaker42.png new file mode 100644 index 0000000..7e9ec32 Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/image/Speaker42.png differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker43.png b/s5e-icons/assets/walkie_talkie/image/Speaker43.png new file mode 100644 index 0000000..216afb2 Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/image/Speaker43.png differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker44.png b/s5e-icons/assets/walkie_talkie/image/Speaker44.png new file mode 100644 index 0000000..873728e Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/image/Speaker44.png differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker45.png b/s5e-icons/assets/walkie_talkie/image/Speaker45.png new file mode 100644 index 0000000..70535de Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/image/Speaker45.png differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker46.png b/s5e-icons/assets/walkie_talkie/image/Speaker46.png new file mode 100644 index 0000000..43563a3 Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/image/Speaker46.png differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker47.png b/s5e-icons/assets/walkie_talkie/image/Speaker47.png new file mode 100644 index 0000000..f7bd1b7 Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/image/Speaker47.png differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker48.png b/s5e-icons/assets/walkie_talkie/image/Speaker48.png new file mode 100644 index 0000000..6a942f9 Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/image/Speaker48.png differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker49.png b/s5e-icons/assets/walkie_talkie/image/Speaker49.png new file mode 100644 index 0000000..f65235c Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/image/Speaker49.png differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker5.png b/s5e-icons/assets/walkie_talkie/image/Speaker5.png new file mode 100644 index 0000000..8681f10 Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/image/Speaker5.png differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker50.png b/s5e-icons/assets/walkie_talkie/image/Speaker50.png new file mode 100644 index 0000000..be90a63 Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/image/Speaker50.png differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker51.png b/s5e-icons/assets/walkie_talkie/image/Speaker51.png new file mode 100644 index 0000000..d6de73f Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/image/Speaker51.png differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker52.png b/s5e-icons/assets/walkie_talkie/image/Speaker52.png new file mode 100644 index 0000000..8b58b2d Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/image/Speaker52.png differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker53.png b/s5e-icons/assets/walkie_talkie/image/Speaker53.png new file mode 100644 index 0000000..7b3b206 Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/image/Speaker53.png differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker54.png b/s5e-icons/assets/walkie_talkie/image/Speaker54.png new file mode 100644 index 0000000..2367008 Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/image/Speaker54.png differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker55.png b/s5e-icons/assets/walkie_talkie/image/Speaker55.png new file mode 100644 index 0000000..dbdf016 Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/image/Speaker55.png differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker56.png b/s5e-icons/assets/walkie_talkie/image/Speaker56.png new file mode 100644 index 0000000..1d3eacb Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/image/Speaker56.png differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker57.png b/s5e-icons/assets/walkie_talkie/image/Speaker57.png new file mode 100644 index 0000000..97c19e3 Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/image/Speaker57.png differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker58.png b/s5e-icons/assets/walkie_talkie/image/Speaker58.png new file mode 100644 index 0000000..a7195d0 Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/image/Speaker58.png differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker59.png b/s5e-icons/assets/walkie_talkie/image/Speaker59.png new file mode 100644 index 0000000..3e36cba Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/image/Speaker59.png differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker6.png b/s5e-icons/assets/walkie_talkie/image/Speaker6.png new file mode 100644 index 0000000..2503467 Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/image/Speaker6.png differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker60.png b/s5e-icons/assets/walkie_talkie/image/Speaker60.png new file mode 100644 index 0000000..cb0e1b9 Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/image/Speaker60.png differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker61.png b/s5e-icons/assets/walkie_talkie/image/Speaker61.png new file mode 100644 index 0000000..93b8905 Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/image/Speaker61.png differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker62.png b/s5e-icons/assets/walkie_talkie/image/Speaker62.png new file mode 100644 index 0000000..d171dab Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/image/Speaker62.png differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker63.png b/s5e-icons/assets/walkie_talkie/image/Speaker63.png new file mode 100644 index 0000000..3bdfd0f Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/image/Speaker63.png differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker64.png b/s5e-icons/assets/walkie_talkie/image/Speaker64.png new file mode 100644 index 0000000..392093f Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/image/Speaker64.png differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker65.png b/s5e-icons/assets/walkie_talkie/image/Speaker65.png new file mode 100644 index 0000000..e26ca7c Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/image/Speaker65.png differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker66.png b/s5e-icons/assets/walkie_talkie/image/Speaker66.png new file mode 100644 index 0000000..57aff14 Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/image/Speaker66.png differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker67.png b/s5e-icons/assets/walkie_talkie/image/Speaker67.png new file mode 100644 index 0000000..2b7281a Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/image/Speaker67.png differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker68.png b/s5e-icons/assets/walkie_talkie/image/Speaker68.png new file mode 100644 index 0000000..cdb5592 Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/image/Speaker68.png differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker69.png b/s5e-icons/assets/walkie_talkie/image/Speaker69.png new file mode 100644 index 0000000..dc7eba5 Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/image/Speaker69.png differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker7.png b/s5e-icons/assets/walkie_talkie/image/Speaker7.png new file mode 100644 index 0000000..054a489 Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/image/Speaker7.png differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker70.png b/s5e-icons/assets/walkie_talkie/image/Speaker70.png new file mode 100644 index 0000000..6bbe93d Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/image/Speaker70.png differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker71.png b/s5e-icons/assets/walkie_talkie/image/Speaker71.png new file mode 100644 index 0000000..bc451fb Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/image/Speaker71.png differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker72.png b/s5e-icons/assets/walkie_talkie/image/Speaker72.png new file mode 100644 index 0000000..69b5ee7 Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/image/Speaker72.png differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker73.png b/s5e-icons/assets/walkie_talkie/image/Speaker73.png new file mode 100644 index 0000000..909de00 Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/image/Speaker73.png differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker74.png b/s5e-icons/assets/walkie_talkie/image/Speaker74.png new file mode 100644 index 0000000..a2d65b2 Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/image/Speaker74.png differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker75.png b/s5e-icons/assets/walkie_talkie/image/Speaker75.png new file mode 100644 index 0000000..77d3cf6 Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/image/Speaker75.png differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker76.png b/s5e-icons/assets/walkie_talkie/image/Speaker76.png new file mode 100644 index 0000000..4bacc59 Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/image/Speaker76.png differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker77.png b/s5e-icons/assets/walkie_talkie/image/Speaker77.png new file mode 100644 index 0000000..3527380 Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/image/Speaker77.png differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker78.png b/s5e-icons/assets/walkie_talkie/image/Speaker78.png new file mode 100644 index 0000000..f9cfe31 Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/image/Speaker78.png differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker79.png b/s5e-icons/assets/walkie_talkie/image/Speaker79.png new file mode 100644 index 0000000..4583948 Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/image/Speaker79.png differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker8.png b/s5e-icons/assets/walkie_talkie/image/Speaker8.png new file mode 100644 index 0000000..6b885ec Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/image/Speaker8.png differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker80.png b/s5e-icons/assets/walkie_talkie/image/Speaker80.png new file mode 100644 index 0000000..6dbe5d3 Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/image/Speaker80.png differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker81.png b/s5e-icons/assets/walkie_talkie/image/Speaker81.png new file mode 100644 index 0000000..cdff971 Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/image/Speaker81.png differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker82.png b/s5e-icons/assets/walkie_talkie/image/Speaker82.png new file mode 100644 index 0000000..304ccea Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/image/Speaker82.png differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker83.png b/s5e-icons/assets/walkie_talkie/image/Speaker83.png new file mode 100644 index 0000000..fe62e25 Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/image/Speaker83.png differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker84.png b/s5e-icons/assets/walkie_talkie/image/Speaker84.png new file mode 100644 index 0000000..263f40c Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/image/Speaker84.png differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker85.png b/s5e-icons/assets/walkie_talkie/image/Speaker85.png new file mode 100644 index 0000000..060ae88 Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/image/Speaker85.png differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker86.png b/s5e-icons/assets/walkie_talkie/image/Speaker86.png new file mode 100644 index 0000000..c8fec2a Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/image/Speaker86.png differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker87.png b/s5e-icons/assets/walkie_talkie/image/Speaker87.png new file mode 100644 index 0000000..053ee6d Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/image/Speaker87.png differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker88.png b/s5e-icons/assets/walkie_talkie/image/Speaker88.png new file mode 100644 index 0000000..15505f5 Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/image/Speaker88.png differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker89.png b/s5e-icons/assets/walkie_talkie/image/Speaker89.png new file mode 100644 index 0000000..162e637 Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/image/Speaker89.png differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker9.png b/s5e-icons/assets/walkie_talkie/image/Speaker9.png new file mode 100644 index 0000000..03b9b1e Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/image/Speaker9.png differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker90.png b/s5e-icons/assets/walkie_talkie/image/Speaker90.png new file mode 100644 index 0000000..ceec461 Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/image/Speaker90.png differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker91.png b/s5e-icons/assets/walkie_talkie/image/Speaker91.png new file mode 100644 index 0000000..b3b35ae Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/image/Speaker91.png differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker92.png b/s5e-icons/assets/walkie_talkie/image/Speaker92.png new file mode 100644 index 0000000..3464b7d Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/image/Speaker92.png differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker93.png b/s5e-icons/assets/walkie_talkie/image/Speaker93.png new file mode 100644 index 0000000..de800f6 Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/image/Speaker93.png differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker94.png b/s5e-icons/assets/walkie_talkie/image/Speaker94.png new file mode 100644 index 0000000..51e6c2a Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/image/Speaker94.png differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker95.png b/s5e-icons/assets/walkie_talkie/image/Speaker95.png new file mode 100644 index 0000000..a22dffe Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/image/Speaker95.png differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker96.png b/s5e-icons/assets/walkie_talkie/image/Speaker96.png new file mode 100644 index 0000000..f7151aa Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/image/Speaker96.png differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker97.png b/s5e-icons/assets/walkie_talkie/image/Speaker97.png new file mode 100644 index 0000000..eae1262 Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/image/Speaker97.png differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker98.png b/s5e-icons/assets/walkie_talkie/image/Speaker98.png new file mode 100644 index 0000000..328931c Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/image/Speaker98.png differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker99.png b/s5e-icons/assets/walkie_talkie/image/Speaker99.png new file mode 100644 index 0000000..11a494e Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/image/Speaker99.png differ diff --git a/s5e-icons/assets/walkie_talkie/image/go_next.png b/s5e-icons/assets/walkie_talkie/image/go_next.png new file mode 100644 index 0000000..e9a1d81 Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/image/go_next.png differ diff --git a/s5e-icons/assets/walkie_talkie/image/stop.png b/s5e-icons/assets/walkie_talkie/image/stop.png new file mode 100644 index 0000000..b611709 Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/image/stop.png differ diff --git a/s5e-icons/assets/walkie_talkie/launcher.png b/s5e-icons/assets/walkie_talkie/launcher.png new file mode 100644 index 0000000..9a611a9 Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/launcher.png differ diff --git a/s5e-icons/assets/walkie_talkie/launcher1.png b/s5e-icons/assets/walkie_talkie/launcher1.png new file mode 100644 index 0000000..60aa469 Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/launcher1.png differ diff --git a/s5e-icons/assets/walkie_talkie/listen.png b/s5e-icons/assets/walkie_talkie/listen.png new file mode 100644 index 0000000..e06f591 Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/listen.png differ diff --git a/s5e-icons/assets/walkie_talkie/login_success.png b/s5e-icons/assets/walkie_talkie/login_success.png new file mode 100644 index 0000000..6e8c826 Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/login_success.png differ diff --git a/s5e-icons/assets/walkie_talkie/next_btn.png b/s5e-icons/assets/walkie_talkie/next_btn.png new file mode 100644 index 0000000..415abc0 Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/next_btn.png differ diff --git a/s5e-icons/assets/walkie_talkie/poc/default_icon.png b/s5e-icons/assets/walkie_talkie/poc/default_icon.png new file mode 100644 index 0000000..d5c33f0 Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/poc/default_icon.png differ diff --git a/s5e-icons/assets/walkie_talkie/poc/group_icon1.png b/s5e-icons/assets/walkie_talkie/poc/group_icon1.png new file mode 100644 index 0000000..9f22404 Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/poc/group_icon1.png differ diff --git a/s5e-icons/assets/walkie_talkie/poc/group_icon2.png b/s5e-icons/assets/walkie_talkie/poc/group_icon2.png new file mode 100644 index 0000000..0adef53 Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/poc/group_icon2.png differ diff --git a/s5e-icons/assets/walkie_talkie/poc/group_icon3.png b/s5e-icons/assets/walkie_talkie/poc/group_icon3.png new file mode 100644 index 0000000..4f1560b Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/poc/group_icon3.png differ diff --git a/s5e-icons/assets/walkie_talkie/poc/group_icon4.png b/s5e-icons/assets/walkie_talkie/poc/group_icon4.png new file mode 100644 index 0000000..5bb064a Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/poc/group_icon4.png differ diff --git a/s5e-icons/assets/walkie_talkie/poc/group_icon5.png b/s5e-icons/assets/walkie_talkie/poc/group_icon5.png new file mode 100644 index 0000000..abd170d Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/poc/group_icon5.png differ diff --git a/s5e-icons/assets/walkie_talkie/poc/member_icon.png b/s5e-icons/assets/walkie_talkie/poc/member_icon.png new file mode 100644 index 0000000..d5c33f0 Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/poc/member_icon.png differ diff --git a/s5e-icons/assets/walkie_talkie/poc/no_sound.png b/s5e-icons/assets/walkie_talkie/poc/no_sound.png new file mode 100644 index 0000000..2244475 Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/poc/no_sound.png differ diff --git a/s5e-icons/assets/walkie_talkie/poc/say_button_mic_icon.png b/s5e-icons/assets/walkie_talkie/poc/say_button_mic_icon.png new file mode 100644 index 0000000..64e5ea1 Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/poc/say_button_mic_icon.png differ diff --git a/s5e-icons/assets/walkie_talkie/poc/sound.png b/s5e-icons/assets/walkie_talkie/poc/sound.png new file mode 100644 index 0000000..9b7b453 Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/poc/sound.png differ diff --git a/s5e-icons/assets/walkie_talkie/poc/talk_listen.png b/s5e-icons/assets/walkie_talkie/poc/talk_listen.png new file mode 100644 index 0000000..c9a9688 Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/poc/talk_listen.png differ diff --git a/s5e-icons/assets/walkie_talkie/poc/toast_listen.png b/s5e-icons/assets/walkie_talkie/poc/toast_listen.png new file mode 100644 index 0000000..c9a9688 Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/poc/toast_listen.png differ diff --git a/s5e-icons/assets/walkie_talkie/poc/toast_say.png b/s5e-icons/assets/walkie_talkie/poc/toast_say.png new file mode 100644 index 0000000..f5b5dec Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/poc/toast_say.png differ diff --git a/s5e-icons/assets/walkie_talkie/poc/tools.png b/s5e-icons/assets/walkie_talkie/poc/tools.png new file mode 100644 index 0000000..e7cf9be Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/poc/tools.png differ diff --git a/s5e-icons/assets/walkie_talkie/poc/voice_icon.png b/s5e-icons/assets/walkie_talkie/poc/voice_icon.png new file mode 100644 index 0000000..85e366d Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/poc/voice_icon.png differ diff --git a/s5e-icons/assets/walkie_talkie/poc/voice_icon_rt.png b/s5e-icons/assets/walkie_talkie/poc/voice_icon_rt.png new file mode 100644 index 0000000..501320a Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/poc/voice_icon_rt.png differ diff --git a/s5e-icons/assets/walkie_talkie/speak.png b/s5e-icons/assets/walkie_talkie/speak.png new file mode 100644 index 0000000..1b715e3 Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/speak.png differ diff --git a/s5e-icons/assets/walkie_talkie/walkie_talkie.png b/s5e-icons/assets/walkie_talkie/walkie_talkie.png new file mode 100644 index 0000000..9a611a9 Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/walkie_talkie.png differ diff --git a/s5e-icons/assets/walkie_talkie/walkie_toast_icon.png b/s5e-icons/assets/walkie_talkie/walkie_toast_icon.png new file mode 100644 index 0000000..81f4e3f Binary files /dev/null and b/s5e-icons/assets/walkie_talkie/walkie_toast_icon.png differ diff --git a/s5e-icons/dist/S5eIcons.face b/s5e-icons/dist/S5eIcons.face new file mode 100644 index 0000000..f51a2a8 Binary files /dev/null and b/s5e-icons/dist/S5eIcons.face differ diff --git a/s5e-icons/dist/S5eIcons_toolchain.face b/s5e-icons/dist/S5eIcons_toolchain.face new file mode 100644 index 0000000..ac7f942 Binary files /dev/null and b/s5e-icons/dist/S5eIcons_toolchain.face differ diff --git a/s5e-icons/docs/logo_preview_480.png b/s5e-icons/docs/logo_preview_480.png new file mode 100644 index 0000000..f3269c3 Binary files /dev/null and b/s5e-icons/docs/logo_preview_480.png differ diff --git a/s5e-icons/docs/s5e_before_after.png b/s5e-icons/docs/s5e_before_after.png new file mode 100644 index 0000000..61e05c9 Binary files /dev/null and b/s5e-icons/docs/s5e_before_after.png differ diff --git a/s5e-icons/docs/s5e_pressure.png b/s5e-icons/docs/s5e_pressure.png new file mode 100644 index 0000000..b6d1043 Binary files /dev/null and b/s5e-icons/docs/s5e_pressure.png differ diff --git a/s5e-icons/docs/s5e_sysicons.png b/s5e-icons/docs/s5e_sysicons.png new file mode 100644 index 0000000..1c12b16 Binary files /dev/null and b/s5e-icons/docs/s5e_sysicons.png differ diff --git a/s5e-icons/docs/s5e_ui_all.png b/s5e-icons/docs/s5e_ui_all.png new file mode 100644 index 0000000..1c8d7c9 Binary files /dev/null and b/s5e-icons/docs/s5e_ui_all.png differ diff --git a/s5e-icons/docs/stock_vs_os4.png b/s5e-icons/docs/stock_vs_os4.png new file mode 100644 index 0000000..f49a746 Binary files /dev/null and b/s5e-icons/docs/stock_vs_os4.png differ diff --git a/s5e-icons/docs/zoom_check.png b/s5e-icons/docs/zoom_check.png new file mode 100644 index 0000000..d73f67c Binary files /dev/null and b/s5e-icons/docs/zoom_check.png differ diff --git a/s5e-icons/docs/历史-S5eSIM-图标替换-说明.md b/s5e-icons/docs/历史-S5eSIM-图标替换-说明.md new file mode 100644 index 0000000..19e6b35 --- /dev/null +++ b/s5e-icons/docs/历史-S5eSIM-图标替换-说明.md @@ -0,0 +1,246 @@ +# Xiaomi Watch S5 eSIM 图标替换 Lua 表盘 —— 交付说明 + +> 目标固件:`miwear.watch.p62lte_v3.112.035_full_adbe74f0 (1).bin`(**Xiaomi Watch S5 eSIM 46mm**,平台 p62lte,480×480) +> 图标来源:**Xiaomi Watch S5 41mm(q63, v4.101.020)的 OS4 图标包**(即上一个项目里那份 S5 图标) +> 全部结论都有本机脚本证据;**真机行为未验证**,第 8 节列出了要你实测的三点。 + +--- + +## 1. 交付物 + +| 文件 | 说明 | +|---|---| +| `_out/faces/S5eIcons.face` | **主交付物** 8,983,508 B(sha256 17f3b9444a0477c5…),表盘 id `462150101`,b5=0(与固件自带表盘一致) | +| `_out/faces/S5eIcons_toolchain.face` | 官方工具链原样产物(b5=10);上面那个装不上时换这个 | +| `_out/s5e_from_os4/vela_app_os4.bin` | 改好图标的整分区镜像(走刷机通道,绕开设备侧脚本) | +| `_out/s5e_from_os4/vela_health_os4.bin` | 同上(health 分区) | +| `_out/s5e_icons/s5e_before_after.png` | 47 个图标:原厂 → 替换后 对照图 | +| `_out/s5e_ui_all.png` | 480×480 七个页面预览(真 MiSans 字体渲染) | +| `watchface_s5e/main.lua` | 表盘源码(带 __UIIMG__ / __PAY__ / __RB__ 占位符) | +| `watchface_s5e/payload/`、`payload_rollback/` | 负载文件 + 清单 | +| `_tools/s5e_*.py` | 全部生成 / 构建 / 校验脚本 | + +--- + +## 2. 目标固件的事实(从固件包里查出来的,不是猜的) + +| 项 | 结论 | 证据 | +|---|---|---| +| 机型 | **Xiaomi Watch S5 eSIM 46mm** | `vela_ap.bin` / `vela_cp_image.bin` 里的机型字符串 | +| 屏幕 | **480×480** | 固件自带表盘素材 `lua/daynight/asset/theme5/bigsun.bin`、`lua/complexzone/asset/Theme1/citymage1.bin` 都是 I8 480×480(231,436 B);`complexzone.lua` 建 480×480 根对象 | +| Lua 宿主 | **有**(Lua 5.4) | `vela_ap.bin`:`Lua 5.4`、`Copyright (C) 1994-2020`、`cannot create state`、`SCRIPT_PATH`、`watchface.db`、`luavgl`,以及**同样的 7 个 C 模块** `luaopen_activity/dataman/miwear/navigator/screen/topic/vibrator` | +| 自带 Lua 表盘 | 7 个 | `vela_watchface.bin`:outdooradv / daynight / corona / complexzone / clearheart / circle / asymmetryzone | +| 图标槽位 | `/dev/app` **34** 个 + `/dev/health` **15** 个 | ROMFS `app` / `health` 卷里的 `*/launcher.bin` | +| 图标格式 | **全部 144×144 ARGB8888 = 82,956 B** | 逐个解码确认 —— **这台机器没有 I8 图标槽位**(S4 eSIM 有 6 个),所以不会有 256 色量化损失 | +| 原厂画法 | 内容 136×136 居中(4px 透明边);`launcher_*` 是 138@(3,3);`debug/lua/voice_alexa/sports_game` 满幅 144 | 逐像素量 alpha 边界 | +| 分区节点 | `/dev/app`、`/dev/health` | 官方 `ota.sh`:`dd if=/ota/vela_app.bin of=/dev/app`、`... of=/dev/health` | + +--- + +## 3. 替换了什么 + +- 能对上 **47** 个槽位(app 33 + health 14);OS4 里没有对应的 2 个槽位**保持原厂**:`walkie_talkie`、`sports_game` +- 其中 **8 个槽位**的 OS4 图标与 S5 eSIM 原厂**逐字节相同**(debug / esimsms / interconnect / lua / sports / sports_course / sports_record / sports_training),照样写入,保证整套一致 +- **另外搬了 8 个系统 UI 素材**(都在 `/dev/app`,从 q63 取同名文件):`system/icon/lock_icon.bin`(锁屏上的锁)、`common/icon/system_lock.bin`(状态栏系统锁)、`common/icon/quiet_mode.bin`(免打扰小图标)、`notifications/default.bin`(通知默认图)、`notifications/no_message.bin`(通知空状态)、`settings/icon/notify.bin`(设置-通知,80→64 缩放)、`mijia/enable|disable_donot_disturb.bin`(米家免打扰,80→64 缩放)。对照图见 `docs/s5e_sysicons.png`。 +- **压力 App 小表情搬了 14 张**(`pressure/icon/`,都在 `/dev/health`):4 档情绪 48px + 32px 三张 + `press_grade1..4` + `presure_icon` + 两张表情底版 +- **控制中心磁贴整套搬了 22 张**(`control_center/icon/*`,128×128 ← q63 116×116 缩放)——这就是你在 S4 eSIM 上看到效果的那批。 +- **`activities` 用自制图覆盖**(`assets/override/activities.png`,112×112 的跑道/同心环,与 S4 项目同一张):裁到实心 bbox(100×100) → 放大到目标槽位画法(136×136,LANCZOS+锐化) → 居中粘回 (4,4);实测成品外径与原厂图标完全对齐 +- **尺寸归一化**:把 OS4 的满幅 144 图按目标槽位的画法缩放(多数 136、`launcher_*` 138)并居中到同一原点 —— 这样**每条负载长度与原槽位完全相等**,ROMFS 的 inode 表和后面所有文件的偏移一个字节都不动 +- 负载 **4.41 MB**(替换 + 回滚各一份),.face 共 8.98 MB + +--- + +## 4. 91 条偏移表(由 `_out/s5e_from_os4/manifest.json` 生成) + +| # | 分区 | 名称 | 分区内偏移 | 长度 | 备注 | +|---|---|---|---|---|---| +| 1 | app | alarm | 0x06226200 | 82956 | | +| 2 | app | calendar | 0x05C23600 | 82956 | | +| 3 | app | chronograph | 0x059D1A00 | 82956 | | +| 4 | app | compass | 0x05802000 | 82956 | | +| 5 | app | debug | 0x05546800 | 82956 | 与 OS4 一致 | +| 6 | app | esimsms | 0x053E6E00 | 82956 | 与 OS4 一致 | +| 7 | app | find_phone | 0x05374C00 | 82956 | | +| 8 | app | flashlight | 0x0535CE00 | 82956 | | +| 9 | app | interconnect | 0x05183C00 | 82956 | 与 OS4 一致 | +| 10 | app | launcher_baidumap | 0x0516F600 | 82956 | | +| 11 | app | launcher_bdmap | 0x0515B000 | 82956 | | +| 12 | app | launcher_calculator | 0x05146A00 | 82956 | | +| 13 | app | launcher_kugou | 0x04DB9400 | 82956 | | +| 14 | app | launcher_micar | 0x04D80A00 | 82956 | | +| 15 | app | launcher_netease | 0x04D6BA00 | 82956 | | +| 16 | app | launcher_wechat | 0x04D57400 | 82956 | | +| 17 | app | launcher_ximalaya | 0x04D42000 | 82956 | | +| 18 | app | lua | 0x04D2DA00 | 82956 | 与 OS4 一致 | +| 19 | app | media | 0x04CFEE00 | 82956 | | +| 20 | app | mijia | 0x04BC8600 | 82956 | | +| 21 | app | nfccard | 0x04900C00 | 82956 | | +| 22 | app | perpetual_calendar | 0x04469C00 | 82956 | | +| 23 | app | phone | 0x0433B200 | 82956 | | +| 24 | app | phone_contacts | 0x04391A00 | 82956 | | +| 25 | app | recorder | 0x042B9400 | 82956 | | +| 26 | app | remote_camera | 0x04088200 | 82956 | | +| 27 | app | settings | 0x02C41C00 | 82956 | | +| 28 | app | timer | 0x02949E00 | 82956 | | +| 29 | app | todolist | 0x0291F200 | 82956 | | +| 30 | app | voice_aivs | 0x022DF800 | 82956 | | +| 31 | app | voice_alexa | 0x021AA600 | 82956 | | +| 32 | app | weather | 0x01B09400 | 82956 | | +| 33 | app | wxpay | 0x0000BC00 | 82956 | | +| 34 | health | activities | 0x0C51B600 | 82956 | | +| 35 | health | barometer | 0x0B910400 | 82956 | | +| 36 | health | breath | 0x08A2BC00 | 82956 | | +| 37 | health | health_today | 0x07077E00 | 82956 | | +| 38 | health | heartrate | 0x06EBB200 | 82956 | | +| 39 | health | innovation_research | 0x06558600 | 82956 | | +| 40 | health | oxygen | 0x0634AA00 | 82956 | | +| 41 | health | pressure | 0x03B9D400 | 82956 | | +| 42 | health | sleep | 0x0272F000 | 82956 | | +| 43 | health | sports | 0x01E89200 | 82956 | 与 OS4 一致 | +| 44 | health | sports_course | 0x02692600 | 82956 | 与 OS4 一致 | +| 45 | health | sports_record | 0x01B5FA00 | 82956 | 与 OS4 一致 | +| 46 | health | sports_training | 0x015A6400 | 82956 | 与 OS4 一致 | +| 47 | health | womenhealth | 0x001E3A00 | 82956 | | +| 48 | app | system/icon/lock_icon.bin | 0x02AB2200 | 2060 | 锁屏上的锁 | +| 49 | app | common/icon/system_lock.bin | 0x058DFC00 | 3340 | 状态栏系统锁 | +| 50 | app | common/icon/quiet_mode.bin | 0x058FCC00 | 3340 | 免打扰/静音小图标 | +| 51 | app | notifications/default.bin | 0x044BE800 | 32012 | 通知默认大图标 | +| 52 | app | notifications/no_message.bin | 0x044B2C00 | 7436 | 通知中心空状态 | +| 53 | app | settings/icon/notify.bin | 0x02D97C00 | 5132 | 设置-通知 | +| 54 | app | mijia/enable_donot_disturb.bin | 0x04BE3A00 | 5132 | 米家 免打扰开 | +| 55 | app | mijia/disable_donot_disturb.bin | 0x04BE6E00 | 5132 | 米家 免打扰关 | +| 56 | health | pressure/icon/presure_relax.bin | 0x03C85400 | 3340 | 压力表情-放松 48 | +| 57 | health | pressure/icon/presure_mild.bin | 0x03C89600 | 3340 | 压力表情-轻度 48 | +| 58 | health | pressure/icon/presure_mid.bin | 0x03C8D800 | 3340 | 压力表情-中度 48 | +| 59 | health | pressure/icon/presure_severe.bin | 0x03C81200 | 3340 | 压力表情-重度 48 | +| 60 | health | pressure/icon/presure_mild32.bin | 0x03C88C00 | 2060 | 压力表情-轻度 32 | +| 61 | health | pressure/icon/presure_mid32.bin | 0x03C8CE00 | 2060 | 压力表情-中度 32 | +| 62 | health | pressure/icon/presure_severe32.bin | 0x03C80800 | 2060 | 压力表情-重度 32 | +| 63 | health | pressure/icon/presure_icon.bin | 0x03C8E600 | 3340 | 压力 App 小图标 | +| 64 | health | pressure/icon/press_grade1.bin | 0x03E62A00 | 2060 | 压力等级徽标 1 | +| 65 | health | pressure/icon/press_grade2.bin | 0x03E62000 | 2060 | 压力等级徽标 2 | +| 66 | health | pressure/icon/press_grade3.bin | 0x03E61600 | 2060 | 压力等级徽标 3 | +| 67 | health | pressure/icon/press_grade4.bin | 0x03E60C00 | 2060 | 压力等级徽标 4 | +| 68 | health | pressure/icon/pressure_eomji_bg.bin | 0x03DC1E00 | 54636 | 表情底版 | +| 69 | health | pressure/icon/pressure_eomji_widbg.bin | 0x03DB9200 | 35628 | 表情底版(小) | +| 70 | app | control_center/icon/settings.bin | 0x0557CC00 | 17420 | 控制中心磁贴 | +| 71 | app | control_center/icon/raise_bright_open.bin | 0x05581200 | 17420 | 控制中心磁贴 | +| 72 | app | control_center/icon/raise_bright_close.bin | 0x05589E00 | 17420 | 控制中心磁贴 | +| 73 | app | control_center/icon/not_disturb_open.bin | 0x05592A00 | 17420 | 控制中心磁贴 | +| 74 | app | control_center/icon/not_disturb_close.bin | 0x0559B600 | 17420 | 控制中心磁贴 | +| 75 | app | control_center/icon/mute_open.bin | 0x055A1800 | 17420 | 控制中心磁贴 | +| 76 | app | control_center/icon/mute_close.bin | 0x055AA400 | 17420 | 控制中心磁贴 | +| 77 | app | control_center/icon/mobile_network_open.bin | 0x055AEA00 | 17420 | 控制中心磁贴 | +| 78 | app | control_center/icon/mobile_network_close.bin | 0x055C0200 | 17420 | 控制中心磁贴 | +| 79 | app | control_center/icon/game_mode_on.bin | 0x055D2800 | 17420 | 控制中心磁贴 | +| 80 | app | control_center/icon/game_mode_off.bin | 0x055DB400 | 17420 | 控制中心磁贴 | +| 81 | app | control_center/icon/flashlight.bin | 0x055E4000 | 17420 | 控制中心磁贴 | +| 82 | app | control_center/icon/find_phone_close.bin | 0x055F1200 | 17420 | 控制中心磁贴 | +| 83 | app | control_center/icon/earphone.bin | 0x055F6600 | 3340 | 控制中心磁贴 | +| 84 | app | control_center/icon/drain_close.bin | 0x05604600 | 17420 | 控制中心磁贴 | +| 85 | app | control_center/icon/cancel.bin | 0x05609A00 | 3340 | 控制中心磁贴 | +| 86 | app | control_center/icon/bt_connect.bin | 0x0560C400 | 3340 | 控制中心磁贴 | +| 87 | app | control_center/icon/battery_open.bin | 0x0560D200 | 17420 | 控制中心磁贴 | +| 88 | app | control_center/icon/battery_close.bin | 0x05615E00 | 17420 | 控制中心磁贴 | +| 89 | app | control_center/icon/air_mode_open.bin | 0x0561A400 | 17420 | 控制中心磁贴 | +| 90 | app | control_center/icon/air_mode_close.bin | 0x0562CA00 | 17420 | 控制中心磁贴 | +| 91 | app | control_center/icon/add.bin | 0x05631000 | 3340 | 控制中心磁贴 | +> 偏移都 512 对齐、长度都是 4 的倍数 → 设备侧 `dd bs=4` 正好写满目标区间,不会多写一个字节到相邻文件里。 + +--- + +## 5. 表盘是怎么写的(机制) + +Lua 表盘不能设置图标;系统图标住在 ROMFS 分区镜像里,必须**写分区 + 重启**才生效。表盘只做运载 + 执行: + +``` +每条负载三步(只用官方存在的 dd,判据全看内容、不看返回码): + 1 版本校验:dd 读出设备上该偏移处 64 字节 -> 与「原厂字节」比对 + 不一致就判为「版本不符」,该分区剩余记录全部跳过(不写坏) + 2 写入 :dd if=/tmp/s5e_wr.bin of=<分区> bs=4 seek=<偏移/4> count=<长度/4> + 3 回读校验:dd 读回整段 82,956 字节 -> 与负载逐字节比对 +回滚 = 同一套流程,只是写入换成原厂字节、版本校验改比对替换后的字节 +最后 sync + reboot(ROMFS 是只读挂载 + 有页缓存,不重启看不到新图标) +``` + +**为什么能等长原地替换**:ROMFS 把 inode、名字、数据首尾相接顺序排布,绝对偏移全靠前面所有内容的长度决定。所有替换都是等长覆盖,所以 inode 表 / superblock / 目录项 / 后续文件偏移全部不变(本机逐位验证)。 +但偏移**锁固件版本**,所以第 1 步的版本校验是必须的:别的版本会全部「版本不符」跳过,而不是把图标写进别人的文件里。 + +表盘里的偏移是**构建时两遍测量回填**的:先编一遍,在 .face 里搜负载魔数定位,量出真实偏移写回 Lua,再编第二遍并逐条校验 0 漂移。 + +--- + +## 6. 本机验证结果(全绿) + +| 验证项 | 结果 | +|---|---| +| 负载回放 == 改后镜像(`vela_app_os4.bin` / `vela_health_os4.bin`) | 逐字节相同 | +| 回滚回放 == 原厂镜像 | 逐字节相同(sha256 一致) | +| inode 表 / superblock / 条目数 | 逐位不变(3432 / 5555 条) | +| 变化的文件 | 恰好只有被改的 `launcher.bin`(app 29、health 10 有实际差异) | +| UI 素材、负载、回滚在最终 .face 里的偏移 | 从 .face 里的 Lua 表读出来重新校验,逐条字节一致(`_tools/s5e_verify.py`,独立于构建脚本) | +| 两遍构建偏移漂移 | 0 | +| 480 页面圆屏切边 | 真实渲染像素测量,7 个页面全部 0 越界 | +| Lua 语法块平衡 | balanced | +| 预览图尺寸 | 326×326(Compiler.exe 对 462 设备强制,实测) | + +--- + +## 7. 「环境预检查」通过意味着什么(**重要**) + +预检查是**只读**的,所以它是**必要但不充分**条件: + +| 预检查能证明 | 预检查证明不了 | +|---|---| +| `/dev/app`、`/dev/health` 两个节点存在且**可读** | **可写** —— 只有真正写第一条时才知道(这一步一个字节都不写) | +| 分区头是 `-rom1fs-`、卷名是 `app` / `health`、报出分区大小 | 表盘宿主的字体/界面能力 | +| 每分区抽 1 条对版:显示 `app 原厂✓` 或 `app 已替换✓` | 其余 46 条的偏移(这些在写入时逐条对版) | +| 负载表全部 4 对齐、不越界 | | + +**为什么证明不了「可写」也敢点**:写入路径自带三重保护 —— + +1. 每条写之前先读设备上 64 字节**对版**:等于「原厂字节」或「替换后字节」任一才动手;两者都不是就判「版本不符」,**该分区剩余记录全部跳过**,绝不往别人的文件里写 +2. 写完整段 82,956 字节**回读比对**,不一致就计失败 +3. 只做**等长原地覆盖**,不碰 inode 表 / superblock / 目录项 —— 最坏结果是「图标没变」,不会把分区写坏 + +**判断成功的唯一标准**:跑完显示 `> 成功 91 / 失败 0(共 91)` 和 `> app 63/63 health 28/28`。**不是 0 就先别重启**,点「回滚修改」再排查。 + +> 因为对版接受两种状态,**重复点「开始替换」或断电后半途重来都是安全的**(已经写入新图标的那部分不会被判成「版本不符」)。 + +--- + +## 8. 还没验证的(真机只能你来测) + +1. **表盘宿主进程有没有权限写 `/dev/app` 和 `/dev/health`** + S4 eSIM 上写 `/dev/app` 是实测通过的;这台同源平台但没测过。**请先点「环境预检查」**(只读,不写任何字节)。 +2. **`lvgl.Font('MiSans-Medium' / 'MiSans-Semibold', size)` 在这台上的表现** + `vela_font.bin` 里确实有 `MiSans-Semibold.ttf` / `MiSans-Medium.ttf`,S4 上这个 API 可用。界面文字依赖它;若不可用,文字会退回主题默认字体(中文可能缺字)。 +3. **`/dev/health` 的写权限**。如果它不可写,app 的 33 条仍会成功,日志里 health 会全失败。 +4. **安装通道**:和 `S5Icons.face` 一样走你的表盘安装方式(Compiler.exe 内部用的是 push 到 `/data/app/watchface/market//resource.bin`)。 + +输出怎么读(「正在替换」页): + +- 每 10 条一行 `> n/47 成 x 失 y` +- 结束:`> 成功 x / 失败 y(共 47)`,必要时 `> 版本不符跳过 n 条`,以及 `> app a/33 health b/14` +- **失败 0** 表示 47 条都写入并回读校验通过 → 点「重启手表」 + +--- + +## 9. 想改的话 + +```powershell +$py = "C:\\Users\\hi\\.dsh\\dsh-runtimes\\dsh-primary-runtime\\dependencies\\python\\python.exe" + +# 只改界面/坐标:改 watchface_s5e/main.lua 与 _tools/s5e_pages.py,然后 +& $py _tools\\s5e_render_ui.py # 渲染 480 预览 +& $py _tools\\s5e_check_clip.py # 圆屏切边检查 +& $py _tools\\s5e_build.py # 两遍构建 -> _out\\faces\\S5eIcons.face +& $py _tools\\s5e_verify.py # 独立校验 + +# 换图标来源 / 换固件版本 +& $py _tools\\s5e_payload.py # 重新生成负载 + 整分区镜像(自带证明) +& $py _tools\\s5e_ui_images.py # 重新生成 UI 素材(改了素材时) +& $py _tools\\s5e_build.py +``` + +> `_build_s5e/`、`build_overlay_s5e/` 是产物,别手改;`watchface_s5e/main.lua` 才是源码。 \ No newline at end of file diff --git a/s5e-icons/docs/说明.md b/s5e-icons/docs/说明.md new file mode 100644 index 0000000..15edb84 --- /dev/null +++ b/s5e-icons/docs/说明.md @@ -0,0 +1,253 @@ +# Xiaomi Watch S5 eSIM 图标替换 Lua 表盘 —— 交付说明 + +> 目标固件:`miwear.watch.p62lte_v3.112.035_full_adbe74f0 (1).bin`(**Xiaomi Watch S5 eSIM 46mm**,平台 p62lte,480×480) +> 图标来源:**Xiaomi Watch S5 41mm(q63, v4.101.020)的 OS4 图标包**(即上一个项目里那份 S5 图标) +> 全部结论都有本机脚本证据;**真机行为未验证**,第 8 节列出了要你实测的三点。 + +--- + +## 1. 交付物 + +| 文件 | 说明 | +|---|---| +| `dist/S5eIcons.face` | **主交付物** 11,466,316 B(sha256 6d9c04f7017a1101…),表盘 id `462150101`,b5=0(与固件自带表盘一致) | +| `dist/S5eIcons_toolchain.face` | 官方工具链原样产物(b5=10);上面那个装不上时换这个 | +| `.work/s5e_from_os4/vela_app_os4.bin` | 改好图标的整分区镜像(走刷机通道,绕开设备侧脚本) | +| `.work/s5e_from_os4/vela_health_os4.bin` | 同上(health 分区) | +| `docs/s5e_before_after.png` | 47 个 launcher 图标:原厂 → 替换后 对照图 | +| `docs/s5e_sysicons.png` | 44 个系统 UI 素材(22 控制中心 + 14 压力表情 + 8 其他):原厂 → 从 S5 41mm 搬过来的 对照图 | +| `docs/s5e_pressure.png` | 14 张压力 App 小表情:原厂 → q63 对照图 | +| `docs/s5e_ui_all.png` | 480×480 七个页面预览(真 MiSans 字体渲染) | +| `main.lua` | 表盘源码(带 __UIIMG__ / __PAY__ / __RB__ 占位符) | +| `payload/`、`payload_rollback/` | 负载文件 + 清单 | +| `tools/s5e_*.py` | 全部生成 / 构建 / 校验脚本 | + +--- + +## 2. 目标固件的事实(从固件包里查出来的,不是猜的) + +| 项 | 结论 | 证据 | +|---|---|---| +| 机型 | **Xiaomi Watch S5 eSIM 46mm** | `vela_ap.bin` / `vela_cp_image.bin` 里的机型字符串 | +| 屏幕 | **480×480** | 固件自带表盘素材 `lua/daynight/asset/theme5/bigsun.bin`、`lua/complexzone/asset/Theme1/citymage1.bin` 都是 I8 480×480(231,436 B);`complexzone.lua` 建 480×480 根对象 | +| Lua 宿主 | **有**(Lua 5.4) | `vela_ap.bin`:`Lua 5.4`、`Copyright (C) 1994-2020`、`cannot create state`、`SCRIPT_PATH`、`watchface.db`、`luavgl`,以及**同样的 7 个 C 模块** `luaopen_activity/dataman/miwear/navigator/screen/topic/vibrator` | +| 自带 Lua 表盘 | 7 个 | `vela_watchface.bin`:outdooradv / daynight / corona / complexzone / clearheart / circle / asymmetryzone | +| 图标槽位 | `/dev/app` **34** 个 + `/dev/health` **15** 个 | ROMFS `app` / `health` 卷里的 `*/launcher.bin` | +| 图标格式 | **全部 144×144 ARGB8888 = 82,956 B** | 逐个解码确认 —— **这台机器没有 I8 图标槽位**(S4 eSIM 有 6 个),所以不会有 256 色量化损失 | +| 原厂画法 | 内容 136×136 居中(4px 透明边);`launcher_*` 是 138@(3,3);`debug/lua/voice_alexa/sports_game` 满幅 144 | 逐像素量 alpha 边界 | +| 分区节点 | `/dev/app`、`/dev/health` | 官方 `ota.sh`:`dd if=/ota/vela_app.bin of=/dev/app`、`... of=/dev/health` | + +--- + +## 3. 替换了什么 + +- 能对上 **47** 个槽位(app 33 + health 14);OS4 里没有对应的 2 个槽位**保持原厂**:`walkie_talkie`、`sports_game` +- 其中 **8 个槽位**的 OS4 图标与 S5 eSIM 原厂**逐字节相同**(debug / esimsms / interconnect / lua / sports / sports_course / sports_record / sports_training),照样写入,保证整套一致 +- **另外搬了 8 个系统 UI 素材**(都在 `/dev/app`,从 q63 取同名文件):`system/icon/lock_icon.bin`(锁屏上的锁)、`common/icon/system_lock.bin`(状态栏系统锁)、`common/icon/quiet_mode.bin`(免打扰小图标)、`notifications/default.bin`(通知默认图)、`notifications/no_message.bin`(通知空状态)、`settings/icon/notify.bin`(设置-通知,80→64 缩放)、`mijia/enable|disable_donot_disturb.bin`(米家免打扰,80→64 缩放)。对照图见 `docs/s5e_sysicons.png`。 +- **控制中心磁贴整套搬了 22 张**(`control_center/icon/*`,S5 eSIM 128×128 ← q63 116×116 缩放 + 复用源图 I8 调色板):勿扰 / 静音 / 手电 / 设置 / 飞行 / 电池 / 移动网络 / 抬腕亮屏 / 排水 / 查找手机 / 蓝牙 / 耳机 / 取消 / 加号 / 游戏模式 / 网络开关。q63 是**浅色主题**(白盘 + 浅色字形),S5 eSIM 原厂是蓝色主题;白盘放在深色控制中心上的实际观感已在 S4 eSIM 上验证过(`s4e-icons/docs/s4e_control_center.png`)。 +- **`activities` 用自制图覆盖**(`assets/override/activities.png`,112×112 的跑道/同心环,与 S4 项目同一张):裁到实心 bbox(100×100) → 放大到目标槽位画法(136×136,LANCZOS+锐化) → 居中粘回 (4,4);实测成品外径与原厂图标完全对齐 +- **尺寸归一化**:把 OS4 的满幅 144 图按目标槽位的画法缩放(多数 136、`launcher_*` 138)并居中到同一原点 —— 这样**每条负载长度与原槽位完全相等**,ROMFS 的 inode 表和后面所有文件的偏移一个字节都不动 +- 负载 **4.41 MB**(替换 + 回滚各一份),.face 共 8.98 MB + +--- + +## 4. 91 条偏移表(由 `.work/s5e_from_os4/manifest.json` 生成) + +> **2026-10-02 追加**:本表之后又加了 **160 条对讲机素材**(`walkie_talkie/**`,来自用户给的整套 PNG, +> 已放进 `assets/walkie_talkie/`)→ 现在一共 **251 条**(app 223 / health 28)。 +> 这些槽位是 **I8+RLE 压缩**、长度不是 4 的倍数,靠 `iconpack.encode_into_slot()`(量化 → RLE → +> `ctrl=0` 空指令补齐到原 `csize`)做到逐字节等长,设备端写入自动用 `dd bs=1`。 +> 完整 253 条名单见 `main.lua` 回填后的 PAYTBL(或 `.work/s5e_from_os4/manifest.json`)。 + +| # | 分区 | 名称 | 分区内偏移 | 长度 | 备注 | +|---|---|---|---|---|---| +| 1 | app | alarm | 0x06226200 | 82956 | | +| 2 | app | calendar | 0x05C23600 | 82956 | | +| 3 | app | chronograph | 0x059D1A00 | 82956 | | +| 4 | app | compass | 0x05802000 | 82956 | | +| 5 | app | debug | 0x05546800 | 82956 | 与 OS4 一致 | +| 6 | app | esimsms | 0x053E6E00 | 82956 | 与 OS4 一致 | +| 7 | app | find_phone | 0x05374C00 | 82956 | | +| 8 | app | flashlight | 0x0535CE00 | 82956 | | +| 9 | app | interconnect | 0x05183C00 | 82956 | 与 OS4 一致 | +| 10 | app | launcher_baidumap | 0x0516F600 | 82956 | | +| 11 | app | launcher_bdmap | 0x0515B000 | 82956 | | +| 12 | app | launcher_calculator | 0x05146A00 | 82956 | | +| 13 | app | launcher_kugou | 0x04DB9400 | 82956 | | +| 14 | app | launcher_micar | 0x04D80A00 | 82956 | | +| 15 | app | launcher_netease | 0x04D6BA00 | 82956 | | +| 16 | app | launcher_wechat | 0x04D57400 | 82956 | | +| 17 | app | launcher_ximalaya | 0x04D42000 | 82956 | | +| 18 | app | lua | 0x04D2DA00 | 82956 | 与 OS4 一致 | +| 19 | app | media | 0x04CFEE00 | 82956 | | +| 20 | app | mijia | 0x04BC8600 | 82956 | | +| 21 | app | nfccard | 0x04900C00 | 82956 | | +| 22 | app | perpetual_calendar | 0x04469C00 | 82956 | | +| 23 | app | phone | 0x0433B200 | 82956 | | +| 24 | app | phone_contacts | 0x04391A00 | 82956 | | +| 25 | app | recorder | 0x042B9400 | 82956 | | +| 26 | app | remote_camera | 0x04088200 | 82956 | | +| 27 | app | settings | 0x02C41C00 | 82956 | | +| 28 | app | timer | 0x02949E00 | 82956 | | +| 29 | app | todolist | 0x0291F200 | 82956 | | +| 30 | app | voice_aivs | 0x022DF800 | 82956 | | +| 31 | app | voice_alexa | 0x021AA600 | 82956 | | +| 32 | app | weather | 0x01B09400 | 82956 | | +| 33 | app | wxpay | 0x0000BC00 | 82956 | | +| 34 | health | activities | 0x0C51B600 | 82956 | | +| 35 | health | barometer | 0x0B910400 | 82956 | | +| 36 | health | breath | 0x08A2BC00 | 82956 | | +| 37 | health | health_today | 0x07077E00 | 82956 | | +| 38 | health | heartrate | 0x06EBB200 | 82956 | | +| 39 | health | innovation_research | 0x06558600 | 82956 | | +| 40 | health | oxygen | 0x0634AA00 | 82956 | | +| 41 | health | pressure | 0x03B9D400 | 82956 | | +| 42 | health | sleep | 0x0272F000 | 82956 | | +| 43 | health | sports | 0x01E89200 | 82956 | 与 OS4 一致 | +| 44 | health | sports_course | 0x02692600 | 82956 | 与 OS4 一致 | +| 45 | health | sports_record | 0x01B5FA00 | 82956 | 与 OS4 一致 | +| 46 | health | sports_training | 0x015A6400 | 82956 | 与 OS4 一致 | +| 47 | health | womenhealth | 0x001E3A00 | 82956 | | +| 48 | app | system/icon/lock_icon.bin | 0x02AB2200 | 2060 | 锁屏上的锁 | +| 49 | app | common/icon/system_lock.bin | 0x058DFC00 | 3340 | 状态栏系统锁 | +| 50 | app | common/icon/quiet_mode.bin | 0x058FCC00 | 3340 | 免打扰/静音小图标 | +| 51 | app | notifications/default.bin | 0x044BE800 | 32012 | 通知默认大图标 | +| 52 | app | notifications/no_message.bin | 0x044B2C00 | 7436 | 通知中心空状态 | +| 53 | app | settings/icon/notify.bin | 0x02D97C00 | 5132 | 设置-通知 | +| 54 | app | mijia/enable_donot_disturb.bin | 0x04BE3A00 | 5132 | 米家 免打扰开 | +| 55 | app | mijia/disable_donot_disturb.bin | 0x04BE6E00 | 5132 | 米家 免打扰关 | +| 56 | health | pressure/icon/presure_relax.bin | 0x03C85400 | 3340 | 压力表情-放松 48 | +| 57 | health | pressure/icon/presure_mild.bin | 0x03C89600 | 3340 | 压力表情-轻度 48 | +| 58 | health | pressure/icon/presure_mid.bin | 0x03C8D800 | 3340 | 压力表情-中度 48 | +| 59 | health | pressure/icon/presure_severe.bin | 0x03C81200 | 3340 | 压力表情-重度 48 | +| 60 | health | pressure/icon/presure_mild32.bin | 0x03C88C00 | 2060 | 压力表情-轻度 32 | +| 61 | health | pressure/icon/presure_mid32.bin | 0x03C8CE00 | 2060 | 压力表情-中度 32 | +| 62 | health | pressure/icon/presure_severe32.bin | 0x03C80800 | 2060 | 压力表情-重度 32 | +| 63 | health | pressure/icon/presure_icon.bin | 0x03C8E600 | 3340 | 压力 App 小图标 | +| 64 | health | pressure/icon/press_grade1.bin | 0x03E62A00 | 2060 | 压力等级徽标 1 | +| 65 | health | pressure/icon/press_grade2.bin | 0x03E62000 | 2060 | 压力等级徽标 2 | +| 66 | health | pressure/icon/press_grade3.bin | 0x03E61600 | 2060 | 压力等级徽标 3 | +| 67 | health | pressure/icon/press_grade4.bin | 0x03E60C00 | 2060 | 压力等级徽标 4 | +| 68 | health | pressure/icon/pressure_eomji_bg.bin | 0x03DC1E00 | 54636 | 表情底版 | +| 69 | health | pressure/icon/pressure_eomji_widbg.bin | 0x03DB9200 | 35628 | 表情底版(小) | +| 70 | app | control_center/icon/settings.bin | 0x0557CC00 | 17420 | 控制中心磁贴 | +| 71 | app | control_center/icon/raise_bright_open.bin | 0x05581200 | 17420 | 控制中心磁贴 | +| 72 | app | control_center/icon/raise_bright_close.bin | 0x05589E00 | 17420 | 控制中心磁贴 | +| 73 | app | control_center/icon/not_disturb_open.bin | 0x05592A00 | 17420 | 控制中心磁贴 | +| 74 | app | control_center/icon/not_disturb_close.bin | 0x0559B600 | 17420 | 控制中心磁贴 | +| 75 | app | control_center/icon/mute_open.bin | 0x055A1800 | 17420 | 控制中心磁贴 | +| 76 | app | control_center/icon/mute_close.bin | 0x055AA400 | 17420 | 控制中心磁贴 | +| 77 | app | control_center/icon/mobile_network_open.bin | 0x055AEA00 | 17420 | 控制中心磁贴 | +| 78 | app | control_center/icon/mobile_network_close.bin | 0x055C0200 | 17420 | 控制中心磁贴 | +| 79 | app | control_center/icon/game_mode_on.bin | 0x055D2800 | 17420 | 控制中心磁贴 | +| 80 | app | control_center/icon/game_mode_off.bin | 0x055DB400 | 17420 | 控制中心磁贴 | +| 81 | app | control_center/icon/flashlight.bin | 0x055E4000 | 17420 | 控制中心磁贴 | +| 82 | app | control_center/icon/find_phone_close.bin | 0x055F1200 | 17420 | 控制中心磁贴 | +| 83 | app | control_center/icon/earphone.bin | 0x055F6600 | 3340 | 控制中心磁贴 | +| 84 | app | control_center/icon/drain_close.bin | 0x05604600 | 17420 | 控制中心磁贴 | +| 85 | app | control_center/icon/cancel.bin | 0x05609A00 | 3340 | 控制中心磁贴 | +| 86 | app | control_center/icon/bt_connect.bin | 0x0560C400 | 3340 | 控制中心磁贴 | +| 87 | app | control_center/icon/battery_open.bin | 0x0560D200 | 17420 | 控制中心磁贴 | +| 88 | app | control_center/icon/battery_close.bin | 0x05615E00 | 17420 | 控制中心磁贴 | +| 89 | app | control_center/icon/air_mode_open.bin | 0x0561A400 | 17420 | 控制中心磁贴 | +| 90 | app | control_center/icon/air_mode_close.bin | 0x0562CA00 | 17420 | 控制中心磁贴 | +| 91 | app | control_center/icon/add.bin | 0x05631000 | 3340 | 控制中心磁贴 | +> 偏移都 512 对齐、长度都是 4 的倍数 → 设备侧 `dd bs=4` 正好写满目标区间,不会多写一个字节到相邻文件里。 + +--- + +## 5. 表盘是怎么写的(机制) + +Lua 表盘不能设置图标;系统图标住在 ROMFS 分区镜像里,必须**写分区 + 重启**才生效。表盘只做运载 + 执行: + +``` +每条负载三步(只用官方存在的 dd,判据全看内容、不看返回码): + 1 版本校验:dd 读出设备上该偏移处 64 字节 -> 与「原厂字节」比对 + 不一致就判为「版本不符」,该分区剩余记录全部跳过(不写坏) + 2 写入 :dd if=/tmp/s5e_wr.bin of=<分区> bs=4 seek=<偏移/4> count=<长度/4> + 3 回读校验:dd 读回整段 82,956 字节 -> 与负载逐字节比对 +回滚 = 同一套流程,只是写入换成原厂字节、版本校验改比对替换后的字节 +最后 sync + reboot(ROMFS 是只读挂载 + 有页缓存,不重启看不到新图标) +``` + +**为什么能等长原地替换**:ROMFS 把 inode、名字、数据首尾相接顺序排布,绝对偏移全靠前面所有内容的长度决定。所有替换都是等长覆盖,所以 inode 表 / superblock / 目录项 / 后续文件偏移全部不变(本机逐位验证)。 +但偏移**锁固件版本**,所以第 1 步的版本校验是必须的:别的版本会全部「版本不符」跳过,而不是把图标写进别人的文件里。 + +表盘里的偏移是**构建时两遍测量回填**的:先编一遍,在 .face 里搜负载魔数定位,量出真实偏移写回 Lua,再编第二遍并逐条校验 0 漂移。 + +--- + +## 6. 本机验证结果(全绿) + +| 验证项 | 结果 | +|---|---| +| 负载回放 == 改后镜像(`vela_app_os4.bin` / `vela_health_os4.bin`) | 逐字节相同 | +| 回滚回放 == 原厂镜像 | 逐字节相同(sha256 一致) | +| inode 表 / superblock / 条目数 | 逐位不变(3432 / 5555 条) | +| 变化的文件 | 恰好只有被改的 `launcher.bin`(app 29、health 10 有实际差异) | +| UI 素材、负载、回滚在最终 .face 里的偏移 | 从 .face 里的 Lua 表读出来重新校验,逐条字节一致(`tools/s5e_verify.py`,独立于构建脚本) | +| 两遍构建偏移漂移 | 0 | +| 480 页面圆屏切边 | 真实渲染像素测量,7 个页面全部 0 越界 | +| Lua 语法块平衡 | balanced | +| 预览图尺寸 | 326×326(Compiler.exe 对 462 设备强制,实测) | + +--- + +## 7. 「环境预检查」通过意味着什么(**重要**) + +预检查是**只读**的,所以它是**必要但不充分**条件: + +| 预检查能证明 | 预检查证明不了 | +|---|---| +| `/dev/app`、`/dev/health` 两个节点存在且**可读** | **可写** —— 只有真正写第一条时才知道(这一步一个字节都不写) | +| 分区头是 `-rom1fs-`、卷名是 `app` / `health`、报出分区大小 | 表盘宿主的字体/界面能力 | +| 每分区抽 1 条对版:显示 `app 原厂✓` 或 `app 已替换✓` | 其余 46 条的偏移(这些在写入时逐条对版) | +| 负载表全部 4 对齐、不越界 | | + +**为什么证明不了「可写」也敢点**:写入路径自带三重保护 —— + +1. 每条写之前先读设备上 64 字节**对版**:等于「原厂字节」或「替换后字节」任一才动手;两者都不是就判「版本不符」,**该分区剩余记录全部跳过**,绝不往别人的文件里写 +2. 写完整段 82,956 字节**回读比对**,不一致就计失败 +3. 只做**等长原地覆盖**,不碰 inode 表 / superblock / 目录项 —— 最坏结果是「图标没变」,不会把分区写坏 + +**判断成功的唯一标准**:跑完显示 `> 成功 91 / 失败 0(共 91)` 和 `> app 63/63 health 28/28`。**不是 0 就先别重启**,点「回滚修改」再排查。 + +> 因为对版接受两种状态,**重复点「开始替换」或断电后半途重来都是安全的**(已经写入新图标的那部分不会被判成「版本不符」)。 + +--- + +## 8. 还没验证的(真机只能你来测) + +1. **表盘宿主进程有没有权限写 `/dev/app` 和 `/dev/health`** + S4 eSIM 上写 `/dev/app` 是实测通过的;这台同源平台但没测过。**请先点「环境预检查」**(只读,不写任何字节)。 +2. **`lvgl.Font('MiSans-Medium' / 'MiSans-Semibold', size)` 在这台上的表现** + `vela_font.bin` 里确实有 `MiSans-Semibold.ttf` / `MiSans-Medium.ttf`,S4 上这个 API 可用。界面文字依赖它;若不可用,文字会退回主题默认字体(中文可能缺字)。 +3. **`/dev/health` 的写权限**。如果它不可写,app 的 33 条仍会成功,日志里 health 会全失败。 +4. **安装通道**:和 `S5Icons.face` 一样走你的表盘安装方式(Compiler.exe 内部用的是 push 到 `/data/app/watchface/market//resource.bin`)。 + +输出怎么读(「正在替换」页): + +- 每 10 条一行 `> n/91 成 x 失 y` +- 结束:`> 成功 x / 失败 y(共 91)`,必要时 `> 版本不符跳过 n 条`,以及 `> app a/63 health b/28` +- **失败 0** 表示 91 条都写入并回读校验通过 → 点「重启手表」 + +--- + +## 9. 想改的话 + +```powershell +$py = "python" # 本机 Python 3 + Pillow + +# 只改界面/坐标:改 main.lua 与 tools/s5e_pages.py,然后 +& $py tools\\s5e_render_ui.py # 渲染 480 预览 +& $py tools\\s5e_check_clip.py # 圆屏切边检查 +& $py tools\\s5e_build.py # 两遍构建 -> dist\\S5eIcons.face +& $py tools\\s5e_verify.py # 独立校验 + +# 换图标来源 / 换固件版本 +& $py tools\\s5e_payload.py # 重新生成负载 + 整分区镜像(自带证明) +& $py tools\\s5e_ui_images.py # 重新生成 UI 素材(改了素材时) +& $py tools\\s5e_build.py +``` + +> `.work/`、`.work/` 是产物,别手改;`main.lua` 才是源码。 \ No newline at end of file diff --git a/s5e-icons/main.lua b/s5e-icons/main.lua new file mode 100644 index 0000000..6c482be --- /dev/null +++ b/s5e-icons/main.lua @@ -0,0 +1,679 @@ +--[[---------------------------------------------------------------------------- + OS4 Icons (S5 eSIM) —— Xiaomi Watch S5 41mm(q63) 的 OS4 图标包 -> Xiaomi Watch S5 eSIM 46mm(p62lte) + + 目标固件:miwear.watch.p62lte_v3.112.035_full(480x480,Lua 5.4 宿主) + 写入目标:两个 ROMFS 分区 + /dev/app 33 条(vela_app.bin 卷名 app sb_size 103042192) + /dev/health 14 条(vela_health.bin 卷名 health sb_size 206924736) + 47 条负载每条都恰好等于原槽位长度(144x144 ARGB8888 = 82956 B), + 所以 ROMFS 的 inode 表 / 后续所有偏移一个字节都不动。 + + 界面按 480x480 设计(_tools/s5e_pages.py 本机真字体渲染 + 圆屏切边检查 0 越界)。 + + 真机已验证的底层能力(沿用 S5->S4 那个表盘): + * 宿主不调用 ui.init -> 加载期建界面 + * SCRIPT_PATH 为空 -> 用 debug.getinfo 反推目录 + * .face 不解包,资源仍在 resource.bin 里 -> 按偏移读出来用 + * os.execute / dd 可用;os.execute 的返回值不可靠 -> 一律用内容比对判成败 +------------------------------------------------------------------------------]] + +local lvgl = require("lvgl") + +local W, H = 480, 480 +local BLK = 4 + +-- 等长覆盖只保证「偏移」512 对齐;RLE 素材的长度常常不是 4 的倍数(如 1635 / 3047), +-- 那种记录必须退化成 bs=1(偏移本身对齐,所以 bs=1 也精确)。 +local function ddBlk(off, len) + if off % BLK == 0 and len % BLK == 0 then return BLK end + return 1 +end +local DEVS = { app = "/dev/app", health = "/dev/health" } +local DEVLIST = { "app", "health" } -- 顺序固定:app 先,health 后 +local TMPH = "/tmp/s5e_head.bin" -- 版本校验读出的 64 字节 +local TMPW = "/tmp/s5e_wr.bin" -- 待写入的负载(resource.bin 里抠出来的那一份) +local TMPB = "/tmp/s5e_back.bin" -- 写完之后回读出来的那一份 + +--------------------------------------------------------------------- 自身目录 +local SRC = "" +if debug and debug.getinfo then + local info = debug.getinfo(1, "S") + SRC = (info and info.source) or "" +end +if type(SRC) ~= "string" then SRC = "" end +if SRC:sub(1, 1) == "@" then SRC = SRC:sub(2) end +SRC = SRC:gsub("\\", "/") +local THIS_DIR = SRC:match("^(.*)/[^/]+$") or "" +local SP = THIS_DIR ~= "" and (THIS_DIR .. "/") or "" +local PARENT = SP:gsub("[^/]+/$", "") + +local RES = nil +do + local c = { PARENT .. "resource.bin", SP .. "../resource.bin", SP .. "resource.bin" } + for i = 1, #c do + local f = io.open(c[i], "rb") + if f then f:close(); RES = c[i]; break end + end +end + +--------------------------------------------------------------------- 资源偏移 +-- UI 图片 { resource.bin 内偏移, 长度, 名字 } —— 由 _tools/s5e_build.py 回填 +local UIIMG = { +__UIIMG__ +} + +-- 负载表:{ 设备, 分区内偏移, 长度, resource.bin 内偏移 } —— 同样由脚本回填 +local PAYTBL = { +__PAY__ +__RB__ +} + +local function prepImages() + local paths = {} + if not RES then return paths end + local f = io.open(RES, "rb") + if not f then return paths end + for i = 1, #UIIMG do + local e = UIIMG[i] + f:seek("set", e[1]) + local d = f:read(e[2]) + if d and #d == e[2] then + local p = "/tmp/s5e_ui_" .. e[3] .. ".bin" + local o = io.open(p, "wb") + if o then o:write(d); o:close(); paths[e[3]] = p end + end + end + f:close() + return paths +end + +local IMG = prepImages() + +--------------------------------------------------------------------- 颜色与字体 +local C_GRAY, C_WHITE = '#999999', '#ffffff' +local C_OK, C_ERR = '#43cf7c', '#d43030' +local C_PLATE, C_CIRC = 0x002927, 0x4f4f4f + +local function getFont(name, size) + local f = nil + pcall(function() f = lvgl.Font(name, size) end) + return f +end + +local F_CLOCK = getFont('MiSans-Semibold', 29) +local F_TITLE = getFont('MiSans-Medium', 31) +local F_ITEM = getFont('MiSans-Medium', 29) +local F_BODY = getFont('MiSans-Medium', 25) +local F_VER = getFont('MiSans-Medium', 21) + +--------------------------------------------------------------------- 按下高亮 +local HL = 0x005149 +local pressed = nil +local tickCount = 0 + +local function setBg(obj, c) + pcall(function() obj:set { bg_color = c } end) +end + +local function pressRelease() + if pressed then + setBg(pressed.obj, C_PLATE) + pressed = nil + end +end + +--------------------------------------------------------------------- 基础 +local ui = {} +local built = false +local pages = {} +local clocks = {} +local job = { run = false, list = nil, i = 0, ok = 0, errs = 0, skipped = 0, + variant = 'replace', bad = {}, done = {}, total = {} } +local pendingVariant = 'replace' +local confirmFrom = 'home' + +local function newPage() + local p = nil + pcall(function() + p = lvgl.Object(nil, { x = 0, y = 0, w = W, h = H, bg_color = 0x000000, + bg_opa = 255, border_width = 0, pad_all = 0 }) + p:clear_flag(lvgl.FLAG.SCROLLABLE) + p:add_flag(lvgl.FLAG.EVENT_BUBBLE) + end) + return p +end + +local function show(p) + if not p then return end + pressRelease() + for k, v in pairs(pages) do + pcall(function() v:add_flag(lvgl.FLAG.HIDDEN) end) + end + pcall(function() p:clear_flag(lvgl.FLAG.HIDDEN) end) +end + +local function bind(obj, fn) + if not obj then return end + local ev = lvgl.EVENT + if ev and ev.SHORT_CLICKED then + if pcall(function() obj:onevent(ev.SHORT_CLICKED, function() fn() end) end) then return end + end + if pcall(function() obj:onClicked(function() fn() end) end) then return end + pcall(function() obj:onevent(ev and ev.CLICKED, fn) end) +end + +local function bindTap(obj, onTap) + if not obj then return end + local ok = pcall(function() + obj:onPressed(function() + setBg(obj, HL) + pressed = { obj = obj, t = tickCount } + end) + obj:onClicked(function() + pressRelease() + if onTap then onTap() end + end) + end) + if not ok then bind(obj, onTap) end +end + +local function setClickable(o) + if not o then return end + pcall(function() o:add_flag(lvgl.FLAG.CLICKABLE) end) +end + +local function lab(parent, o) + o.text_font = o.text_font or F_BODY + local l = nil + pcall(function() l = parent:Label(o) end) + return l +end + +local function clockLabel(parent) + -- 居中不用 text_align(它要的是 lv_text_align_t;传 ALIGN.CENTER 会越界被忽略) + -- 改为按真字体量出的宽度手工算 x + local l = lab(parent, { x = 201, y = 4, w = 78, h = 46, text = '--:--', + text_color = C_GRAY, font_size = 29, text_font = F_CLOCK }) + clocks[#clocks + 1] = l + return l +end + +local function pageTitle(parent, text, x, w) + return lab(parent, { x = x, y = 36, w = w, h = 46, text = text, + text_color = C_WHITE, font_size = 31, text_font = F_TITLE }) +end + +local function backArrow(parent, x, onTap) + local im = nil + if IMG.back then + pcall(function() im = parent:Image { src = IMG.back, x = x or 167, y = 44 } end) + setClickable(im) + bind(im, onTap) + end + return im +end + +-- 列表项:底板(整块可点) + 圆形遮罩 + 图标 + 标题(pad_all=0 必须,否则子元素右下偏移) +local function listItem(parent, y, iconkey, title, onTap) + local plate = nil + pcall(function() + plate = parent:Object { x = 50, y = y, w = 379, h = 91, pad_all = 0, + bg_color = C_PLATE, border_width = 0, radius = 35 } + end) + if plate then + if onTap then + setClickable(plate) + bindTap(plate, onTap) + end + pcall(function() + plate:Object { x = 21, y = 20, w = 52, h = 52, + bg_color = C_CIRC, border_width = 0, radius = 26 } + end) + if IMG[iconkey] then + pcall(function() plate:Image { src = IMG[iconkey], x = 31, y = 30 } end) + end + lab(plate, { x = 82, y = 26, w = 214, h = 40, text = title, + text_color = C_WHITE, font_size = 29, text_font = F_ITEM }) + end + return plate +end + +local function bigButton(parent, text) + local b = nil + pcall(function() + b = parent:Object { x = 111, y = 376, w = 258, h = 82, pad_all = 0, + bg_color = C_PLATE, border_width = 0, radius = 41 } + end) + if b then setClickable(b) end + -- "确认替换"/"重启手表" 29px 量得 116px -> 居中 x = 111 + (258-116)/2 = 182 + lab(b, { x = 71, y = 22, w = 126, h = 40, text = text, + text_color = C_WHITE, font_size = 29, text_font = F_ITEM }) + return b +end + +local function clockTick() + local t = '--:--' + if os and type(os.date) == "function" then + local ok, s = pcall(os.date, "%H:%M") + if ok and type(s) == "string" then t = s end + end + for i = 1, #clocks do + pcall(function() clocks[i]:set { text = t } end) + end +end + +--------------------------------------------------------------------- 外壳 +local function slurp(p, n) + local f = io.open(p, "rb") + if not f then return nil end + local d = f:read(n or "*a") + f:close() + return d +end + +local function sh(cmd) + if type(os.execute) ~= "function" then return -1 end + local a, b, c = os.execute(cmd) + if type(a) == "number" then return a end + if a == true then return 0 end + if type(c) == "number" then return c end + if type(b) == "number" then return b end + return -1 +end + +-- 从 resource.bin 里取一段(负载 / 原始字节) +local function slice(off, len) + if not RES then return nil end + local f = io.open(RES, "rb") + if not f then return nil end + f:seek("set", off) + local d = f:read(len) + f:close() + return d +end + +-- 读设备上某偏移处的 head 字节(不写,只读) +local function readBack(dev, off, len, tmp, bsz) + if os.remove then pcall(os.remove, tmp) end + sh(string.format("dd if=%s of=%s bs=%d skip=%d count=%d", + DEVS[dev], tmp, bsz or 1, bsz and (off // bsz) or off, + bsz and (len // bsz) or len)) + return slurp(tmp, len) +end + +--------------------------------------------------------------------- 预检查 +local desc1 = nil + +local function setDesc1(txt, color) + if not desc1 then return end + pcall(function() desc1:set { text = txt, text_color = color } end) +end + +local function doCheck() + if not RES then + setDesc1('找不到 resource.bin,无法操作。', C_ERR) + return false + end + local list = PAYTBL.pay or {} + if #list == 0 then + setDesc1('预检查失败:负载表为空。', C_ERR) + return false + end + local msg = {} + for i = 1, #DEVLIST do + local d = DEVLIST[i] + local h = readBack(d, 0, 48, TMPH, 1) + if not h or #h < 48 then + msg[#msg + 1] = d .. ' 读不到' + elseif h:sub(1, 8) ~= "-rom1fs-" then + msg[#msg + 1] = d .. ' 头不是 ROMFS' + else + local z = h:find("\0", 17, true) + local vol = h:sub(17, (z or 33) - 1) + local size = string.unpack(" 0 then + setDesc1(head .. string.format('\n%d 项未对齐!', bad), C_ERR) + return false + end + local okAll = true + for i = 1, #msg do + if msg[i]:find('读不到') or msg[i]:find('不是 ROMFS') or msg[i]:find('卷名') then okAll = false end + end + -- 抽样版本校验:每个分区取第一条负载,比对设备上真实的 64 字节与原厂字节。 + -- 这一步能确认"负载偏移是给这一版固件算的",而且只读、不写任何字节。 + for i = 1, #DEVLIST do + local d = DEVLIST[i] + local idx = nil + for k = 1, #list do + if list[k][1] == d then idx = k break end + end + if idx then + local o = PAYTBL.rb[idx] + local p = PAYTBL.pay[idx] + local want = o and slice(o[4], 64) or nil + local new = p and slice(p[4], 64) or nil + local got = o and readBack(d, o[2], 64, TMPH, 1) or nil + if want and got and #got >= 64 and got == want then + msg[#msg + 1] = d .. ' 原厂✓' + elseif new and got and #got >= 64 and got == new then + msg[#msg + 1] = d .. ' 已替换✓' + else + msg[#msg + 1] = d .. ' 抽样×' + okAll = false + end + end + end + setDesc1(head .. '\n' .. table.concat(msg, ' / '), okAll and C_OK or C_ERR) + return okAll +end + +--------------------------------------------------------------------- 替换 / 回滚 / 校验 +local logLabel = nil +local rebootBtn = nil +local runningTitle = nil +local logLines = {} + +local function logOut(s) + logLines[#logLines + 1] = s + while #logLines > 8 do table.remove(logLines, 1) end + if logLabel then + pcall(function() logLabel:set { text = table.concat(logLines, "\n") } end) + end +end + +local function showReboot() + if rebootBtn then pcall(function() rebootBtn:clear_flag(lvgl.FLAG.HIDDEN) end) end +end + +-- 一条记录的动作(三条 dd:版本校验读 / 写 / 回读): +-- mode='write' : 先确认设备上现有 64B 等于原厂字节(版本对得上),写入新图标,整段回读比对 +-- mode='rollback' : 反向 —— 先确认现有 64B 等于替换后的字节,再写回原厂字节并回读比对 +-- 判据一律是内容比对,不看 os.execute 的返回值(这台机器上它不可靠) +local function runOne(i, mode) + local srcname = (mode == 'rollback') and 'rb' or 'pay' + local wantname = (mode == 'rollback') and 'pay' or 'rb' + local rec = PAYTBL[srcname][i] + local want = PAYTBL[wantname][i] + if not rec or not want then return 'err' end + local dev = rec[1] + -- 现状认定:等于「原厂字节」(want)或等于「替换后字节」(rec)都算对版。 + -- 这样重复点替换、或断电后半途重来都不会被判成「版本不符」而卡住。 + local headW = slice(want[4], 64) + local headO = slice(rec[4], 64) + if not headW or not headO or #headW < 64 or #headO < 64 then return 'err' end + local cur = readBack(dev, rec[2], 64, TMPH, 1) + if not cur or #cur < 64 then return 'err' end + if cur ~= headW and cur ~= headO then return 'ver' end + local blob = slice(rec[4], rec[3]) + if not blob or #blob ~= rec[3] then return 'err' end + -- 先把负载落到 /tmp(resource.bin 内的偏移不保证 4 对齐,所以不能用 dd 直接 skip) + local wf = io.open(TMPW, "wb") + if not wf then return 'err' end + wf:write(blob) + wf:close() + local b = ddBlk(rec[2], rec[3]) + sh(string.format("dd if=%s of=%s bs=%d seek=%d count=%d", + TMPW, DEVS[dev], b, rec[2] // b, rec[3] // b)) + local back = readBack(dev, rec[2], rec[3], TMPB, b) + if not back or #back ~= rec[3] or back ~= blob then return 'err' end + return 'ok' +end + +local function startJob(variant) + if job.run then return end + local pay = PAYTBL.pay or {} + if #pay == 0 then + setDesc1('负载表为空,已中止。', C_ERR) + return + end + job.run, job.i, job.ok, job.errs, job.skipped = true, 0, 0, 0, 0 + job.variant, job.bad, job.done, job.total = variant, {}, {}, {} + job.list = pay + for k = 1, #pay do + local d = pay[k][1] + job.total[d] = (job.total[d] or 0) + 1 + end + logLines = {} + logOut((variant == 'rollback') and '> 开始回滚' or '> 开始替换') + local _devs = {} + for d in pairs(job.total) do _devs[#_devs + 1] = d end + table.sort(_devs) + local _parts = {} + for i = 1, #_devs do + _parts[#_parts + 1] = string.format('%s %d 条', _devs[i], job.total[_devs[i]]) + end + logOut('> ' .. table.concat(_parts, ' / ')) + logOut('> 读取 resource.bin') + if rebootBtn then pcall(function() rebootBtn:add_flag(lvgl.FLAG.HIDDEN) end) end + if runningTitle then + pcall(function() + runningTitle:set { text = (variant == 'rollback') and '正在回滚' or '正在替换' } + end) + end + show(pages.running) +end + +local function tick() + tickCount = tickCount + 1 + if pressed and (tickCount - pressed.t) > 40 then pressRelease() end + if not job.run or not job.list then return end + job.i = job.i + 1 + local n = #job.list + if job.i > n then + job.run = false + logOut((job.variant == 'rollback') and '> 回滚完毕,请重启手表。' + or '> 替换完毕,请重启手表。') + logOut(string.format('> 成功 %d / 失败 %d(共 %d)', job.ok, job.errs, n)) + if job.skipped > 0 then + logOut(string.format('> 版本不符跳过 %d 条', job.skipped)) + end + logOut(string.format('> app %d/%d health %d/%d', + job.done.app or 0, job.total.app or 0, + job.done.health or 0, job.total.health or 0)) + showReboot() + return + end + local dev = job.list[job.i][1] + if job.bad[dev] then + job.skipped = job.skipped + 1 + job.errs = job.errs + 1 + else + local r = runOne(job.i, (job.variant == 'rollback') and 'rollback' or 'write') + if r == 'ok' then + job.ok = job.ok + 1 + job.done[dev] = (job.done[dev] or 0) + 1 + elseif r == 'ver' then + job.errs = job.errs + 1 + job.skipped = job.skipped + 1 + job.bad[dev] = true + logOut('> ' .. dev .. ' 版本不符,已跳过') + else + job.errs = job.errs + 1 + end + end + if job.i % 10 == 0 or job.i == n then + logOut(string.format('> %d/%d 成 %d 失 %d', job.i, n, job.ok, job.errs)) + end +end + +--------------------------------------------------------------------- 页面 +local function buildHome() + local p = newPage() + if not p then return nil end + clockLabel(p) + pageTitle(p, 'OS4 Icons', 164, 152) + listItem(p, 88, 'ic_help', '快速帮助', function() show(pages.help) end) + listItem(p, 186, 'ic_replace', '开始替换', function() + pendingVariant = 'replace' + confirmFrom = 'home' + show(pages.confirm) + end) + listItem(p, 285, 'ic_about', '关于', function() show(pages.about) end) + return p +end + +local function buildHelp() + local p = newPage() + if not p then return nil end + clockLabel(p) + backArrow(p, 167, function() show(pages.home) end) + local t = pageTitle(p, '快速帮助', 188, 124) + setClickable(t) + bind(t, function() show(pages.home) end) + + listItem(p, 88, 'ic_check', '环境预检查', function() doCheck() end) + desc1 = lab(p, { x = 67, y = 182, w = 348, h = 76, + text = '检查 app / health 两个分区\n与 47 条负载的完整性。', + text_color = C_GRAY, font_size = 25, text_font = F_BODY }) + + listItem(p, 264, 'ic_rollback', '回滚修改', function() + pendingVariant = 'rollback' + confirmFrom = 'help' + show(pages.confirm) + end) + lab(p, { x = 67, y = 355, w = 348, h = 36, + text = '恢复 47 个图标的原始样式。', + text_color = C_GRAY, font_size = 25, text_font = F_BODY }) + return p +end + +local function buildConfirm() + local p = newPage() + if not p then return nil end + clockLabel(p) + local function goBack() show(pages[confirmFrom] or pages.home) end + backArrow(p, 167, goBack) + local t = pageTitle(p, '开始替换', 188, 124) + setClickable(t) + bind(t, goBack) + + -- 多色文本不能用 recolor -> 拆成多个标签 + 手工算 x(宽度用真 MiSans 25px 量出) + lab(p, { x = 60, y = 93, w = 360, h = 36, + text = '替换图标包之前,请先确认自己', text_color = C_GRAY, + font_size = 25, text_font = F_BODY }) + lab(p, { x = 60, y = 126, w = 360, h = 36, + text = '的系统版本号为 ', text_color = C_GRAY, font_size = 25, text_font = F_BODY }) + lab(p, { x = 244, y = 126, w = 120, h = 36, text = '3.112.035', + text_color = C_WHITE, font_size = 25, text_font = F_BODY }) + lab(p, { x = 356, y = 126, w = 40, h = 36, text = '!', + text_color = C_GRAY, font_size = 25, text_font = F_BODY }) + lab(p, { x = 60, y = 196, w = 360, h = 36, text = '请确认自己的手表型号为', + text_color = C_GRAY, font_size = 25, text_font = F_BODY }) + lab(p, { x = 60, y = 229, w = 372, h = 36, text = 'Xiaomi Watch S5 eSIM 46mm', + text_color = C_WHITE, font_size = 25, text_font = F_BODY }) + lab(p, { x = 427, y = 229, w = 40, h = 36, text = '!', + text_color = C_GRAY, font_size = 25, text_font = F_BODY }) + lab(p, { x = 60, y = 331, w = 360, h = 36, text = '搞机有风险,变砖后果自负。', + text_color = C_GRAY, font_size = 25, text_font = F_BODY }) + + local b = bigButton(p, '确认替换') + bindTap(b, function() startJob(pendingVariant) end) + return p +end + +local function buildAbout() + local p = newPage() + if not p then return nil end + clockLabel(p) + backArrow(p, 198, function() show(pages.home) end) + local t = pageTitle(p, '关于', 220, 62) + setClickable(t) + bind(t, function() show(pages.home) end) + + pcall(function() + p:Object { x = 54, y = 81, w = 373, h = 210, pad_all = 0, + bg_color = C_PLATE, border_width = 0, radius = 31 } + end) + if IMG.logo then + pcall(function() p:Image { src = IMG.logo, x = 200, y = 111 } end) + end + lab(p, { x = 170, y = 198, w = 141, h = 40, text = 'OS4 Icons', + text_color = C_WHITE, font_size = 29, text_font = F_ITEM }) + lab(p, { x = 216, y = 234, w = 49, h = 28, text = '1.0.0', + text_color = C_GRAY, font_size = 21, text_font = F_VER }) + + listItem(p, 300, 'ic_qq', '732681995', nil) + return p +end + +local function buildRunning() + local p = newPage() + if not p then return nil end + clockLabel(p) + runningTitle = pageTitle(p, '正在替换', 178, 124) + logLabel = lab(p, { x = 67, y = 93, w = 348, h = 277, text = '', + text_color = C_WHITE, font_size = 25, text_font = F_BODY }) + rebootBtn = bigButton(p, '重启手表') + if rebootBtn then pcall(function() rebootBtn:add_flag(lvgl.FLAG.HIDDEN) end) end + bindTap(rebootBtn, function() + logOut('> 正在重启…') + sh("sync") + sh("reboot") + end) + return p +end + +--------------------------------------------------------------------- 组装 +local function build() + if built then return end + built = true + pages.home = buildHome() + pages.help = buildHelp() + pages.confirm = buildConfirm() + pages.about = buildAbout() + pages.running = buildRunning() + if pages.home then show(pages.home) end + clockTick() + pcall(function() + ui.timer = lvgl.Timer { period = 40, repeat_count = -1, cb = function() tick() end } + if ui.timer and ui.timer.resume then ui.timer:resume() end + end) + pcall(function() + ui.clockTimer = lvgl.Timer { period = 1000, repeat_count = -1, + cb = function() clockTick() end } + if ui.clockTimer and ui.clockTimer.resume then ui.clockTimer:resume() end + end) +end + +local okbuild, errbuild = pcall(build) +if not okbuild then + pcall(function() + local r = lvgl.Object(nil, { x = 0, y = 0, w = W, h = H, bg_color = 0x000000, + bg_opa = 255, border_width = 0 }) + r:Label { x = 20, y = 20, w = W - 40, h = H - 40, + text = 'BUILD FAILED:\n' .. tostring(errbuild), + text_color = '#ffffff', font_size = 13, text_font = F_BODY } + end) +end + +ui.init = function() + pcall(build) + return ui.root +end + +function pageOnPause() + if ui.timer then pcall(function() ui.timer:pause() end) end + if ui.clockTimer then pcall(function() ui.clockTimer:pause() end) end +end + +function pageOnResume() + if ui.timer then pcall(function() ui.timer:resume() end) end + if ui.clockTimer then pcall(function() ui.clockTimer:resume() end) end +end + +return ui diff --git a/s5e-icons/payload/payload.lua b/s5e-icons/payload/payload.lua new file mode 100644 index 0000000..557f955 --- /dev/null +++ b/s5e-icons/payload/payload.lua @@ -0,0 +1,188 @@ +-- PAY: {设备, 分区内偏移, 长度, payload 文件内相对偏移} —— 由 _tools/s5e_payload.py 生成 +local PAY = { + { "app", 0x06226200, 82956, 0 }, + { "app", 0x05c23600, 82956, 82956 }, + { "app", 0x059d1a00, 82956, 165912 }, + { "app", 0x05802000, 82956, 248868 }, + { "app", 0x05546800, 82956, 331824 }, + { "app", 0x053e6e00, 82956, 414780 }, + { "app", 0x05374c00, 82956, 497736 }, + { "app", 0x0535ce00, 82956, 580692 }, + { "app", 0x05183c00, 82956, 663648 }, + { "app", 0x0516f600, 82956, 746604 }, + { "app", 0x0515b000, 82956, 829560 }, + { "app", 0x05146a00, 82956, 912516 }, + { "app", 0x04db9400, 82956, 995472 }, + { "app", 0x04d80a00, 82956, 1078428 }, + { "app", 0x04d6ba00, 82956, 1161384 }, + { "app", 0x04d57400, 82956, 1244340 }, + { "app", 0x04d42000, 82956, 1327296 }, + { "app", 0x04d2da00, 82956, 1410252 }, + { "app", 0x04cfee00, 82956, 1493208 }, + { "app", 0x04bc8600, 82956, 1576164 }, + { "app", 0x04900c00, 82956, 1659120 }, + { "app", 0x04469c00, 82956, 1742076 }, + { "app", 0x0433b200, 82956, 1825032 }, + { "app", 0x04391a00, 82956, 1907988 }, + { "app", 0x042b9400, 82956, 1990944 }, + { "app", 0x04088200, 82956, 2073900 }, + { "app", 0x02c41c00, 82956, 2156856 }, + { "app", 0x02949e00, 82956, 2239812 }, + { "app", 0x0291f200, 82956, 2322768 }, + { "app", 0x022df800, 82956, 2405724 }, + { "app", 0x021aa600, 82956, 2488680 }, + { "app", 0x01b09400, 82956, 2571636 }, + { "app", 0x0000bc00, 82956, 2654592 }, + { "health", 0x0c51b600, 82956, 2737548 }, + { "health", 0x0b910400, 82956, 2820504 }, + { "health", 0x08a2bc00, 82956, 2903460 }, + { "health", 0x07077e00, 82956, 2986416 }, + { "health", 0x06ebb200, 82956, 3069372 }, + { "health", 0x06558600, 82956, 3152328 }, + { "health", 0x0634aa00, 82956, 3235284 }, + { "health", 0x03b9d400, 82956, 3318240 }, + { "health", 0x0272f000, 82956, 3401196 }, + { "health", 0x01e89200, 82956, 3484152 }, + { "health", 0x02692600, 82956, 3567108 }, + { "health", 0x01b5fa00, 82956, 3650064 }, + { "health", 0x015a6400, 82956, 3733020 }, + { "health", 0x001e3a00, 82956, 3815976 }, + { "app", 0x02ab2200, 2060, 3898932 }, + { "app", 0x058dfc00, 3340, 3900992 }, + { "app", 0x058fcc00, 3340, 3904332 }, + { "app", 0x02d97c00, 5132, 3907672 }, + { "app", 0x04be3a00, 5132, 3912804 }, + { "app", 0x04be6e00, 5132, 3917936 }, + { "health", 0x03c85400, 3340, 3923068 }, + { "health", 0x03c89600, 3340, 3926408 }, + { "health", 0x03c8d800, 3340, 3929748 }, + { "health", 0x03c81200, 3340, 3933088 }, + { "health", 0x03c88c00, 2060, 3936428 }, + { "health", 0x03c8ce00, 2060, 3938488 }, + { "health", 0x03c80800, 2060, 3940548 }, + { "health", 0x03c8e600, 3340, 3942608 }, + { "health", 0x03e62a00, 2060, 3945948 }, + { "health", 0x03e62000, 2060, 3948008 }, + { "health", 0x03e61600, 2060, 3950068 }, + { "health", 0x03e60c00, 2060, 3952128 }, + { "health", 0x03dc1e00, 54636, 3954188 }, + { "health", 0x03db9200, 35628, 4008824 }, + { "app", 0x0557cc00, 17420, 4044452 }, + { "app", 0x05581200, 17420, 4061872 }, + { "app", 0x05589e00, 17420, 4079292 }, + { "app", 0x05592a00, 17420, 4096712 }, + { "app", 0x0559b600, 17420, 4114132 }, + { "app", 0x055a1800, 17420, 4131552 }, + { "app", 0x055aa400, 17420, 4148972 }, + { "app", 0x055aea00, 17420, 4166392 }, + { "app", 0x055c0200, 17420, 4183812 }, + { "app", 0x055d2800, 17420, 4201232 }, + { "app", 0x055db400, 17420, 4218652 }, + { "app", 0x055e4000, 17420, 4236072 }, + { "app", 0x055f1200, 17420, 4253492 }, + { "app", 0x055f6600, 3340, 4270912 }, + { "app", 0x05604600, 17420, 4274252 }, + { "app", 0x05609a00, 3340, 4291672 }, + { "app", 0x0560d200, 17420, 4295012 }, + { "app", 0x05615e00, 17420, 4312432 }, + { "app", 0x0561a400, 17420, 4329852 }, + { "app", 0x0562ca00, 17420, 4347272 }, + { "app", 0x05631000, 3340, 4364692 }, + { "app", 0x0559fc00, 3340, 4368032 }, + { "app", 0x05577800, 3340, 4371372 }, + { "app", 0x05576a00, 3340, 4374712 }, + { "app", 0x05575c00, 3340, 4378052 }, + { "app", 0x05574e00, 3340, 4381392 }, + { "app", 0x05574000, 3340, 4384732 }, + { "app", 0x05627600, 3340, 4388072 }, + { "app", 0x0560c400, 3340, 4391412 }, + { "app", 0x0560b600, 3340, 4394752 }, + { "app", 0x0560a800, 3340, 4398092 }, + { "app", 0x055c4800, 3340, 4401432 }, + { "app", 0x05573200, 3340, 4404772 }, + { "app", 0x05572400, 3340, 4408112 }, + { "app", 0x05571600, 3340, 4411452 }, + { "app", 0x05570800, 3340, 4414792 }, + { "app", 0x044c6600, 5260, 4418132 }, + { "app", 0x044be800, 32012, 4423392 }, + { "app", 0x044bda00, 3340, 4455404 }, + { "app", 0x044b9a00, 1900, 4458744 }, + { "app", 0x044b7800, 3052, 4460644 }, + { "app", 0x044b7200, 1292, 4463696 }, + { "app", 0x044b6600, 2092, 4464988 }, + { "app", 0x044b5800, 3340, 4467080 }, + { "app", 0x044b4a00, 3340, 4470420 }, + { "app", 0x044b2c00, 7436, 4473760 }, + { "app", 0x0438ec00, 3340, 4481196 }, + { "app", 0x0438e200, 2444, 4484536 }, + { "app", 0x0438d800, 2444, 4486980 }, + { "app", 0x0438ce00, 2444, 4489424 }, + { "app", 0x0438c400, 2444, 4491868 }, + { "app", 0x0438ba00, 2444, 4494312 }, + { "app", 0x0438b000, 2444, 4496756 }, + { "app", 0x0438a600, 2444, 4499200 }, + { "app", 0x04389c00, 2444, 4501644 }, + { "app", 0x04389200, 2444, 4504088 }, + { "app", 0x04388800, 2444, 4506532 }, + { "app", 0x04387e00, 2444, 4508976 }, + { "app", 0x04385c00, 3340, 4511420 }, + { "app", 0x04384e00, 3340, 4514760 }, + { "app", 0x04383800, 5132, 4518100 }, + { "app", 0x04381a00, 7436, 4523232 }, + { "app", 0x0437ee00, 5132, 4530668 }, + { "app", 0x04378600, 3340, 4535800 }, + { "app", 0x04377800, 3340, 4539140 }, + { "app", 0x0436ee00, 2060, 4542480 }, + { "app", 0x04357000, 10252, 4544540 }, + { "app", 0x04355200, 7436, 4554792 }, + { "app", 0x04353c00, 5132, 4562228 }, + { "app", 0x04352e00, 3340, 4567360 }, + { "app", 0x042dee00, 13580, 4570700 }, + { "app", 0x042db800, 13580, 4584280 }, + { "app", 0x042d8200, 13580, 4597860 }, + { "app", 0x042d6400, 7436, 4611440 }, + { "app", 0x042d4600, 7436, 4618876 }, + { "app", 0x042d3000, 5132, 4626312 }, + { "app", 0x042d1a00, 5132, 4631444 }, + { "app", 0x042d0400, 5132, 4636576 }, + { "app", 0x042aae00, 58636, 4641708 }, + { "app", 0x042aa000, 3340, 4700344 }, + { "app", 0x042a9200, 3340, 4703684 }, + { "app", 0x042a8400, 3340, 4707024 }, + { "app", 0x042a5a00, 10252, 4710364 }, + { "app", 0x0429f800, 3340, 4720616 }, + { "app", 0x0429ea00, 3340, 4723956 }, + { "app", 0x0429d400, 5132, 4727296 }, + { "app", 0x0429c600, 3340, 4732428 }, + { "app", 0x0429aa00, 3340, 4735768 }, + { "app", 0x04298e00, 3340, 4739108 }, + { "app", 0x04298000, 3340, 4742448 }, + { "app", 0x04296a00, 5132, 4745788 }, + { "app", 0x04294c00, 7436, 4750920 }, + { "app", 0x02a65c00, 58636, 4758356 }, + { "app", 0x02a60e00, 19436, 4816992 }, + { "app", 0x02a5e200, 3340, 4836428 }, + { "app", 0x02a5c600, 3340, 4839768 }, + { "app", 0x02a5b800, 3340, 4843108 }, + { "app", 0x029e1400, 3340, 4846448 }, + { "app", 0x029dd200, 16396, 4849788 }, + { "app", 0x029dc400, 3340, 4866184 }, + { "app", 0x029d7600, 19436, 4869524 }, + { "app", 0x029b8e00, 120844, 4888960 }, + { "app", 0x0299b400, 120844, 5009804 }, + { "app", 0x0295f200, 3340, 5130648 }, + { "app", 0x0295e400, 3340, 5133988 }, + { "app", 0x02949000, 3340, 5137328 }, + { "app", 0x02944e00, 16396, 5140668 }, + { "app", 0x02935600, 62988, 5157064 }, + { "app", 0x02933800, 7436, 5220052 }, + { "app", 0x0291e800, 2220, 5227488 }, + { "app", 0x0291de00, 2220, 5229708 }, + { "app", 0x02919800, 17420, 5231928 }, + { "app", 0x02917a00, 7436, 5249348 }, + { "app", 0x0290fc00, 32012, 5256784 }, + { "app", 0x02907e00, 32012, 5288796 }, + { "app", 0x02907000, 3340, 5320808 }, + { "app", 0x02905200, 7436, 5324148 }, + { "app", 0x02903400, 7436, 5331584 }, +} diff --git a/s5e-icons/payload_rollback/rollback.lua b/s5e-icons/payload_rollback/rollback.lua new file mode 100644 index 0000000..3eeeb98 --- /dev/null +++ b/s5e-icons/payload_rollback/rollback.lua @@ -0,0 +1,188 @@ +-- RB: {设备, 分区内偏移, 长度, payload 文件内相对偏移} —— 由 _tools/s5e_payload.py 生成 +local RB = { + { "app", 0x06226200, 82956, 0 }, + { "app", 0x05c23600, 82956, 82956 }, + { "app", 0x059d1a00, 82956, 165912 }, + { "app", 0x05802000, 82956, 248868 }, + { "app", 0x05546800, 82956, 331824 }, + { "app", 0x053e6e00, 82956, 414780 }, + { "app", 0x05374c00, 82956, 497736 }, + { "app", 0x0535ce00, 82956, 580692 }, + { "app", 0x05183c00, 82956, 663648 }, + { "app", 0x0516f600, 82956, 746604 }, + { "app", 0x0515b000, 82956, 829560 }, + { "app", 0x05146a00, 82956, 912516 }, + { "app", 0x04db9400, 82956, 995472 }, + { "app", 0x04d80a00, 82956, 1078428 }, + { "app", 0x04d6ba00, 82956, 1161384 }, + { "app", 0x04d57400, 82956, 1244340 }, + { "app", 0x04d42000, 82956, 1327296 }, + { "app", 0x04d2da00, 82956, 1410252 }, + { "app", 0x04cfee00, 82956, 1493208 }, + { "app", 0x04bc8600, 82956, 1576164 }, + { "app", 0x04900c00, 82956, 1659120 }, + { "app", 0x04469c00, 82956, 1742076 }, + { "app", 0x0433b200, 82956, 1825032 }, + { "app", 0x04391a00, 82956, 1907988 }, + { "app", 0x042b9400, 82956, 1990944 }, + { "app", 0x04088200, 82956, 2073900 }, + { "app", 0x02c41c00, 82956, 2156856 }, + { "app", 0x02949e00, 82956, 2239812 }, + { "app", 0x0291f200, 82956, 2322768 }, + { "app", 0x022df800, 82956, 2405724 }, + { "app", 0x021aa600, 82956, 2488680 }, + { "app", 0x01b09400, 82956, 2571636 }, + { "app", 0x0000bc00, 82956, 2654592 }, + { "health", 0x0c51b600, 82956, 2737548 }, + { "health", 0x0b910400, 82956, 2820504 }, + { "health", 0x08a2bc00, 82956, 2903460 }, + { "health", 0x07077e00, 82956, 2986416 }, + { "health", 0x06ebb200, 82956, 3069372 }, + { "health", 0x06558600, 82956, 3152328 }, + { "health", 0x0634aa00, 82956, 3235284 }, + { "health", 0x03b9d400, 82956, 3318240 }, + { "health", 0x0272f000, 82956, 3401196 }, + { "health", 0x01e89200, 82956, 3484152 }, + { "health", 0x02692600, 82956, 3567108 }, + { "health", 0x01b5fa00, 82956, 3650064 }, + { "health", 0x015a6400, 82956, 3733020 }, + { "health", 0x001e3a00, 82956, 3815976 }, + { "app", 0x02ab2200, 2060, 3898932 }, + { "app", 0x058dfc00, 3340, 3900992 }, + { "app", 0x058fcc00, 3340, 3904332 }, + { "app", 0x02d97c00, 5132, 3907672 }, + { "app", 0x04be3a00, 5132, 3912804 }, + { "app", 0x04be6e00, 5132, 3917936 }, + { "health", 0x03c85400, 3340, 3923068 }, + { "health", 0x03c89600, 3340, 3926408 }, + { "health", 0x03c8d800, 3340, 3929748 }, + { "health", 0x03c81200, 3340, 3933088 }, + { "health", 0x03c88c00, 2060, 3936428 }, + { "health", 0x03c8ce00, 2060, 3938488 }, + { "health", 0x03c80800, 2060, 3940548 }, + { "health", 0x03c8e600, 3340, 3942608 }, + { "health", 0x03e62a00, 2060, 3945948 }, + { "health", 0x03e62000, 2060, 3948008 }, + { "health", 0x03e61600, 2060, 3950068 }, + { "health", 0x03e60c00, 2060, 3952128 }, + { "health", 0x03dc1e00, 54636, 3954188 }, + { "health", 0x03db9200, 35628, 4008824 }, + { "app", 0x0557cc00, 17420, 4044452 }, + { "app", 0x05581200, 17420, 4061872 }, + { "app", 0x05589e00, 17420, 4079292 }, + { "app", 0x05592a00, 17420, 4096712 }, + { "app", 0x0559b600, 17420, 4114132 }, + { "app", 0x055a1800, 17420, 4131552 }, + { "app", 0x055aa400, 17420, 4148972 }, + { "app", 0x055aea00, 17420, 4166392 }, + { "app", 0x055c0200, 17420, 4183812 }, + { "app", 0x055d2800, 17420, 4201232 }, + { "app", 0x055db400, 17420, 4218652 }, + { "app", 0x055e4000, 17420, 4236072 }, + { "app", 0x055f1200, 17420, 4253492 }, + { "app", 0x055f6600, 3340, 4270912 }, + { "app", 0x05604600, 17420, 4274252 }, + { "app", 0x05609a00, 3340, 4291672 }, + { "app", 0x0560d200, 17420, 4295012 }, + { "app", 0x05615e00, 17420, 4312432 }, + { "app", 0x0561a400, 17420, 4329852 }, + { "app", 0x0562ca00, 17420, 4347272 }, + { "app", 0x05631000, 3340, 4364692 }, + { "app", 0x0559fc00, 3340, 4368032 }, + { "app", 0x05577800, 3340, 4371372 }, + { "app", 0x05576a00, 3340, 4374712 }, + { "app", 0x05575c00, 3340, 4378052 }, + { "app", 0x05574e00, 3340, 4381392 }, + { "app", 0x05574000, 3340, 4384732 }, + { "app", 0x05627600, 3340, 4388072 }, + { "app", 0x0560c400, 3340, 4391412 }, + { "app", 0x0560b600, 3340, 4394752 }, + { "app", 0x0560a800, 3340, 4398092 }, + { "app", 0x055c4800, 3340, 4401432 }, + { "app", 0x05573200, 3340, 4404772 }, + { "app", 0x05572400, 3340, 4408112 }, + { "app", 0x05571600, 3340, 4411452 }, + { "app", 0x05570800, 3340, 4414792 }, + { "app", 0x044c6600, 5260, 4418132 }, + { "app", 0x044be800, 32012, 4423392 }, + { "app", 0x044bda00, 3340, 4455404 }, + { "app", 0x044b9a00, 1900, 4458744 }, + { "app", 0x044b7800, 3052, 4460644 }, + { "app", 0x044b7200, 1292, 4463696 }, + { "app", 0x044b6600, 2092, 4464988 }, + { "app", 0x044b5800, 3340, 4467080 }, + { "app", 0x044b4a00, 3340, 4470420 }, + { "app", 0x044b2c00, 7436, 4473760 }, + { "app", 0x0438ec00, 3340, 4481196 }, + { "app", 0x0438e200, 2444, 4484536 }, + { "app", 0x0438d800, 2444, 4486980 }, + { "app", 0x0438ce00, 2444, 4489424 }, + { "app", 0x0438c400, 2444, 4491868 }, + { "app", 0x0438ba00, 2444, 4494312 }, + { "app", 0x0438b000, 2444, 4496756 }, + { "app", 0x0438a600, 2444, 4499200 }, + { "app", 0x04389c00, 2444, 4501644 }, + { "app", 0x04389200, 2444, 4504088 }, + { "app", 0x04388800, 2444, 4506532 }, + { "app", 0x04387e00, 2444, 4508976 }, + { "app", 0x04385c00, 3340, 4511420 }, + { "app", 0x04384e00, 3340, 4514760 }, + { "app", 0x04383800, 5132, 4518100 }, + { "app", 0x04381a00, 7436, 4523232 }, + { "app", 0x0437ee00, 5132, 4530668 }, + { "app", 0x04378600, 3340, 4535800 }, + { "app", 0x04377800, 3340, 4539140 }, + { "app", 0x0436ee00, 2060, 4542480 }, + { "app", 0x04357000, 10252, 4544540 }, + { "app", 0x04355200, 7436, 4554792 }, + { "app", 0x04353c00, 5132, 4562228 }, + { "app", 0x04352e00, 3340, 4567360 }, + { "app", 0x042dee00, 13580, 4570700 }, + { "app", 0x042db800, 13580, 4584280 }, + { "app", 0x042d8200, 13580, 4597860 }, + { "app", 0x042d6400, 7436, 4611440 }, + { "app", 0x042d4600, 7436, 4618876 }, + { "app", 0x042d3000, 5132, 4626312 }, + { "app", 0x042d1a00, 5132, 4631444 }, + { "app", 0x042d0400, 5132, 4636576 }, + { "app", 0x042aae00, 58636, 4641708 }, + { "app", 0x042aa000, 3340, 4700344 }, + { "app", 0x042a9200, 3340, 4703684 }, + { "app", 0x042a8400, 3340, 4707024 }, + { "app", 0x042a5a00, 10252, 4710364 }, + { "app", 0x0429f800, 3340, 4720616 }, + { "app", 0x0429ea00, 3340, 4723956 }, + { "app", 0x0429d400, 5132, 4727296 }, + { "app", 0x0429c600, 3340, 4732428 }, + { "app", 0x0429aa00, 3340, 4735768 }, + { "app", 0x04298e00, 3340, 4739108 }, + { "app", 0x04298000, 3340, 4742448 }, + { "app", 0x04296a00, 5132, 4745788 }, + { "app", 0x04294c00, 7436, 4750920 }, + { "app", 0x02a65c00, 58636, 4758356 }, + { "app", 0x02a60e00, 19436, 4816992 }, + { "app", 0x02a5e200, 3340, 4836428 }, + { "app", 0x02a5c600, 3340, 4839768 }, + { "app", 0x02a5b800, 3340, 4843108 }, + { "app", 0x029e1400, 3340, 4846448 }, + { "app", 0x029dd200, 16396, 4849788 }, + { "app", 0x029dc400, 3340, 4866184 }, + { "app", 0x029d7600, 19436, 4869524 }, + { "app", 0x029b8e00, 120844, 4888960 }, + { "app", 0x0299b400, 120844, 5009804 }, + { "app", 0x0295f200, 3340, 5130648 }, + { "app", 0x0295e400, 3340, 5133988 }, + { "app", 0x02949000, 3340, 5137328 }, + { "app", 0x02944e00, 16396, 5140668 }, + { "app", 0x02935600, 62988, 5157064 }, + { "app", 0x02933800, 7436, 5220052 }, + { "app", 0x0291e800, 2220, 5227488 }, + { "app", 0x0291de00, 2220, 5229708 }, + { "app", 0x02919800, 17420, 5231928 }, + { "app", 0x02917a00, 7436, 5249348 }, + { "app", 0x0290fc00, 32012, 5256784 }, + { "app", 0x02907e00, 32012, 5288796 }, + { "app", 0x02907000, 3340, 5320808 }, + { "app", 0x02905200, 7436, 5324148 }, + { "app", 0x02903400, 7436, 5331584 }, +} diff --git a/s5e-icons/tools/iconpack.py b/s5e-icons/tools/iconpack.py new file mode 100644 index 0000000..4012c6e --- /dev/null +++ b/s5e-icons/tools/iconpack.py @@ -0,0 +1,447 @@ +"""Build / apply a system-app launcher-icon replacement for a Vela `vela_app.bin`. + +Why "same length" is the whole ball game +---------------------------------------- +ROMFS packs superblock -> inode -> name -> file data strictly back to back, and +every `next` / `spec` pointer is an ABSOLUTE offset. If a replacement file +changes length by even one byte, every later offset shifts and the image is +corrupt. So a replacement must encode to EXACTLY the original byte length: + + * source PNG == original dimensions -> encode, lengths match, done. + * source PNG != original dimensions -> two options: + (default) resize to the original dimensions (icon renders identically) + --pad keep the source dimensions, zero-pad the container to the + original length (renders smaller unless the launcher scales) + +Usage: + iconpack.py list + iconpack.py make [--pad] [--patched] + iconpack.py apply +""" +import os, sys, json, struct, hashlib, shutil +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from romfs import Romfs, img_header +from lvgl_bin import rle_compress, pad_rle +from PIL import Image + +CF_ARGB8888, CF_I8, MAGIC = 0x10, 0x0a, 0x19 +FMT = {CF_ARGB8888: 'ARGB8888', CF_I8: 'I8'} +RESAMPLE = Image.LANCZOS + + +# ----------------------------------------------------------------- encoders +def encode_argb8888(img): + rgba = img.convert('RGBA') + w, h = rgba.size + px = rgba.load() + out = bytearray() + for y in range(h): + row = bytearray() + for x in range(w): + r, g, b, a = px[x, y] + row += bytes((b, g, r, a)) + out += row + return bytes(out) + + +def encode_i8(img, alpha_levels=16, sharpen_up=0.0): + """I8 encoder with an ALPHA-AWARE palette. + + The naive version quantised RGB only and then stored one *averaged* alpha + per palette entry. Any colour shared by an opaque body pixel and a + semi-transparent edge pixel collapsed to a mid alpha, and fully transparent + pixels inherited that same entry -- so the transparent margin became faintly + visible (measured: perpetual_calendar haze 0.000 -> 1.000, i.e. a grey veil + around the whole icon). + + Here the palette is built over (quantised RGB, quantised alpha) PAIRS, so a + colour can own several entries at different alphas and alpha 0 stays alpha 0. + """ + rgba = img.convert('RGBA') + w, h = rgba.size + src = rgba.load() + + # average colour of the visible pixels: transparent pixels must not invent + # their own colour clusters + rs = gs = bs = n = 0 + for y in range(h): + for x in range(w): + r, g, b, a = src[x, y] + if a > 0: + rs += r; gs += g; bs += b; n += 1 + avg = (rs // n, gs // n, bs // n) if n else (0, 0, 0) + + step = max(1, 256 // max(1, alpha_levels)) + flat = Image.new('RGB', (w, h)) + fp = flat.load() + qa = [[0] * w for _ in range(h)] + for y in range(h): + for x in range(w): + r, g, b, a = src[x, y] + fp[x, y] = (r, g, b) if a > 0 else avg + q = int(round(a / float(step))) * step + qa[y][x] = 255 if q > 255 else q + + p = flat.quantize(colors=256, method=Image.MEDIANCUT) + pal_rgb = p.getpalette()[:768] + idx = p.tobytes() + + cnt = {} + for y in range(h): + row = y * w + for x in range(w): + k = (idx[row + x], qa[y][x]) + cnt[k] = cnt.get(k, 0) + 1 + keep = sorted(cnt.items(), key=lambda kv: -kv[1])[:256] + + pal = bytearray() + remap = {} + by_rgb = {} + for slot, ((pi, al), _c) in enumerate(keep): + r = pal_rgb[pi * 3] if pi * 3 + 2 < len(pal_rgb) else 0 + g = pal_rgb[pi * 3 + 1] if pi * 3 + 2 < len(pal_rgb) else 0 + b = pal_rgb[pi * 3 + 2] if pi * 3 + 2 < len(pal_rgb) else 0 + pal += bytes((b, g, r, al)) + remap[(pi, al)] = slot + by_rgb.setdefault(pi, []).append((al, slot)) + removed = 256 - len(keep) + if removed > 0: + pal += b'\x00\x00\x00\x00' * removed + for pi in by_rgb: + by_rgb[pi].sort() + + out = bytearray(w * h) + for y in range(h): + row = y * w + for x in range(w): + pi = idx[row + x] + al = qa[y][x] + s = remap.get((pi, al)) + if s is None: + cand = by_rgb.get(pi) + if cand: + s = min(cand, key=lambda t: abs(t[0] - al))[1] + else: + s = 0 + out[row + x] = s + return bytes(pal) + bytes(out) + + +def encode_i8_palette(img, pal_bgra, alpha_tol=12): + """Quantise to an EXISTING 256-entry BGRA palette (as stored in an I8 blob). + + Reusing the source icon's own palette preserves the artwork's gradient shades + and its anti-aliased edge levels. A freshly clustered palette cannot: the + plate gradient eats the 256 entries, the edge collapses to a few alpha steps + and dropped pixels land on wrong entries -- measured on remote_camera as a + jagged plate rim plus stray coloured speckles. + + The search is constrained by alpha so that + * fully transparent source pixels can only pick transparent entries + (no veil, no speckles), and + * fully opaque source pixels can only pick opaque entries + (the body never goes translucent). + """ + rgba = img.convert('RGBA') + w, h = rgba.size + px = rgba.load() + + ent = [(pal_bgra[i * 4 + 2], pal_bgra[i * 4 + 1], + pal_bgra[i * 4], pal_bgra[i * 4 + 3]) for i in range(256)] + clear = [i for i, e in enumerate(ent) if e[3] == 0] + if not clear: # guarantee a transparent entry exists + worst = min(range(256), key=lambda i: ent[i][3]) + ent[worst] = (0, 0, 0, 0) + clear = [worst] + solid = [i for i, e in enumerate(ent) if e[3] >= 248] + by_alpha = {} + for i, e in enumerate(ent): + by_alpha.setdefault(e[3], []).append(i) + + def premul(e): + r, g, b, a = e + return (r * a // 255, g * a // 255, b * a // 255, a) + + P = [premul(e) for e in ent] + cache = {} + out = bytearray(w * h) + for y in range(h): + row = y * w + for x in range(w): + r, g, b, a = px[x, y] + key = (r >> 2, g >> 2, b >> 2, a >> 3) + s = cache.get(key) + if s is None: + if a == 0: + cand = clear + elif a >= 248 and solid: + cand = solid + else: + cand = [] + for da in range(0, alpha_tol + 1): + cand += by_alpha.get(a + da, ()) if a + da <= 255 else () + if da: + cand += by_alpha.get(a - da, ()) if a - da >= 0 else () + if not cand: + cand = range(256) + pr, pg, pb, pa = premul((r, g, b, a)) + best, bd = cand[0], None + for i in cand: + q = P[i] + d = ((pr - q[0]) ** 2 + (pg - q[1]) ** 2 + (pb - q[2]) ** 2 + + 2 * (pa - q[3]) ** 2) + if bd is None or d < bd: + bd, best = d, i + s = best + cache[key] = s + out[row + x] = s + + pal = bytearray() + for (r, g, b, a) in ent: + pal += bytes((b, g, r, a)) + return bytes(pal) + bytes(out) + + +def encode(img, cf, w, h, flags=0, pad_to=None, palette=None): + if cf == CF_ARGB8888: + body = encode_argb8888(img) + elif palette is not None: + body = encode_i8_palette(img, palette) + else: + body = encode_i8(img) + stride = w * (4 if cf == CF_ARGB8888 else 1) + blob = struct.pack(' pad_to: + raise ValueError(f'encoded {len(blob)} > slot {pad_to}') + blob += b'\x00' * (pad_to - len(blob)) + return blob + + +# ----------------------------------------------------------------- helpers +def raw_i8_strided(img, stride, colors=256): + """I8 像素流:1024 B 调色板(BGRA)+ stride*h 个索引(行尾按 header 的 stride 补 0)。 + + RLE 槽位专用:用 FASTOCTREE 自适应减色 + 关闭抖动 —— 颜色越少、相邻像素越容易出现 + 长游程,RLE 流就越短(这是能不能塞进原 csize 的关键)。 + """ + q = img.convert('RGBA').quantize(colors=max(2, min(256, colors)), + method=Image.FASTOCTREE, dither=Image.NONE) + if q.palette.mode == 'RGBA': + pal = list(q.getpalette('RGBA')) + else: + rgb = q.getpalette('RGB') + pal = [v for i in range(256) for v in (rgb[i * 3], rgb[i * 3 + 1], rgb[i * 3 + 2], 255)] + pal = (pal + [0] * 1024)[:1024] + pal_b = bytearray() + for i in range(256): # 存储序:B, G, R, A + r, g, b, a = pal[i * 4:i * 4 + 4] + pal_b += bytes((b, g, r, a)) + idx = q.tobytes() + w, h = img.size + rows = bytearray() + for y in range(h): + rows += idx[y * w:(y + 1) * w] + if stride > w: + rows += b'\x00' * (stride - w) + return bytes(pal_b) + bytes(rows) + + +# 从大到小试:优先保真,装不下再减色 +RLE_COLOR_STEPS = (256, 192, 160, 128, 96, 80, 64, 48, 40, 32, 24, 16, 12, 8, 6, 4) + + +def encode_into_slot(img, stock_blob, slot_len=None, colors=None): + """把 img 编成与 stock_blob **逐字节等长** 的 LVGL v9 blob(沿用原 header)。 + + 真实固件里这些 UI 素材有两种槽位形态,都必须支持,否则 ROMFS 后面所有偏移全平移: + * ARGB8888 未压缩(144x144 图标那种):像素流 + 零填充到原长度 + * I8 + RLE 压缩(walkie_talkie 里几乎全是):量化 -> RLE -> 用 ctrl=0 空指令补齐到原 csize + (颜色数从 256 逐级往下试,取第一个能塞进原 csize 的) + 返回 (blob, how);不支持 / 压缩后仍超长时返回 (None, 原因)。 + """ + h = img_header(stock_blob) + if h is None: + return None, '不是 LVGL bin' + w, ht, cf, flags = h['w'], h['h'], h['cf'], h['flags'] + if slot_len is None: + slot_len = len(stock_blob) + out_img = img.convert('RGBA') + if out_img.size != (w, ht): + # 约定:**长宽比不一致的素材直接不替换**(保持原厂图),而不是拉伸或裁切 —— + # 拉伸会把方块变长方形;裁切会切掉按钮两端。宁可保持原厂,也不给设备塞变形图。 + pw, ph = out_img.size + if abs(pw / ph - w / ht) / (w / ht) > 0.005: + return None, ('长宽比不一致(源 %dx%d = %.3f,槽位 %dx%d = %.3f)-> 按约定跳过,保持原厂' + % (pw, ph, pw / ph, w, ht, w / ht)) + out_img = out_img.resize((w, ht), RESAMPLE) + if cf == CF_ARGB8888 and not (flags & 0x08): + blob = stock_blob[:12] + encode_argb8888(out_img) + if len(blob) > slot_len: + return None, 'ARGB 编码 %d > 槽位 %d' % (len(blob), slot_len) + return blob + b'\x00' * (slot_len - len(blob)), 'ARGB(补 %d)' % (slot_len - len(blob)) + if cf == CF_I8 and (flags & 0x08): + method, csize, usize = struct.unpack(' csize %d(已减到 %d 色)' % (len(comp), csize, n_last) + n, raw, comp = best + comp = comp + bytes((1, raw[-1])) # 把流做到正好 usize 字节,和原厂一致 + padded = pad_rle(comp, csize) + if padded is None: + return None, '补齐失败' + blob = stock_blob[:12] + struct.pack(' inode for every launcher icon. + + alarm/launcher.bin -> alarm + sports/training/launcher.bin -> sports_training + phone/contacts_launcher.bin -> phone_contacts + """ + out = {} + for p, i in r.entries(): + if not p.rsplit('/', 1)[-1].endswith('launcher.bin'): + continue + stem = p[:-4] # drop '.bin' + if stem.endswith('_launcher'): + stem = stem[:-len('_launcher')] + elif stem.endswith('/launcher'): + stem = stem[:-len('/launcher')] + out[stem.replace('/', '_')] = i + return out + + +def describe(r, i): + h = img_header(r.read(i)) + if not h: + return '?', '?', '?' + return f'{h["w"]}x{h["h"]}', FMT.get(h['cf'], f'0x{h["cf"]:02x}'), h + + +def cmd_list(image): + r = Romfs(image) + print(f'{os.path.basename(image)} volume={r.volume} romfs_size={r.sb_size}') + for app, i in sorted(targets(r).items()): + dim, fmt, _ = describe(r, i) + print(f' {app:<26} data@0x{i.data_off:08x} len={i.size:<7} {dim} {fmt}') + + +def cmd_make(image, pngdir, outdir, pad=False, write_patched=False): + r = Romfs(image) + tg = targets(r) + os.makedirs(outdir, exist_ok=True) + payload_dir = os.path.join(outdir, 'payload') + os.makedirs(payload_dir, exist_ok=True) + patch = bytearray() + rollback = bytearray() + records, skipped = [], [] + for fn in sorted(os.listdir(pngdir)): + if not fn.lower().endswith('.png'): + continue + app = fn[:-4] + if app not in tg: + skipped.append((app, 'no such app')) + continue + ino = tg[app] + orig = r.read(ino) + _, fmt, h = describe(r, ino) + if h is None: + skipped.append((app, f'not LVGL image (len={ino.size})')) + continue + src = Image.open(os.path.join(pngdir, fn)).convert('RGBA') + src_dim = f'{src.size[0]}x{src.size[1]}' + resized = False + if src.size != (h['w'], h['h']): + if pad: + tw, th = src.size + blob = encode(src, h['cf'], tw, th, h['flags'], pad_to=len(orig)) + else: + src = src.resize((h['w'], h['h']), RESAMPLE) + tw, th = src.size + resized = True + blob = encode(src, h['cf'], tw, th, h['flags']) + else: + tw, th = src.size + blob = encode(src, h['cf'], tw, th, h['flags']) + if len(blob) != len(orig): + skipped.append((app, f'length {len(blob)} != {len(orig)}')) + continue + patch += struct.pack('12}{"bytes":>8} {"fmt":<10}{"stock":>9}{"source":>9} note') + for m in records: + note = 'resized' if m['resized'] else ('padded' if m['padded'] else '') + print(f'{m["app"]:<26}0x{m["offset"]:08x}{m["length"]:>8} {m["format"]:<10}' + f'{m["stock_dim"]:>9}{m["source_dim"]:>9} {note}') + for a, why in skipped: + print(f' SKIP {a}: {why}') + print(f'\n{len(records)} replacement(s)') + print(f' payload -> {payload_dir}') + print(f' patch -> {os.path.join(outdir, "icons.patch")} ({len(patch)} bytes)') + print(f' rollback -> {os.path.join(outdir, "icons.rollback")} ({len(rollback)} bytes)') + print(f' manifest -> {os.path.join(outdir, "manifest.json")}') + if write_patched: + out = os.path.join(outdir, 'vela_app_patched.bin') + cmd_apply(image, os.path.join(outdir, 'icons.patch'), out) + + +def cmd_apply(image, patch, out): + data = bytearray(open(image, 'rb').read()) + blob = open(patch, 'rb').read() + off = n = 0 + while off < len(blob): + addr, ln = struct.unpack(' {out}') + + +if __name__ == '__main__': + a = sys.argv[1:] + if not a: + print(__doc__) + elif a[0] == 'list': + cmd_list(a[1]) + elif a[0] == 'make': + cmd_make(a[1], a[2], a[3], '--pad' in a, '--patched' in a) + elif a[0] == 'apply': + cmd_apply(a[1], a[2], a[3]) + else: + print(__doc__) diff --git a/s5e-icons/tools/lua_balance.py b/s5e-icons/tools/lua_balance.py new file mode 100644 index 0000000..a33ebba --- /dev/null +++ b/s5e-icons/tools/lua_balance.py @@ -0,0 +1,72 @@ +"""Crude but effective Lua block-balance checker: strips comments and strings, +then counts block openers vs `end`.""" +import sys, re + +def strip(src): + out = [] + i, n = 0, len(src) + while i < n: + c = src[i] + # long bracket comment / string --[[ ]] , --[==[ ]==] + m = re.match(r'--\[(=*)\[', src[i:]) + if m: + close = ']' + m.group(1) + ']' + j = src.find(close, i) + i = n if j < 0 else j + len(close) + out.append(' ') + continue + if src.startswith('--', i): + j = src.find('\n', i) + i = n if j < 0 else j + out.append(' ') + continue + m = re.match(r'\[(=*)\[', src[i:]) + if m: + close = ']' + m.group(1) + ']' + j = src.find(close, i) + i = n if j < 0 else j + len(close) + out.append('""') + continue + if c in '"\'': + j = i + 1 + while j < n: + if src[j] == '\\': + j += 2 + continue + if src[j] == c: + break + j += 1 + i = j + 1 + out.append('""') + continue + out.append(c) + i += 1 + return ''.join(out) + + +def check(path): + src = open(path, encoding='utf-8').read() + code = strip(src) + def cnt(k): + return len(re.findall(r'(? n: + raise ValueError('truncated literal run') + out += data[i:i + nb] + i += nb + else: + cnt = ctrl + if i + blk > n: + raise ValueError('truncated repeat unit') + unit = data[i:i + blk] + i += blk + out += unit * cnt + if out_len is not None and len(out) >= out_len: + break + return bytes(out) + + +def rle_compress(raw, max_run=127): + """LVGL RLE 编码器(rle_decompress 的逆向,blk=1)。 + + 贪心:先找同字节游程(>=2 用「重复」指令),否则攒「原样」指令。 + 输出长度总是 <= 原图像素流,因为最坏情况每条 literal 只多 1 个控制字节。 + """ + out = bytearray() + i, n = 0, len(raw) + while i < n: + v = raw[i] + j = i + 1 + while j < n and raw[j] == v and j - i < max_run: + j += 1 + run = j - i + if run >= 2: + out.append(run); out.append(v); i = j + continue + lit = bytearray() + k = i + while k < n and len(lit) < max_run: + if k + 1 < n and raw[k] == raw[k + 1]: + break + lit.append(raw[k]); k += 1 + if not lit: # 理论上到不了这里 + lit.append(v); k = i + 1 + out.append(0x80 | len(lit)); out += lit + i = k + return bytes(out) + + +def pad_rle(comp, want): + """把 RLE 流补到 want 字节:用 ctrl=0(重复 0 次 = 产出 0 像素)的 2 字节空指令。 + + ctrl=0 是合法指令(`len = 0`,两个越界判断都不触发),所以设备端解码器读完 csize + 字节既不会越界、也不会返回 INVALID —— 这是「等长覆盖」能成立的关键。 + 差值为奇数时先把一条 literal 拆成两条(payload 不变、多 1 个控制字节)。 + """ + d = want - len(comp) + if d < 0: + return None + if d % 2: + b = bytearray(comp) + for idx in range(len(b) - 1, -1, -1): + if b[idx] & 0x80 and (b[idx] & 0x7F) >= 2: + cnt = b[idx] & 0x7F + lit = bytes(b[idx + 1: idx + 1 + cnt]) + half = cnt // 2 + b[idx: idx + 1 + cnt] = (bytes([0x80 | half]) + lit[:half] + + bytes([0x80 | (cnt - half)]) + lit[half:]) + break + else: + return None + comp = bytes(b) + d -= 1 + return comp + b'\x00\x00' * (d // 2) + + +def parse_header(blob): + if len(blob) < 12 or blob[0] != MAGIC: + return None + magic, cf, flags, w, h, stride, rsv = struct.unpack(' 切到该颜色(该字符不显示) + - 再次遇到单独的 # -> 结束着色,回到基础色(该字符不显示) + """ + runs, buf, cur, incol = [], '', base, False + i = 0 + while i < len(text): + if text[i] == '#': + if re.match(r'[0-9a-fA-F]{6}', text[i + 1:i + 7]): + runs.append((cur, buf)); buf = '' + cur = text[i + 1:i + 7].lower(); incol = True + i += 7 + continue + if incol: + runs.append((cur, buf)); buf = '' + cur = base; incol = False + i += 1 + continue + buf += text[i]; i += 1 + runs.append((cur, buf)) + return runs + + +def wrap_runs(d, runs, f, maxw): + """按像素宽度折行,返回 [[(color, text), ...], ...](尽量保持原换行)""" + lines = [[]] + for color, chunk in runs: + for k, seg in enumerate(chunk.split('\n')): + if k: + lines.append([]) + if not seg: + continue + cur = '' + for ch in seg: + if d.textlength(cur + ch, font=f) > maxw and cur: + lines[-1].append((color, cur)) + lines.append([]) + cur = ch + else: + cur += ch + if cur: + lines[-1].append((color, cur)) + return lines + + +OVERFLOW = [] # (标签文本前 12 字, css_lh, 行数, 块高, 框高) + + +def draw_label(img, d, it): + f = font(it.get('weight', 'medium'), it['size']) + runs = split_recolor(it['text'], it.get('color', 'ffffff')) \ + if it.get('recolor') else [(it.get('color', 'ffffff'), it['text'])] + asc, desc = f.getmetrics() + natural = asc + desc + lh = max(it.get('lh', 0), natural) # LVGL 用字体自身行高,取较大者预测 + lines = wrap_runs(d, runs, f, it['w']) + total = lh * len(lines) + if total > it['h'] + 0.5: + OVERFLOW.append((it['text'][:14].replace('\n', '/'), + round(it.get('lh', 0), 2), len(lines), + round(total, 1), it['h'], round(natural, 2))) + if it.get('vcenter'): + y = it['y'] + (it['h'] - total) / 2 + elif it.get('vtop'): + y = it['y'] + else: + y = it['y'] + (it['h'] - total) / 2 + for ln in lines: + wsum = sum(d.textlength(t, font=f) for _, t in ln) + if it.get('align') == 'center': + x = it['x'] + (it['w'] - wsum) / 2 + elif it.get('align') == 'right': + x = it['x'] + it['w'] - wsum + else: + x = it['x'] + for color, t in ln: + d.text((x, y), t, font=f, fill=_rgb(color) + (255,)) + x += d.textlength(t, font=f) + y += lh + + +def render(items, out, mask=True, ring=False): + img = Image.new('RGBA', (W, H), (0, 0, 0, 0)) + d = ImageDraw.Draw(img) + for it in items: + k = it.get('kind') + if k == 'rect': + box = [it['x'], it['y'], it['x'] + it['w'] - 1, it['y'] + it['h'] - 1] + r = it.get('radius', 0) + (d.rounded_rectangle if r else d.rectangle)( + box, radius=r, fill=_rgb(it['bg']) + (255,)) if r else d.rectangle( + box, fill=_rgb(it['bg']) + (255,)) + elif k == 'circle': + box = [it['x'], it['y'], it['x'] + it['w'] - 1, it['y'] + it['h'] - 1] + d.ellipse(box, fill=_rgb(it['bg']) + (255,)) + elif k == 'image': + p = it['src'] + if os.path.exists(p): + im = Image.open(p).convert('RGBA').resize((it['w'], it['h']), Image.LANCZOS) + img.alpha_composite(im, (it['x'], it['y'])) + elif k == 'label': + draw_label(img, d, it) + if mask: + m = Image.new('L', (W, H), 0) + ImageDraw.Draw(m).ellipse([0, 0, W - 1, H - 1], fill=255) + img.putalpha(m) + base = Image.new('RGB', (W, H), (0, 0, 0)) + base.paste(img, (0, 0), img) + if ring: + ImageDraw.Draw(base).ellipse([0, 0, W - 1, H - 1], outline=(80, 80, 90)) + base.save(out) + return out + + +if __name__ == '__main__': + from pages import PAGES + outs = [] + for name, items in PAGES.items(): + p = os.path.join(ROOT, '_out', f'ui_{name}.png') + render(items, p) + outs.append(p) + print('wrote', p) + # 拼一张总览图 + cols = 3 + rows = (len(outs) + cols - 1) // cols + sheet = Image.new('RGB', (cols * (W + 12) + 12, rows * (H + 12) + 12), (28, 28, 32)) + for i, p in enumerate(outs): + im = Image.open(p) + sheet.paste(im, (12 + (i % cols) * (W + 12), 12 + (i // cols) * (H + 12))) + sp = os.path.join(ROOT, '_out', 'ui_all.png') + sheet.save(sp) + print('wrote', sp, sheet.size) diff --git a/s5e-icons/tools/romfs.py b/s5e-icons/tools/romfs.py new file mode 100644 index 0000000..753dda2 --- /dev/null +++ b/s5e-icons/tools/romfs.py @@ -0,0 +1,112 @@ +"""Minimal reader for Xiaomi Vela ROMFS partition images (Linux 'rom1fs' variant). + +Layout (big-endian): + super : magic "-rom1fs-", u32 size, u32 checksum, name (NUL-term, padded to 16) + inode : u32 next (low 4 bits = ROMFH_ type flags), u32 spec, u32 size, u32 checksum, + name (NUL-term, padded to 16), then file data immediately after. + type : 0=hardlink (spec = target inode offset), 1=dir (spec = offset of first child), + 2=regular file, 8=exec bit OR-ed in. + For a directory, its children are the chain starting at `spec`, linked by + `next & ~0xF`, and they stay inside the directory's own block. +""" +import struct, os + +ROUND16 = lambda n: (n + 15) & ~15 + +class Inode: + __slots__ = ('off', 'next', 'spec', 'size', 'cksum', 'name', 'namelen', 'data_off', 'kind') + + @property + def type_flags(self): + return self.next & 0xF + + @property + def type(self): + return self.next & 0x7 + + @property + def exec_bit(self): + return bool(self.next & 0x8) + + @property + def next_off(self): + return self.next & ~0xF + + KIND = {0: 'HRD', 1: 'DIR', 2: 'REG', 3: 'SYM', 4: 'BLK', 5: 'CHR', 6: 'SCK', 7: 'FIF'} + + def __repr__(self): + return (f'') + + +class Romfs: + def __init__(self, path): + self.path = path + self.data = open(path, 'rb').read() + d = self.data + if d[0:8] != b'-rom1fs-': + raise ValueError('not a romfs image') + self.sb_size, self.sb_cksum = struct.unpack('>II', d[8:16]) + end = d.index(b'\x00', 16) + self.volume = d[16:end].decode('latin1') + self.root_off = 16 + ROUND16(end - 16 + 1) + + def inode(self, off): + i = Inode() + i.off = off + i.next, i.spec, i.size, i.cksum = struct.unpack('>IIII', self.data[off:off+16]) + p = off + 16 + end = self.data.find(b'\x00', p, p + 256) + if end < 0: + raise ValueError(f'bad name at 0x{off:x}') + i.name = self.data[p:end].decode('latin1') + i.namelen = end - p + i.data_off = off + 16 + ROUND16(i.namelen + 1) + return i + + def walk(self, first, limit, prefix=''): + """Yield (path, Inode) for every entry reachable from `first` below `limit`. + + `limit` bounds the sibling chain. The LAST entry of a chain has next==0, + but its own children still live after it: they inherit the parent's limit + instead of being bounded by that zero. + """ + off = first + seen = set() + while off and (limit == 0 or off < limit) and off not in seen: + seen.add(off) + i = self.inode(off) + path = prefix + i.name + yield path, i + if i.type == 1 and i.spec: # directory -> first child at spec + yield from self.walk(i.spec, i.next_off or limit, path + '/') + off = i.next_off + + def entries(self): + """All entries of the image, as (path, Inode).""" + return self.walk(self.root_off + 0x20, len(self.data)) + + def read(self, ino): + return self.data[ino.data_off:ino.data_off + ino.size] + + +def img_header(blob): + if len(blob) < 12 or blob[0] != 0x19: + return None + magic, cf, flags, w, h, stride, res = struct.unpack(' 2 and sys.argv[2] not in path: + continue + kind = ('DIR ' if i.type == 1 else 'LNK ' if i.type == 0 else 'FILE') + print(f'{kind} {i.size:>9} {path}') + print(f'-- total entries: {n}') diff --git a/s5e-icons/tools/s5e_build.py b/s5e-icons/tools/s5e_build.py new file mode 100644 index 0000000..c0a37e5 --- /dev/null +++ b/s5e-icons/tools/s5e_build.py @@ -0,0 +1,266 @@ +# -*- coding: utf-8 -*- +"""Two-pass build for the S5 eSIM icon face. + + watchface_s5e/main.lua 源码(含 __UIIMG__ / __PAY__ / __RB__ 占位符) + build_overlay_s5e/S5eIcons 中间产物(回填后的 Lua 落在这里) + _build_s5e/S5eIcons 编译工作区(Compiler.exe 的输入) + _out/faces/S5eIcons.face 交付物 + +为什么两遍:.face 里各资源的偏移取决于打包顺序,我们靠在 .face 里搜字节来定位。 +Lua 源码排在所有资源之后,所以回填(Lua 变长)不会移动前面的资源;脚本最后逐条校验 0 漂移。 +""" +import os, sys, json, shutil, subprocess, hashlib, struct +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from PIL import Image, ImageDraw +import render_ui as R +from s5e_pages import PAGES + +from paths import PROJ, WORK, DIST, TEMPLATE +WF = PROJ # 源码就在项目根 +TPL = TEMPLATE +OVER = os.path.join(WORK, 'overlay', 'S5eIcons') +BUILD = os.path.join(WORK, 'build', 'S5eIcons') +OUTFACE = DIST +NAME = 'S5eIcons' +WF_ID = '462150101' # 462 = S5 家族(固件自带 AOD 表盘是 462150001..8) +DEVICE_TYPE = '462' +SCREEN = 480 +PREVIEW = 326 +MAGIC_PAY = b'\x00S5E-PAYv1\x00\xa5\x5a\xc3\x3c' +MAGIC_RB = b'\x00S5E-RBKv1\x00\x5a\xa5\x3c\xc3' +MAGIC_LEN = len(MAGIC_PAY) # 15 —— 别再手写常量,之前写 16 让偏移整体差 1 +LOG = [] + + +def log(s): + print(s) + LOG.append(s) + + +def make_preview(path): + """480 首页 -> 圆形 alpha -> 326x326(Compiler.exe 只收这个尺寸)""" + R.W = R.H = SCREEN + items = PAGES['home'] + img = Image.new('RGBA', (SCREEN, SCREEN), (0, 0, 0, 0)) + d = ImageDraw.Draw(img) + for it in items: + k = it.get('kind') + if k == 'rect': + box = [it['x'], it['y'], it['x'] + it['w'] - 1, it['y'] + it['h'] - 1] + r = it.get('radius', 0) + (d.rounded_rectangle(box, radius=r, fill=R._rgb(it['bg']) + (255,)) if r + else d.rectangle(box, fill=R._rgb(it['bg']) + (255,))) + elif k == 'circle': + d.ellipse([it['x'], it['y'], it['x'] + it['w'] - 1, it['y'] + it['h'] - 1], + fill=R._rgb(it['bg']) + (255,)) + elif k == 'image': + if os.path.exists(it['src']): + im = Image.open(it['src']).convert('RGBA').resize((it['w'], it['h']), Image.LANCZOS) + img.alpha_composite(im, (it['x'], it['y'])) + elif k == 'label': + R.draw_label(img, d, it) + base = Image.new('RGBA', (SCREEN, SCREEN), (0, 0, 0, 255)) + base.alpha_composite(img) + mask = Image.new('L', (SCREEN, SCREEN), 0) + ImageDraw.Draw(mask).ellipse([0, 0, SCREEN - 1, SCREEN - 1], fill=255) + base.putalpha(mask) + base.resize((PREVIEW, PREVIEW), Image.LANCZOS).save(path) + + +def prepare(lua_text): + if os.path.exists(BUILD): + shutil.rmtree(BUILD) + shutil.copytree(TPL, BUILD) + old = os.path.join(BUILD, 'watchface', 'fprj', 'LuaDevTemplate.fprj') + if os.path.exists(old): + os.remove(old) + + cfg = {"projectName": NAME, "watchfaceId": WF_ID, "power_consumption": "3", + "resourceBin": {"lvglVersion": 9, "colorFormat": "I8", "compress": "NONE", + "input": "watchface/fprj/images/preview.png", "name": "preview"}} + json.dump(cfg, open(os.path.join(BUILD, 'watchface.config.json'), 'w', encoding='utf-8'), + indent=4, ensure_ascii=False) + + fprj = ('\r\n' + '\r\n' + ' \r\n' + ' \r\n' + ' \r\n\r\n') % (DEVICE_TYPE, NAME, SCREEN, SCREEN) + open(os.path.join(BUILD, 'watchface', 'fprj', NAME + '.fprj'), 'w', + encoding='utf-16', newline='').write(fprj) + + fprjdir = os.path.join(BUILD, 'watchface', 'fprj') + make_preview(os.path.join(fprjdir, 'images', 'preview.png')) + open(os.path.join(fprjdir, 'app', 'lua', 'main.lua'), 'w', + encoding='utf-8', newline='\n').write(lua_text) + + imgdir = os.path.join(fprjdir, 'app', 'images') + for sub, src in (('pay', os.path.join(WF, 'payload')), + ('ui', os.path.join(WF, 'ui_images'))): + dst = os.path.join(imgdir, sub) + if os.path.exists(dst): + shutil.rmtree(dst) + os.makedirs(dst) + for fn in sorted(os.listdir(src)): + shutil.copyfile(os.path.join(src, fn), os.path.join(dst, fn)) + shutil.copyfile(os.path.join(WF, 'payload_rollback', 'rollback.bin'), + os.path.join(imgdir, 'pay', 'rollback.bin')) + + +def run_build(script=True): + ps = os.path.join(BUILD, 'scripts', 'build_face.ps1') + r = subprocess.run(['powershell', '-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', ps], + capture_output=True, text=True, errors='replace') + tail = [l for l in ((r.stdout or '') + (r.stderr or '')).splitlines() if l.strip()][-6:] + for l in tail: + log(' | ' + l) + face = os.path.join(BUILD, 'bin', NAME + '.face') + if not os.path.exists(face): + raise SystemExit('build failed: no ' + face) + return open(face, 'rb').read() + + +def compile_raw(): + exe = os.path.join(BUILD, 'watchface', 'tools', 'Compiler.exe') + fprj = os.path.join(BUILD, 'watchface', 'fprj', NAME + '.fprj') + out = os.path.join(BUILD, 'raw') + if os.path.exists(out): + shutil.rmtree(out) + os.makedirs(out) + subprocess.run([exe, '-b', fprj, out, NAME + '.face', WF_ID], + capture_output=True, text=True, errors='replace') + p = os.path.join(out, NAME + '.face') + if not os.path.exists(p): + raise SystemExit('raw compile failed') + return open(p, 'rb').read() + + +def hits(face, blob): + out, s = [], 0 + while True: + i = face.find(blob, s) + if i < 0: + return out + out.append(i) + s = i + 1 + + +def locate(face, rels): + """量出 UI 图片 / 负载 在 .face 里的偏移,并逐条字节校验""" + ui = [] + for fn in sorted(os.listdir(os.path.join(WF, 'ui_images'))): + if not fn.endswith('.bin'): + continue + blob = open(os.path.join(WF, 'ui_images', fn), 'rb').read() + h = hits(face, blob) + if len(h) != 1: + raise SystemExit('UI %s 命中 %d 次' % (fn, len(h))) + ui.append((h[0], len(blob), fn[:-4])) + pay_base = hits(face, MAGIC_PAY) + rb_base = hits(face, MAGIC_RB) + if len(pay_base) != 1 or len(rb_base) != 1: + raise SystemExit('payload magic 命中 %d/%d 次(应为 1)' % (len(pay_base), len(rb_base))) + pay_base, rb_base = pay_base[0], rb_base[0] + + hashes = {'pay': {i: r['sha256'] for i, r in enumerate(recs)}, + 'rb': {i: r['rb_sha256'] for i, r in enumerate(recs)}} + for tag, base, path in (('pay', pay_base, os.path.join(WF, 'payload', 'payload.bin')), + ('rb', rb_base, os.path.join(WF, 'payload_rollback', 'rollback.bin'))): + data = open(path, 'rb').read() + # 整个负载文件必须**原样连续**放在 base 处 —— 这是"偏移=base+magic+rel"成立的前提 + if face.find(data) != base: + raise SystemExit('%s 文件不是原样连续存放(找不到或位置不符)' % tag) + for i, (off, ln) in enumerate(rels[tag]): + res = base + MAGIC_LEN + off + if i == 0: + log(' %s 资源偏移 0x%x(%%4=%d,运行时走 /tmp 中转,不依赖对齐)' + % (tag, res, res % 4)) + # 用独立的 sha256(来自生成负载时的 manifest)校验,避免"自己比自己"这种恒真检查 + seg = face[res:res + ln] + if len(seg) != ln or hashlib.sha256(seg).hexdigest() != hashes[tag][i]: + raise SystemExit('%s[%d] 在 0x%x 处字节不吻合' % (tag, i, res)) + return ui, pay_base, rb_base + + +FACE = None +rels = {'pay': [], 'rb': []} +recs = json.load(open(os.path.join(WORK, 's5e_from_os4', 'manifest.json'), encoding='utf-8'))['records'] +off = 0 +for r in recs: + rels['pay'].append((off, r['length'])) + off += r['length'] +off = 0 +for r in recs: + rels['rb'].append((off, r['length'])) + off += r['length'] + +src = open(os.path.join(WF, 'main.lua'), encoding='utf-8').read() +for ph in ('__UIIMG__', '__PAY__', '__RB__'): + if ph not in src: + raise SystemExit('watchface_s5e/main.lua 缺少占位符 ' + ph) + +# ---------------------------------------------------------------- pass 1 +log('第 1 遍:占位符版编一次,量出资源偏移') +prepare(src) +face1 = run_build() +log(' face %d bytes' % len(face1)) +ui, pay_base, rb_base = locate(face1, rels) +for o, l, n in ui: + log(' %-12s off=0x%06x len=%d' % (n, o, l)) +log(' payload base=0x%06x rollback base=0x%06x' % (pay_base, rb_base)) + +ui_txt = '\n'.join(' { %d, %d, "%s" },' % (o, l, n) for o, l, n in ui) +pay_txt = ' pay = {\n' + '\n'.join( + ' { "%s", 0x%08x, %d, %d },' % (r['dev'], r['offset'], r['length'], pay_base + MAGIC_LEN + rels['pay'][i][0]) + for i, r in enumerate(recs)) + '\n },' +rb_txt = ' rb = {\n' + '\n'.join( + ' { "%s", 0x%08x, %d, %d },' % (r['dev'], r['offset'], r['length'], rb_base + MAGIC_LEN + rels['rb'][i][0]) + for i, r in enumerate(recs)) + '\n },' +resolved = src.replace('__UIIMG__', ui_txt).replace('__PAY__', pay_txt).replace('__RB__', rb_txt) +assert '__UIIMG__' not in resolved and '__PAY__' not in resolved and '__RB__' not in resolved +overlay_lua = os.path.join(OVER, 'watchface', 'fprj', 'app', 'lua') +os.makedirs(overlay_lua, exist_ok=True) +open(os.path.join(overlay_lua, 'main.lua'), 'w', encoding='utf-8', newline='\n').write(resolved) +log('第 2 遍:回填后的 Lua(%d bytes)重新编译' % len(resolved)) + +# ---------------------------------------------------------------- pass 2 +prepare(resolved) +face2 = run_build() +ui2, pay_base2, rb_base2 = locate(face2, rels) +drift = 0 +if (pay_base2, rb_base2) != (pay_base, rb_base) or ui2 != ui: + drift = 1 +log('偏移校验:%s' % ('0 漂移' if not drift else '!! 有漂移')) +if drift: + raise SystemExit('resource offsets drifted between pass 1 and pass 2') + +# ---------------------------------------------------------------- finalize +raw = compile_raw() +d = bytearray(raw) +assert d[0:4] == b'\x5a\xa5\x34\x12', 'bad face magic' +d[40:50] = WF_ID.encode() + b'\x00' * (10 - len(WF_ID)) +os.makedirs(OUTFACE, exist_ok=True) +faithful = os.path.join(OUTFACE, NAME + '.face') +open(faithful, 'wb').write(bytes(d)) +toolchain = os.path.join(OUTFACE, NAME + '_toolchain.face') +shutil.copyfile(os.path.join(BUILD, 'bin', NAME + '.face'), toolchain) + +# 交付产物独立自查:直接从最终 .face 里抠出来比对 +fin = open(faithful, 'rb').read() +ui3, pb3, rb3 = locate(fin, rels) +assert ui3 == ui and pb3 == pay_base and rb3 == rb_base +for i, r in enumerate(recs): + res = pb3 + MAGIC_LEN + rels['pay'][i][0] + assert fin[res:res + r['length']] == open(os.path.join(WF, 'payload', 'payload.bin'), 'rb').read()[MAGIC_LEN + rels['pay'][i][0]:MAGIC_LEN + rels['pay'][i][0] + r['length']] +log('') +log('%-34s %10s b4 b5 id sha256[:16]' % ('file', 'bytes')) +for p in (faithful, toolchain): + b = open(p, 'rb').read() + log('%-34s %10d %2d %2d %-10r %s' % (os.path.basename(p), len(b), b[4], b[5], b[40:50], + hashlib.sha256(b).hexdigest()[:16])) +_p32, = struct.unpack(' 8: + dd = math.hypot(x - CX, y - CY) + if dd > worst: + worst, wpt = dd, (x, y) + if dd > RAD: + outside += 1 + print('%-10s %8.1f %11d %s%s' % (name, worst, outside, wpt, + '' if worst <= RAD else ' <-- CLIPPED')) + if outside: bad += 1 +print('-' * 60) +print('centre (240,240) radius 240 canvas 480x480 pages clipped:', bad) diff --git a/s5e-icons/tools/s5e_pages.py b/s5e-icons/tools/s5e_pages.py new file mode 100644 index 0000000..79a4bb7 --- /dev/null +++ b/s5e-icons/tools/s5e_pages.py @@ -0,0 +1,133 @@ +# -*- coding: utf-8 -*- +"""Xiaomi Watch S5 eSIM (480x480) page spec. + +Same design language as the 466x466 S5->S4 face, uniformly scaled by 480/466 and +re-centred; every coordinate here is what watchface_s5e/main.lua uses, and +_tools/s5e_render_ui.py draws exactly this spec so the previews are honest. +""" +import os + +from paths import PROJ, DOCS +ROOT = PROJ +ASSETS = os.path.join(ROOT, 'assets') +W = H = 480 +BG = (0, 0, 0) +GRAY = '999999' +WHITE = 'ffffff' +PLATE = '002927' +CIRCLE = '4f4f4f' +OK = '43cf7c' +ERR = 'd43030' +S600, S500 = 'semibold', 'medium' + +# list item geometry +ITEM_X, ITEM_W, ITEM_H, ITEM_R = 50, 379, 91, 35 +ICONC_DX, ICONC_DY, ICONC_SZ = 21, 20, 52 +ICON_DX, ICON_DY, ICON_SZ = 31, 30, 31 +TITLE_DX, TITLE_DY, TITLE_W, TITLE_H = 82, 26, 214, 40 +ICON_PNG = {'ic_help': 'MdiLightbulbOutline.png', 'ic_replace': 'MdiBandage.png', + 'ic_about': 'MdiInformationOutline.png', 'ic_check': 'MdiClipboardTextSearchOutline.png', + 'ic_rollback': 'MdiReplyAllOutline.png', 'ic_qq': 'qq.png'} + + +def item(y, iconkey, title): + return [ + dict(kind='rect', x=ITEM_X, y=y, w=ITEM_W, h=ITEM_H, bg=PLATE, radius=ITEM_R), + dict(kind='circle', x=ITEM_X + ICONC_DX, y=y + ICONC_DY, w=ICONC_SZ, h=ICONC_SZ, bg=CIRCLE), + dict(kind='image', x=ITEM_X + ICON_DX, y=y + ICON_DY, w=ICON_SZ, h=ICON_SZ, + src=os.path.join(ASSETS, ICON_PNG[iconkey])), + dict(kind='label', x=ITEM_X + TITLE_DX, y=y + TITLE_DY, w=TITLE_W, h=TITLE_H, + text=title, size=29, weight=S500, color=WHITE, align='left', vtop=True), + ] + + +def clock(t='10:24'): + return dict(kind='label', x=201, y=4, w=78, h=46, text=t, size=29, + weight=S600, color=GRAY, align='left', vtop=True) + + +def title(text, x, w=260): + return dict(kind='label', x=x, y=36, w=w, h=46, text=text, size=31, + weight=S500, color=WHITE, align='left', vtop=True) + + +def back_arrow(x=167): + return dict(kind='image', x=x, y=44, w=18, h=29, src=os.path.join(ASSETS, 'backarrow.png')) + + +def big_button(y, text): + return [ + dict(kind='rect', x=111, y=y, w=258, h=82, bg=PLATE, radius=41), + dict(kind='label', x=182, y=y + 22, w=126, h=40, text=text, size=29, + weight=S500, color=WHITE, align='left', vtop=True), + ] + + +PAGE_HOME = [clock(), title('OS4 Icons', 164, 152), + *item(88, 'ic_help', '快速帮助'), + *item(186, 'ic_replace', '开始替换'), + *item(285, 'ic_about', '关于')] + +_HELP_HEAD = [clock(), back_arrow(167), title('快速帮助', 188, 124)] +_HELP_D1 = dict(kind='label', x=67, y=182, w=348, h=76, + text='检查 app / health 两个分区\n与 47 条负载的完整性。', size=25, + weight=S500, color=GRAY, align='left', vtop=True) +_HELP_I2 = item(264, 'ic_rollback', '回滚修改') +_HELP_D2 = dict(kind='label', x=67, y=355, w=348, h=36, + text='恢复 47 个图标的原始样式。', size=25, + weight=S500, color=GRAY, align='left', vtop=True) +PAGE_HELP = _HELP_HEAD + item(88, 'ic_check', '环境预检查') + [_HELP_D1] + _HELP_I2 + [_HELP_D2] + +HELP_DESC_OK = dict(kind='label', x=67, y=182, w=348, h=76, + text='预检查通过 | 47 条 | 3.72MB\n两个分区都可读。', + size=25, weight=S500, color=OK, align='left', vtop=True) +HELP_DESC_ERR = dict(kind='label', x=67, y=182, w=348, h=76, + text='预检查失败:设备节点不可读\n或分区头不是 ROMFS。', + size=25, weight=S500, color=ERR, align='left', vtop=True) +PAGE_HELP_OK = _HELP_HEAD + item(88, 'ic_check', '环境预检查') + [HELP_DESC_OK] + _HELP_I2 + [_HELP_D2] +PAGE_HELP_ERR = _HELP_HEAD + item(88, 'ic_check', '环境预检查') + [HELP_DESC_ERR] + _HELP_I2 + [_HELP_D2] + +WARN_LABELS = [ + dict(kind='label', x=60, y=93, w=360, h=36, + text='替换图标包之前,请先确认自己', size=25, weight=S500, color=GRAY, + align='left', vtop=True), + dict(kind='label', x=60, y=126, w=360, h=36, + text='的系统版本号为 ', size=25, weight=S500, color=GRAY, align='left', vtop=True), + dict(kind='label', x=244, y=126, w=120, h=36, text='3.112.035', + size=25, weight=S500, color=WHITE, align='left', vtop=True), + dict(kind='label', x=356, y=126, w=40, h=36, text='!', + size=25, weight=S500, color=GRAY, align='left', vtop=True), + dict(kind='label', x=60, y=196, w=360, h=36, text='请确认自己的手表型号为', + size=25, weight=S500, color=GRAY, align='left', vtop=True), + dict(kind='label', x=60, y=229, w=372, h=36, text='Xiaomi Watch S5 eSIM 46mm', + size=25, weight=S500, color=WHITE, align='left', vtop=True), + dict(kind='label', x=427, y=229, w=40, h=36, text='!', + size=25, weight=S500, color=GRAY, align='left', vtop=True), + dict(kind='label', x=60, y=331, w=360, h=36, text='搞机有风险,变砖后果自负。', + size=25, weight=S500, color=GRAY, align='left', vtop=True), +] +PAGE_CONFIRM = [clock(), back_arrow(167), title('开始替换', 188, 124)] + WARN_LABELS + big_button(376, '确认替换') + +LOG_MID = ('> 开始替换\n' + '> app 33 条 / health 14 条\n' + '> 写入 20/47\n' + '> 替换完毕,请重启手表。\n' + '> 若发现问题,请回滚修改。') +PAGE_RUNNING = [clock(), title('正在替换', 178, 124), + dict(kind='label', x=67, y=93, w=348, h=277, text=LOG_MID, size=25, + weight=S500, color=WHITE, align='left', vtop=True), + *big_button(376, '重启手表')] + +PAGE_ABOUT = [ + clock(), back_arrow(198), title('关于', 220, 62), + dict(kind='rect', x=54, y=81, w=373, h=210, bg=PLATE, radius=31), + dict(kind='image', x=200, y=111, w=80, h=80, src=os.path.join(DOCS, 'logo_preview_480.png')), + dict(kind='label', x=170, y=198, w=141, h=40, text='OS4 Icons', + size=29, weight=S500, color=WHITE, align='left', vtop=True), + dict(kind='label', x=216, y=234, w=49, h=28, text='1.0.0', + size=21, weight=S500, color=GRAY, align='left', vtop=True), + *item(300, 'ic_qq', '732681995'), +] + +PAGES = {'home': PAGE_HOME, 'help': PAGE_HELP, 'help_ok': PAGE_HELP_OK, 'help_err': PAGE_HELP_ERR, + 'confirm': PAGE_CONFIRM, 'running': PAGE_RUNNING, 'about': PAGE_ABOUT} diff --git a/s5e-icons/tools/s5e_payload.py b/s5e-icons/tools/s5e_payload.py new file mode 100644 index 0000000..ce17706 --- /dev/null +++ b/s5e-icons/tools/s5e_payload.py @@ -0,0 +1,808 @@ +# -*- coding: utf-8 -*- +"""S5 41mm (q63, OS4) launcher icons -> Xiaomi Watch S5 eSIM (p62lte, 480x480). + +Target slots live in TWO partitions: + vela_app.bin (/dev/app) 34 launcher.bin + vela_health.bin (/dev/health) 15 launcher.bin +All 49 target slots are 144x144 ARGB8888 (82956 bytes) -- so the replacement is +ALWAYS exactly one slot long, which is what keeps the ROMFS absolute offsets valid. + +Sizing convention (measured, not guessed): + the S5 eSIM stock art is inset in the 144 canvas (136x136 at (4,4) for almost + every slot, 138 at (3,3) for launcher_*, 144 full-bleed for a few), while the + OS4 source art is mostly full-bleed 144x144. So each source is cropped to its + painted bbox, scaled to the TARGET slot's own painted size and pasted at the + TARGET slot's origin. Sources that already match are copied without resampling + (bit-exact). + +Outputs + watchface_s5e/payload/payload.bin MAGIC + concat(replace blobs) + watchface_s5e/payload/payload.lua per-record table for main.lua + watchface_s5e/payload_rollback/... same for the stock bytes + _out/s5e_from_os4/vela_app_os4.bin whole-image variants (route C) + _out/s5e_from_os4/vela_health_os4.bin +""" +import os, sys, json, struct, hashlib, shutil +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from romfs import Romfs, img_header +from lvgl_bin import decode_lvgl_bin +import iconpack as ip +from PIL import Image, ImageFilter + +from paths import PROJ, INPUTS, WORK +NEW = os.path.join(INPUTS, 'target') # 目标固件分区(p62lte) +OLD = os.path.join(INPUTS, 'source') # 图标来源分区(q63 = S5 41mm) +WF = PROJ +OUT = os.path.join(WORK, 's5e_from_os4') # manifest + 整分区镜像(产物) + +PART = [('app', 'vela_app.bin', '/dev/app'), + ('health', 'vela_health.bin', '/dev/health')] +MAGIC_PAY = b'\x00S5E-PAYv1\x00\xa5\x5a\xc3\x3c' # 15 B, must stay unique in the .face +MAGIC_RB = b'\x00S5E-RBKv1\x00\x5a\xa5\x3c\xc3' + +# 自制图覆盖:优先级高于固件来源(和 S4 项目同一套做法)。 +# activities : 112x112 的红/绿/青同心环 = 用户要的「跑道」图标 +# voice_aivs : 144x144 的「小爱同学」用户 PNG +OVERRIDE = {k: os.path.join(PROJ, 'assets', 'override', k + '.png') + for k in ('activities', 'voice_aivs')} + +os.makedirs(OUT, exist_ok=True) +os.makedirs(os.path.join(WF, 'payload'), exist_ok=True) +os.makedirs(os.path.join(WF, 'payload_rollback'), exist_ok=True) + + +def ink_bbox(img, thr=16): + a = img.convert('RGBA').getchannel('A') + w, h = a.size + px = a.load() + x0, y0, x1, y1 = w, h, -1, -1 + for y in range(h): + for x in range(w): + if px[x, y] > thr: + if x < x0: x0 = x + if y < y0: y0 = y + if x > x1: x1 = x + if y > y1: y1 = y + return None if x1 < 0 else (x0, y0, x1, y1) + + +def normalize(src_img, slot_bb, canvas): + """crop to painted bbox -> scale to the slot's painted size -> paste at slot origin""" + cw, ch = canvas + if slot_bb is None: + return Image.new('RGBA', (cw, ch), (0, 0, 0, 0)) + sx0, sy0, sx1, sy1 = slot_bb + tw, th = sx1 - sx0 + 1, sy1 - sy0 + 1 + nb = ink_bbox(src_img) + if nb is None: + return Image.new('RGBA', (cw, ch), (0, 0, 0, 0)) + if (nb[2] - nb[0] + 1, nb[3] - nb[1] + 1) == (tw, th) and (nb[0], nb[1]) == (sx0, sy0): + return src_img.convert('RGBA') # already identical geometry + art = src_img.convert('RGBA').crop((nb[0], nb[1], nb[2] + 1, nb[3] + 1)) + if art.size != (tw, th): + up = tw > art.size[0] or th > art.size[1] + art = art.resize((tw, th), Image.LANCZOS) + if up: + art = art.filter(ImageFilter.UnsharpMask(radius=2, percent=70, threshold=3)) + canvas_img = Image.new('RGBA', (cw, ch), (0, 0, 0, 0)) + canvas_img.paste(art, (sx0, sy0)) + return canvas_img + + +def normalize_fit(src_img, target_bb, canvas): + """等比缩放源实心区域到能放进 target_bb 的最大尺寸并居中(不拉伸)。""" + cw, ch = canvas + cv = Image.new('RGBA', (cw, ch), (0, 0, 0, 0)) + if target_bb is None: + return cv + nb = ink_bbox(src_img) + if nb is None: + return cv + tw, th = target_bb[2] - target_bb[0] + 1, target_bb[3] - target_bb[1] + 1 + art = src_img.convert('RGBA').crop((nb[0], nb[1], nb[2] + 1, nb[3] + 1)) + s = min(tw / art.size[0], th / art.size[1]) + nw, nh = max(1, round(art.size[0] * s)), max(1, round(art.size[1] * s)) + if (nw, nh) != art.size: + up = s > 1.0 + art = art.resize((nw, nh), Image.LANCZOS) + if up: + art = art.filter(ImageFilter.UnsharpMask(radius=2, percent=70, threshold=3)) + cv.paste(art, (target_bb[0] + (tw - nw) // 2, target_bb[1] + (th - nh) // 2)) + return cv + + +# ------------------------------------------------------------------ load slots +slots = [] # ordered records +ROM_N = {} +for tag, fn, dev in PART: + r = Romfs(os.path.join(NEW, fn)) + ROM_N[tag] = r + tg = ip.targets(r) + for name in sorted(tg): + ino = tg[name] + blob = r.read(ino) + h, img = decode_lvgl_bin(blob) + slots.append(dict(tag=tag, dev=dev, image=fn, name=name, ino=ino, blob=blob, + h=h, img=img, bb=ink_bbox(img) if img is not None else None)) + +src = {} +ROM_O = {} +for fn in ['vela_app.bin', 'vela_health.bin']: + r = Romfs(os.path.join(OLD, fn)) + ROM_O['app' if fn == 'vela_app.bin' else 'health'] = r + for name, ino in ip.targets(r).items(): + if name in src: continue + h, img = decode_lvgl_bin(r.read(ino)) + src[name] = (h, img, r.read(ino)) + +print('target slots %d source icons %d' % (len(slots), len(src))) +rows, newblobs, stock_of, skipped = [], {}, {}, [] +for s in slots: + h = s['h'] + if h is None: + skipped.append((s['name'], 'target not decodable')); continue + if s['name'] not in src: + skipped.append((s['name'], 'no OS4 source')); continue + sh, simg, sblob = src[s['name']] + if simg is None: + skipped.append((s['name'], 'source not decodable')); continue + src_from = 'q63 固件 %s' % s['image'] + ov = OVERRIDE.get(s['name']) + if ov and os.path.exists(ov): + # 自制图覆盖:裁到实心 bbox -> 缩放到目标槽位的画法(136x136)-> 居中粘回 (4,4) + simg = Image.open(ov).convert('RGBA') + sblob = b'' + src_from = '自制图 %s (%dx%d)' % (os.path.basename(ov), simg.size[0], simg.size[1]) + print(' [override] %-14s <- %s %dx%d' % (s['name'], os.path.basename(ov), + simg.size[0], simg.size[1])) + out_img = normalize(simg, s['bb'], (h['w'], h['h'])) + blob = ip.encode(out_img, h['cf'], h['w'], h['h'], h['flags']) + if len(blob) != s['ino'].size: + skipped.append((s['name'], 'length %d != slot %d' % (len(blob), s['ino'].size))); continue + exact = (blob == sblob) + newblobs[s['name']] = blob + stock_of[s['name']] = s['blob'] + rows.append(dict(name=s['name'], tag=s['tag'], dev=s['tag'], node=s['dev'], image=s['image'], + offset=s['ino'].data_off, length=s['ino'].size, + dim='%dx%d' % (h['w'], h['h']), cf=ip.FMT.get(h['cf'], hex(h['cf'])), + stock_ink=s['bb'], source_from=src_from, + identical_to_stock=exact, + sha256=hashlib.sha256(blob).hexdigest(), + rb_sha256=hashlib.sha256(s['blob']).hexdigest())) + +print('%-24s %-7s %-12s %-14s %s' % ('name', 'part', 'offset', 'slot', 'stock ink -> new')) +for s in slots: + r = next((x for x in rows if x['name'] == s['name']), None) + if r: + print('%-24s %-7s 0x%08x %-8d %-14s' % (r['name'], r['tag'], r['offset'], r['length'], + str(r['stock_ink']) + (' (identical)' if r['identical_to_stock'] else ''))) +for n, why in skipped: + print('%-24s SKIP %s' % (n, why)) +print('\nreplace %d skip %d' % (len(rows), len(skipped))) + + +# ------------------------------------------------- 系统 UI 素材:从 S5 41mm(q63) 搬过来 +# 只搬「独立字形图标」(锁屏 / 通知中心 / 设置 / 米家)。控制中心的开关磁贴**故意没搬**: +# q63 的控制中心是白色主题(白盘 + 深色字形,运行时着色),S5 eSIM 是蓝色主题(预着色成品), +# 只搬那 4 张会变成「白盘混在蓝盘里」;要搬就得整套 ~40 张一起搬。 +SYSICONS = [ + ('app', 'system/icon/lock_icon.bin', '锁屏上的锁'), + ('app', 'common/icon/system_lock.bin', '状态栏系统锁'), + ('app', 'common/icon/lock.bin', '通用锁'), + ('app', 'system/icon/unlock.bin', '解锁提示页'), + ('app', 'common/icon/quiet_mode.bin', '免打扰/静音小图标'), + ('app', 'common/icon/unread_msg.bin', '未读消息红点'), + ('app', 'notifications/default.bin', '通知默认大图标'), + ('app', 'notifications/default_small.bin', '通知默认小图标'), + ('app', 'notifications/no_message.bin', '通知中心空状态'), + ('app', 'settings/icon/notify.bin', '设置-通知'), + ('app', 'settings/disturb/confirm.bin', '免打扰确认按钮'), + ('app', 'esimsms/icon/esim_sms_reminder.bin', '短信提醒'), + ('app', 'esimsms/icon/sms_no_read.bin', '未读短信'), + ('app', 'esimsms/icon/sms_reminder_watch.bin', '手表短信提醒'), + ('app', 'phone/icon/send_sms_new.bin', '新建短信'), + ('app', 'mijia/enable_donot_disturb.bin', '米家 免打扰开'), + ('app', 'mijia/disable_donot_disturb.bin', '米家 免打扰关'), + ('health', 'activities/icon/notification_stand.bin', '久坐提醒'), + ('health', 'activities/icon/notification_vigor.bin', '活力提醒'), + # 压力 App 的小表情(4 档情绪 × 32/48/96,96 那三张两边本来就一样,会被自动跳过) + ('health', 'pressure/icon/presure_relax.bin', '压力表情-放松 48'), + ('health', 'pressure/icon/presure_mild.bin', '压力表情-轻度 48'), + ('health', 'pressure/icon/presure_mid.bin', '压力表情-中度 48'), + ('health', 'pressure/icon/presure_severe.bin', '压力表情-重度 48'), + ('health', 'pressure/icon/presure_relax32.bin', '压力表情-放松 32'), + ('health', 'pressure/icon/presure_mild32.bin', '压力表情-轻度 32'), + ('health', 'pressure/icon/presure_mid32.bin', '压力表情-中度 32'), + ('health', 'pressure/icon/presure_severe32.bin', '压力表情-重度 32'), + ('health', 'pressure/icon/presure_relax96.bin', '压力表情-放松 96'), + ('health', 'pressure/icon/presure_mild96.bin', '压力表情-轻度 96'), + ('health', 'pressure/icon/presure_mid96.bin', '压力表情-中度 96'), + ('health', 'pressure/icon/presure_severe96.bin', '压力表情-重度 96'), + ('health', 'pressure/icon/presure_icon.bin', '压力 App 小图标'), + ('health', 'pressure/icon/press_grade1.bin', '压力等级徽标 1'), + ('health', 'pressure/icon/press_grade2.bin', '压力等级徽标 2'), + ('health', 'pressure/icon/press_grade3.bin', '压力等级徽标 3'), + ('health', 'pressure/icon/press_grade4.bin', '压力等级徽标 4'), + ('health', 'pressure/icon/pressure_eomji_bg.bin', '表情底版'), + ('health', 'pressure/icon/pressure_eomji_widbg.bin', '表情底版(小)'), + # 没有搬(体积大 / 不是表情):pressure_bg、success_bg、success_arc、 + # press_widget_bg*、press_chart_bg、pressure_widget_pre_img*、reminder、measure/* +] + + +# 整目录一起搬(q63 有同名文件的才搬):控制中心的开关磁贴 +SYSICONS_DIRS = ['control_center/icon/'] + + +def find_inode(romfs, path): + for p, i in romfs.entries(): + if p == path and i.type != 1: + return i + return None + + +print('\n--- 系统 UI 素材(锁屏 / 勿扰 / 通知 / 控制中心):q63 -> S5 eSIM ---') +_jobs = [(d, p, n) for d, p, n in SYSICONS] +_seen = {p for _, p, _ in _jobs} +for _dev in ROM_N: + for _p, _i in ROM_N[_dev].entries(): + if _i.type == 1 or _p in _seen: + continue + if any(_p.startswith(_d) for _d in SYSICONS_DIRS): + _jobs.append((_dev, _p, '控制中心磁贴')) + _seen.add(_p) +for dev, path, note in _jobs: + ti = find_inode(ROM_N[dev], path) + oi = find_inode(ROM_O[dev], path) + if ti is None: + skipped.append((path, '目标固件没有这个文件')); continue + if oi is None: + skipped.append((path, 'q63 没有这个文件')); continue + tgt_blob = ROM_N[dev].read(ti) + src_blob = ROM_O[dev].read(oi) + if src_blob == tgt_blob: + print(' %-52s 两边字节完全相同,跳过' % path) + continue + th = img_header(tgt_blob) + sh = img_header(src_blob) + if len(src_blob) == len(tgt_blob): + new, how = src_blob, 'copy(等长)' + elif th and sh and th['cf'] == sh['cf']: + timg = decode_lvgl_bin(tgt_blob)[1] + simg = decode_lvgl_bin(src_blob)[1] + if timg is None or simg is None: + skipped.append((path, '无法解码')); continue + pal = src_blob[12:12 + 1024] if sh['cf'] == ip.CF_I8 else None + out = normalize_fit(simg, ink_bbox(timg), (th['w'], th['h'])) + new = ip.encode(out, th['cf'], th['w'], th['h'], th['flags'], palette=pal) + how = 'rescale %dx%d->%dx%d' % (sh['w'], sh['h'], th['w'], th['h']) + else: + skipped.append((path, '长度/格式不支持')); continue + if len(new) != len(tgt_blob): + skipped.append((path, '编码 %d != 槽位 %d' % (len(new), len(tgt_blob)))); continue + name = 'sys:' + path + newblobs[name] = new + stock_of[name] = tgt_blob + rows.append(dict(name=name, kind='sysicon', path=path, note=note, how=how, + tag=dev, dev=dev, node='/dev/' + dev, image=path, + offset=ti.data_off, length=ti.size, + dim=('%dx%d' % (th['w'], th['h'])) if th else '?', + cf=ip.FMT.get(th['cf'], hex(th['cf'])) if th else '?', + stock_ink=ink_bbox(decode_lvgl_bin(tgt_blob)[1]) if th and decode_lvgl_bin(tgt_blob)[1] else None, + source_from='q63 固件 / ' + path, identical_to_stock=False, + sha256=hashlib.sha256(new).hexdigest(), + rb_sha256=hashlib.sha256(tgt_blob).hexdigest())) + print(' %-52s %-22s %s' % (path, how, note)) + + +# ------------------------------------------------- 对讲机素材(用户提供的整套 PNG) +# walkie_talkie/ 下的槽位几乎**全是 I8+RLE 压缩**的:不能像图标那样直接编码(长度会变), +# 必须走 ip.encode_into_slot():量化 -> RLE -> 用 ctrl=0 空指令补齐到原 csize, +# 这样字节数与原槽位完全相等,ROMFS 后面所有偏移一个字节都不动。 +WALKIE_SRC = os.path.join(PROJ, 'assets', 'walkie_talkie') +# 对讲机素材:**默认不打包**(用户 2026-10-02 决定不改)。ICON_WALKIE=1 可打开。 +DO_WALKIE = os.environ.get('ICON_WALKIE') == '1' +print('\n--- 对讲机素材 walkie_talkie(用户 PNG)-> S5 eSIM ---') +walkie_n = 0 +if not DO_WALKIE: + print(' 按设置跳过这一组(ICON_WALKIE=1 可打开)') +elif not os.path.isdir(WALKIE_SRC): + print(' 没有 %s,跳过这一组' % WALKIE_SRC) +else: + rels = [] + for root, _, files in os.walk(WALKIE_SRC): + for f in files: + if f.lower().endswith('.png'): + rels.append(os.path.relpath(os.path.join(root, f), WALKIE_SRC).replace('\\', '/')) + for rel in sorted(rels): + path = 'walkie_talkie/' + rel[:-4] + '.bin' + dev = 'app' + ti = find_inode(ROM_N[dev], path) + if ti is None: + skipped.append((path, '目标固件没有这个槽位')); continue + tgt_blob = ROM_N[dev].read(ti) + img = Image.open(os.path.join(WALKIE_SRC, rel)) + new, how = ip.encode_into_slot(img, tgt_blob, ti.size) + if new is None: + skipped.append((path, how)); continue + if new == tgt_blob: + print(' %-52s 与现有一致,跳过' % path); continue + th = img_header(tgt_blob) + name = 'walk:' + path + newblobs[name] = new + stock_of[name] = tgt_blob + rows.append(dict(name=name, kind='walkie', path=path, note='对讲机素材', how=how, + tag=dev, dev=dev, node='/dev/' + dev, image=path, + offset=ti.data_off, length=ti.size, + dim=('%dx%d' % (th['w'], th['h'])) if th else '?', + cf=ip.FMT.get(th['cf'], hex(th['cf'])) if th else '?', + stock_ink=None, source_from='用户 PNG assets/walkie_talkie/' + rel, + identical_to_stock=False, + sha256=hashlib.sha256(new).hexdigest(), + rb_sha256=hashlib.sha256(tgt_blob).hexdigest())) + walkie_n += 1 + if walkie_n <= 6 or walkie_n % 40 == 0: + print(' %-52s %-18s %d B' % (path, how, ti.size)) + print(' 对讲机素材写入 %d 条' % walkie_n) + + +# ------------------------------------------------- 控制中心顶部状态区(q63 -> S5 eSIM) +# q63 的顶栏素材在 control_center/bar/:wifi 4 档 / 蜂窝信号 5 档 / 3G·4G 文字 / +# 蓝牙连接·断开 / 飞行模式 / 定位。本机同名槽位在 control_center/icon/(48x48 I8)。 +# 尺寸不同的(q63 那几张 36x36)按**实心范围**归一化(normalize(simg, ink_bbox(timg), ...))。 +CC_TOP = [ + ('control_center/icon/network_4g.bin', 'control_center/bar/network_4g.bin', '4G 文字'), + ('control_center/icon/network_3g.bin', 'control_center/bar/network_3g.bin', '3G 文字'), + ('control_center/icon/signal_strength.bin', 'control_center/bar/signal_strength0.bin', '蜂窝信号 0'), + ('control_center/icon/signal_strength1.bin', 'control_center/bar/signal_strength1.bin', '蜂窝信号 1'), + ('control_center/icon/signal_strength2.bin', 'control_center/bar/signal_strength2.bin', '蜂窝信号 2'), + ('control_center/icon/signal_strength3.bin', 'control_center/bar/signal_strength3.bin', '蜂窝信号 3'), + ('control_center/icon/signal_strength4.bin', 'control_center/bar/signal_strength4.bin', '蜂窝信号 4'), + ('control_center/icon/air_mode_conn.bin', 'control_center/bar/air.bin', '飞行模式'), + ('control_center/icon/bt_connect.bin', 'control_center/bar/connect.bin', '蓝牙已连接手机'), + ('control_center/icon/bt_connect_g.bin', 'control_center/bar/connect.bin', '蓝牙已连接手机(g)'), + ('control_center/icon/bt_disconnect.bin', 'control_center/bar/disconnect.bin', '蓝牙未连接'), + ('control_center/icon/location.bin', 'control_center/bar/local.bin', '定位'), + ('control_center/icon/wifi_level_0.bin', 'control_center/bar/wifi_level_0.bin', 'WiFi 0'), + ('control_center/icon/wifi_level_1.bin', 'control_center/bar/wifi_level_1.bin', 'WiFi 1'), + ('control_center/icon/wifi_level_2.bin', 'control_center/bar/wifi_level_2.bin', 'WiFi 2'), + ('control_center/icon/wifi_level_3.bin', 'control_center/bar/wifi_level_3.bin', 'WiFi 3'), +] +# 手机电量(本机 phone/icon/battery_level_*,64x22):q63 同名但 64x32 +for _n in range(0, 11): + CC_TOP.append(('phone/icon/battery_level_%d.bin' % _n, + 'phone/icon/battery_level_%d.bin' % _n, '手机电量 %d' % _n)) + +print('\n--- 控制中心顶部状态区(q63 control_center/bar -> S5 eSIM)---') +cc_top_n = 0 +for tpath, spath, note in CC_TOP: + ti = find_inode(ROM_N['app'], tpath) + if ti is None: + ti = find_inode(ROM_N['health'], tpath) + dev = 'health' + else: + dev = 'app' + if ti is None: + skipped.append((tpath, '本机没有这个槽位')); continue + oi = find_inode(ROM_O[dev], spath) + if oi is None: + skipped.append((spath, 'q63 没有这个素材')); continue + tgt_blob = ROM_N[dev].read(ti) + src_blob = ROM_O[dev].read(oi) + th = img_header(tgt_blob) + sh = img_header(src_blob) + timg = decode_lvgl_bin(tgt_blob)[1] + simg = decode_lvgl_bin(src_blob)[1] + if timg is None or simg is None: + skipped.append((tpath, '解码失败')); continue + out = normalize(simg, ink_bbox(timg), (th['w'], th['h'])) + pal = src_blob[12:12 + 1024] if sh['cf'] == ip.CF_I8 else None + new = ip.encode(out, th['cf'], th['w'], th['h'], th['flags'], palette=pal) + if len(new) != len(tgt_blob): + failed.append((tpath, '编码 %d != 槽位 %d' % (len(new), len(tgt_blob)))); continue + if new == tgt_blob: + skipped.append((tpath, '两边已经一致')); continue + # 与 SYSICONS 组同名 -> 同一槽位只保留一条记录(这里覆盖那边的旧风格版本) + name = 'sys:' + tpath + rows[:] = [r for r in rows if r['name'] != name] + newblobs[name] = new + stock_of[name] = tgt_blob + rows.append(dict(name=name, kind='sysicon', path=tpath, note=note, how='归一化(%s)' % note, + tag=dev, dev=dev, node='/dev/' + dev, image=tpath, + offset=ti.data_off, length=ti.size, + dim='%dx%d' % (th['w'], th['h']), + cf=ip.FMT.get(th['cf'], hex(th['cf'])), + stock_ink=ink_bbox(timg), source_from='q63 ' + spath, + identical_to_stock=False, + sha256=hashlib.sha256(new).hexdigest(), + rb_sha256=hashlib.sha256(tgt_blob).hexdigest())) + cc_top_n += 1 + print(' %-46s <- %-40s %s' % (tpath, spath, note)) +print(' 顶栏状态区写入 %d 条' % cc_top_n) + + +# ------------------------------------------------- 米家 / 融合中心(控制中心里的米家设备图标) +# q63 的 fusion_center/** 与 mijia/** 里**同名路径**本机也有的就移植;本机还有一份 +# fusion_center/gauss/**(同名的一并写)。画布尺寸两边常常不同(q63 116/64 vs 本机 96/128), +# 按**实心范围**对齐并转成本机槽位的格式。 +PLATE_KEEP = ('headset',) # 只有这种「裸图案」槽位保留底版 + 图案归位 +# 控制中心的米家/融合中心那组:**默认不打包**(用户 2026-10-02 决定不改)。ICON_MIJIA=1 可打开。 +DO_MIJIA = os.environ.get('ICON_MIJIA') == '1' +# q63 的「柔光玻璃」底盘(干净的深色玻璃圆盘 + 发丝高光环) +_glass = None +_g = find_inode(ROM_O['app'], 'fusion_center/fg.bin') +if _g is not None: + _glass = decode_lvgl_bin(ROM_O['app'].read(_g))[1] + if _glass is not None: + _glass = _glass.convert('RGBA') + print('柔光玻璃盘: q63 fusion_center/fg.bin %dx%d' % _glass.size) +MIJIA_PREFIX = ('fusion_center/', 'mijia/') +def plate_glyph_bbox(img, tol=26, min_plate=0.30): + """本机图标是「底版 + 图案」时,返回底版上图案的 bbox(底版色 = 出现最多的颜色)。""" + rgba = img.convert('RGBA') + w, h = rgba.size + px = rgba.load() + cnt = {} + for y in range(h): + for x in range(w): + p = px[x, y] + if p[3] > 16: + cnt[p] = cnt.get(p, 0) + 1 + if not cnt: + return None + plate, n = max(cnt.items(), key=lambda kv: kv[1]) + if n >= min_plate * w * h: + xs, ys = [], [] + for y in range(h): + for x in range(w): + p = px[x, y] + if p[3] > 16 and (abs(p[0] - plate[0]) + abs(p[1] - plate[1]) + abs(p[2] - plate[2])) > tol: + xs.append(x); ys.append(y) + if xs: + return (min(xs), min(ys), max(xs), max(ys)) + # 兜底:底版是渐变(没有主导纯色)时,用**亮度**找亮图案 + lum = {} + mx = 0 + for y in range(h): + for x in range(w): + p = px[x, y] + if p[3] > 16: + v = 0.299 * p[0] + 0.587 * p[1] + 0.114 * p[2] + lum[(x, y)] = v + mx = max(mx, v) + if not lum or mx <= 0: + return None + thr = 0.62 * mx + xs = [x for (x, y), v in lum.items() if v >= thr] + ys = [y for (x, y), v in lum.items() if v >= thr] + if not xs: + return None + bb = (min(xs), min(ys), max(xs), max(ys)) + if (bb[2] - bb[0] + 1) * (bb[3] - bb[1] + 1) > 0.92 * w * h: + return None + return bb + + +def ink_ratio(img): + bb = ink_bbox(img) + if bb is None: + return 0.0 + w, h = img.size + return max((bb[2] - bb[0] + 1) / w, (bb[3] - bb[1] + 1) / h) + + +def normalize_fit(src_img, target_bb, canvas): + """等比缩放源实心区域到能放进 target_bb 的最大尺寸并居中(不拉伸)。""" + cw, ch = canvas + cv = Image.new('RGBA', (cw, ch), (0, 0, 0, 0)) + if target_bb is None: + return cv + nb = ink_bbox(src_img) + if nb is None: + return cv + tw, th = target_bb[2] - target_bb[0] + 1, target_bb[3] - target_bb[1] + 1 + art = src_img.convert('RGBA').crop((nb[0], nb[1], nb[2] + 1, nb[3] + 1)) + s = min(tw / art.size[0], th / art.size[1]) + nw, nh = max(1, round(art.size[0] * s)), max(1, round(art.size[1] * s)) + if (nw, nh) != art.size: + up = s > 1.0 + art = art.resize((nw, nh), Image.LANCZOS) + if up: + art = art.filter(ImageFilter.UnsharpMask(radius=2, percent=70, threshold=3)) + cv.paste(art, (target_bb[0] + (tw - nw) // 2, target_bb[1] + (th - nh) // 2)) + return cv + + + +def overlay_glyph_in_place(tgt_blob, th, src_img, target_bb): + """在**索引层**把 q63 的裸图案画进本机 I8 图标:底版像素一个比特都不动。 + + 底版是抖动/渐变,重新量化(用谁的调色板都一样)会出噪点或白环;只有直接改索引数组 + 底版才保持原样。图案像素按最近色映射到**本机自己的调色板**(本机图标里本来就有白色)。 + """ + w, h, cf = th['w'], th['h'], th['cf'] + if cf != ip.CF_I8 or (th['flags'] & 0x08): + return None + if len(tgt_blob) != 12 + 1024 + w * h: + return None + pal_b = tgt_blob[12:12 + 1024] + pal = [(pal_b[i * 4 + 2], pal_b[i * 4 + 1], pal_b[i * 4 + 0], pal_b[i * 4 + 3]) for i in range(256)] + idx = bytearray(tgt_blob[12 + 1024:12 + 1024 + w * h]) + nb = ink_bbox(src_img) + if nb is None or target_bb is None: + return None + tw, thh = target_bb[2] - target_bb[0] + 1, target_bb[3] - target_bb[1] + 1 + art = src_img.convert('RGBA').crop((nb[0], nb[1], nb[2] + 1, nb[3] + 1)) + s = min(tw / art.size[0], thh / art.size[1]) + art = art.resize((max(1, round(art.size[0] * s)), max(1, round(art.size[1] * s))), Image.LANCZOS) + ox = target_bb[0] + (tw - art.width) // 2 + oy = target_bb[1] + (thh - art.height) // 2 + cache = {} + for y in range(art.height): + for x in range(art.width): + r, g, b, a = art.getpixel((x, y)) + if a < 24: + continue + key = (r, g, b, a) + ci = cache.get(key) + if ci is None: + best, bd = 0, 1 << 30 + for k in range(256): + pr, pg, pb, pa = pal[k] + d = (pr - r) ** 2 + (pg - g) ** 2 + (pb - b) ** 2 + 2 * (pa - a) ** 2 + if d < bd: + bd, best = d, k + cache[key] = ci = best + ix, iy = ox + x, oy + y + if 0 <= ix < w and 0 <= iy < h: + idx[iy * w + ix] = ci + return tgt_blob[:12 + 1024] + bytes(idx) + + +def plate_share(img, gbb): + """底版像素占实心像素的比例 —— 确认这张图**真的有底版**(不是纯图案)。""" + if gbb is None: + return 0.0 + a = img.convert('RGBA').getchannel('A') + w, h = a.size + px = a.load() + ink = tot = 0 + for y in range(h): + for x in range(w): + if px[x, y] > 16: + tot += 1 + if not (gbb[0] <= x <= gbb[2] and gbb[1] <= y <= gbb[3]): + ink += 1 + return (ink / tot) if tot else 0.0 + + +print('\n--- 米家 / 融合中心素材(q63 -> S5 eSIM)%s ---' % ('' if DO_MIJIA else ':已按设置跳过')) +mijia_n = 0 +_mijia_srcs = [] +if DO_MIJIA: + for _dev0 in ('app', 'health'): + for _p, _i in ROM_O[_dev0].entries(): + if _i.type == 2 and _p.startswith(MIJIA_PREFIX) and not _p.endswith('launcher.bin'): + _mijia_srcs.append((_p, _dev0)) +for _q, _srcdev in sorted(set(_mijia_srcs)): + cands = [_q] + if _q.startswith('fusion_center/icon/'): + cands.append(_q.replace('fusion_center/icon/', 'fusion_center/gauss/')) + for tpath in cands: + dev = 'app' + ti = find_inode(ROM_N[dev], tpath) + if ti is None: + ti = find_inode(ROM_N['health'], tpath) + dev = 'health' + if ti is None: + if tpath == _q: + skipped.append((tpath, '本机没有这个槽位')) + continue + oi = find_inode(ROM_O[_srcdev], _q) + if oi is None: + skipped.append((_q, 'q63 没有这个素材')); continue + tgt_blob = ROM_N[dev].read(ti) + src_blob = ROM_O[_srcdev].read(oi) + th, sh = img_header(tgt_blob), img_header(src_blob) + if th is None or sh is None: + skipped.append((tpath, '头部异常')); continue + simg = decode_lvgl_bin(src_blob)[1] + if simg is None: + skipped.append((tpath, 'q63 素材解码失败(cf=0x%02x)' % sh['cf'])); continue + if (th['flags'] & 0x08) or th['cf'] not in (ip.CF_I8, ip.CF_ARGB8888): + new, how = ip.encode_into_slot(simg, tgt_blob, ti.size) + else: + timg = decode_lvgl_bin(tgt_blob)[1] + if timg is None: + skipped.append((tpath, '本机图解码失败(cf=0x%02x)' % th['cf'])); continue + gbb = plate_glyph_bbox(timg) + pal = None + if (gbb is not None and ink_ratio(simg) < 0.90 + and plate_share(timg, gbb) >= 0.45 + and tpath.rsplit('/', 1)[-1][:-4] in PLATE_KEEP): + # 底版原样保留(索引层),只把 q63 的裸图案画进去 + new = overlay_glyph_in_place(tgt_blob, th, simg, gbb) + if new is None: + skipped.append((tpath, '索引层合成不可用')); continue + out = None + how = '底版原样+图案入索引 %dx%d->%dx%d' % (sh['w'], sh['h'], th['w'], th['h']) + else: + out = normalize_fit(simg, ink_bbox(timg), (th['w'], th['h'])) + how = '归一化 %dx%d->%dx%d' % (sh['w'], sh['h'], th['w'], th['h']) + pal = src_blob[12:12 + 1024] if sh['cf'] == ip.CF_I8 else None + if out is not None: + new = ip.encode(out, th['cf'], th['w'], th['h'], th['flags'], palette=pal) + if new is None or len(new) != ti.size: + skipped.append((tpath, '编码失败/长度不符')); continue + if new == tgt_blob: + skipped.append((tpath, '两边已经一致')); continue + name = 'sys:' + tpath + rows[:] = [r for r in rows if r['name'] != name] + newblobs[name] = new + stock_of[name] = tgt_blob + rows.append(dict(name=name, kind='sysicon', path=tpath, note='米家/融合中心', how=how, + tag=dev, dev=dev, node='/dev/' + dev, image=tpath, + offset=ti.data_off, length=ti.size, + dim='%dx%d' % (th['w'], th['h']), + cf=ip.FMT.get(th['cf'], hex(th['cf'])), + stock_ink=ink_bbox(timg) if 'timg' in dir() and timg else None, + source_from='q63 ' + _q, identical_to_stock=False, + sha256=hashlib.sha256(new).hexdigest(), + rb_sha256=hashlib.sha256(tgt_blob).hexdigest())) + mijia_n += 1 + if mijia_n <= 8 or mijia_n % 20 == 0: + print(' %-52s <- %-34s %s' % (tpath, _q, how)) +print(' 米家/融合中心写入 %d 条' % mijia_n) + + +# ------------------------------------------------- 焦点通知素材(q63 notifications/** -> 本机) +PORT_PREFIXES = ('notifications/', 'phone/', 'recorder/', 'timer/', 'todolist/') +# 用户指定:这两张保持原厂不换 +NOTIF_SKIP = ('notifications/dotted_line.bin', 'notifications/track.bin', + # 用户指定:录音机这 3 张不换 + 'recorder/mark_opt.bin', 'recorder/menu.bin', 'recorder/confirm.bin', + # 用户指定:拨号列表这 3 张不换 + 'phone/icon/list_icon_keypad.bin', 'phone/icon/list_icon_call_log.bin', + 'phone/icon/list_icon_contact.bin', + # 用户指定:组件预览底图这 2 张不换 + 'phone/widget/phone_widget.bin', 'phone/widget/pre_phone_widget.bin') +# 应用图标归 launcher 组管,这里跳过避免重复写同一槽位 +PORT_SKIP_TAIL = ('launcher.bin', 'launcher') +print('\n--- 通知/电话/录音机素材(q63 同名移植 -> S5 eSIM)---') +notif_n = 0 +for _q, _srcdev in sorted((p, 'app') for p, i in ROM_O['app'].entries() + if i.type == 2 and p.startswith(PORT_PREFIXES)): + if _q in NOTIF_SKIP: + skipped.append((_q, '按约定保持原厂')); continue + if _q.rsplit('/', 1)[-1].endswith('launcher.bin'): + continue # 应用图标归 launcher 组管 + dev = 'app' + ti = find_inode(ROM_N[dev], _q) + if ti is None: + ti = find_inode(ROM_N['health'], _q) + dev = 'health' + if ti is None: + skipped.append((_q, '本机没有这个槽位')); continue + oi = find_inode(ROM_O[_srcdev], _q) + if oi is None: + skipped.append((_q, 'q63 没有这个素材')); continue + tgt_blob = ROM_N[dev].read(ti) + src_blob = ROM_O[_srcdev].read(oi) + th, sh = img_header(tgt_blob), img_header(src_blob) + if th is None or sh is None: + skipped.append((_q, '头部异常')); continue + simg = decode_lvgl_bin(src_blob)[1] + if simg is None: + skipped.append((_q, 'q63 素材解码失败(cf=0x%02x)' % sh['cf'])); continue + if (th['flags'] & 0x08) or th['cf'] not in (ip.CF_I8, ip.CF_ARGB8888): + new, how = ip.encode_into_slot(simg, tgt_blob, ti.size) + if new is None: + skipped.append((_q, how)); continue + else: + timg = decode_lvgl_bin(tgt_blob)[1] + if timg is None: + skipped.append((_q, '本机图解码失败(cf=0x%02x)' % th['cf'])); continue + out = normalize_fit(simg, ink_bbox(timg), (th['w'], th['h'])) + pal = src_blob[12:12 + 1024] if sh['cf'] == ip.CF_I8 else None + new = ip.encode(out, th['cf'], th['w'], th['h'], th['flags'], palette=pal) + how = '归一化 %dx%d->%dx%d' % (sh['w'], sh['h'], th['w'], th['h']) + if len(new) != ti.size: + skipped.append((_q, '编码 %d != 槽位 %d' % (len(new), ti.size))); continue + if new == tgt_blob: + skipped.append((_q, '两边已经一致')); continue + name = 'sys:' + _q + rows[:] = [r for r in rows if r['name'] != name] + newblobs[name] = new + stock_of[name] = tgt_blob + rows.append(dict(name=name, kind='sysicon', path=_q, note='焦点通知', how=how, + tag=dev, dev=dev, node='/dev/' + dev, image=_q, + offset=ti.data_off, length=ti.size, + dim='%dx%d' % (th['w'], th['h']), + cf=ip.FMT.get(th['cf'], hex(th['cf'])), + stock_ink=ink_bbox(timg) if 'timg' in dir() and timg else None, + source_from='q63 ' + _q, identical_to_stock=False, + sha256=hashlib.sha256(new).hexdigest(), + rb_sha256=hashlib.sha256(tgt_blob).hexdigest())) + notif_n += 1 + print(' %-46s %s' % (_q, how)) +print(' 通知/电话/录音机写入 %d 条' % notif_n) + + +# ------------------------------------------------------------------ payload files +pay = MAGIC_PAY + b''.join(newblobs[r['name']] for r in rows) +rb = MAGIC_RB + b''.join(stock_of[r['name']] for r in rows) + +open(os.path.join(WF, 'payload', 'payload.bin'), 'wb').write(pay) +open(os.path.join(WF, 'payload_rollback', 'rollback.bin'), 'wb').write(rb) + +def lua_table(recs, var): + lines = ['-- %s: {设备, 分区内偏移, 长度, payload 文件内相对偏移} —— 由 _tools/s5e_payload.py 生成' % var, + 'local %s = {' % var] + rel = 0 + for r in recs: + lines.append(' { "%s", 0x%08x, %d, %d },' % (r['dev'], r['offset'], r['length'], rel)) + rel += len(stock_of[r['name']]) + lines.append('}') + return '\n'.join(lines) + '\n' + +open(os.path.join(WF, 'payload', 'payload.lua'), 'w', encoding='utf-8', newline='\n').write( + lua_table(rows, 'PAY')) +open(os.path.join(WF, 'payload_rollback', 'rollback.lua'), 'w', encoding='utf-8', newline='\n').write( + lua_table(rows, 'RB')) + +json.dump(dict(target='Xiaomi Watch S5 eSIM 46mm (p62lte) v3.112.035', + source='Xiaomi Watch S5 41mm (q63) v4.101.020 launcher icons', + device_nodes={'app': '/dev/app', 'health': '/dev/health'}, + payload_file='payload.bin', payload_magic=MAGIC_PAY.hex(), + rollback_file='rollback.bin', rollback_magic=MAGIC_RB.hex(), + count=len(rows), records=rows, + skipped=[dict(name=n, reason=w) for n, w in skipped]), + open(os.path.join(OUT, 'manifest.json'), 'w', encoding='utf-8'), indent=1, ensure_ascii=False) + +# ------------------------------------------------------------------ patched whole images +for tag, fn, dev in PART: + srcp = os.path.join(NEW, fn) + dstp = os.path.join(OUT, fn.replace('.bin', '_os4.bin')) + shutil.copyfile(srcp, dstp) + with open(dstp, 'r+b') as f: + for r in rows: + if r['tag'] == tag: + f.seek(r['offset']) + f.write(newblobs[r['name']]) + a = [(p, i.off, i.next, i.spec, i.size, i.cksum) for p, i in Romfs(srcp).entries()] + b = [(p, i.off, i.next, i.spec, i.size, i.cksum) for p, i in Romfs(dstp).entries()] + r1, r2 = Romfs(srcp), Romfs(dstp) + i1 = {p: i for p, i in r1.entries()} + i2 = {p: i for p, i in r2.entries()} + changed = sorted(p for p in i1 if r1.read(i1[p]) != r2.read(i2[p])) + + # 期望变化的文件 = 所有 new != stock 的记录(launcher 图标 + 系统 UI 素材) + mine = [r for r in rows if r['tag'] == tag and newblobs[r['name']] != stock_of[r['name']]] + sys_paths = {r['path'] for r in mine if r.get('kind') in ('sysicon', 'walkie')} + unexpected = [p for p in changed + if not (p.rsplit('/', 1)[-1].endswith('launcher.bin') or p in sys_paths)] + print('\n%s -> %s' % (fn, os.path.basename(dstp))) + print(' entries %d, inode table identical: %s, superblock identical: %s' % ( + len(a), a == b, (r1.sb_size, r1.sb_cksum) == (r2.sb_size, r2.sb_cksum))) + print(' changed files: %d (expected %d) sha256 %s' % ( + len(changed), len(mine), hashlib.sha256(open(dstp, 'rb').read()).hexdigest()[:16])) + assert not unexpected, 'unexpected changed file! %s' % unexpected[:3] + assert len(changed) == len(mine), 'changed %d != expected %d' % (len(changed), len(mine)) + # 端到端:把负载按 (偏移,长度) 放回去,必须与改后镜像逐字节相同 + d = bytearray(open(srcp, 'rb').read()) + for r in rows: + if r['tag'] == tag: + d[r['offset']:r['offset'] + r['length']] = newblobs[r['name']] + assert bytes(d) == open(dstp, 'rb').read(), 'payload replay != patched image' + print(' payload replay == patched image: True') + # 回滚也必须能逐字节还原原厂镜像 + e = bytearray(open(dstp, 'rb').read()) + for r in rows: + if r['tag'] == tag: + e[r['offset']:r['offset'] + r['length']] = stock_of[r['name']] + orig = open(srcp, 'rb').read() + assert bytes(e) == orig, 'rollback replay != stock image' + print(' rollback replay == stock image: True (sha256 %s)' + % hashlib.sha256(orig).hexdigest()[:16]) + +print('\n-> %s' % OUT) diff --git a/s5e-icons/tools/s5e_render_ui.py b/s5e-icons/tools/s5e_render_ui.py new file mode 100644 index 0000000..21a090d --- /dev/null +++ b/s5e-icons/tools/s5e_render_ui.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +"""Render the 480x480 page spec with the real MiSans fonts (previews + overview).""" +import os, sys +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +import render_ui as R +from PIL import Image +from paths import DOCS +R.W = R.H = 480 +from s5e_pages import PAGES + +outs = [] +for name, items in PAGES.items(): + p = os.path.join(DOCS, 's5e_ui_%s.png' % name) + R.render(items, p) + outs.append((name, p)) + print('wrote', p) +if R.OVERFLOW: + print('!! label overflow (text, lh, lines, total, box, natural):') + for o in R.OVERFLOW: + print(' ', o) +else: + print('no label overflow') +cols = 3 +rows = (len(outs) + cols - 1) // cols +sheet = Image.new('RGB', (cols * (480 + 12) + 12, rows * (480 + 12) + 12), (28, 28, 32)) +for i, (name, p) in enumerate(outs): + sheet.paste(Image.open(p), (12 + (i % cols) * 492, 12 + (i // cols) * 492)) +sp = os.path.join(DOCS, 's5e_ui_all.png') +sheet.save(sp) +print('wrote', sp, sheet.size) diff --git a/s5e-icons/tools/s5e_ui_images.py b/s5e-icons/tools/s5e_ui_images.py new file mode 100644 index 0000000..5f6b007 --- /dev/null +++ b/s5e-icons/tools/s5e_ui_images.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +"""UI image assets for the 480 face + the 326 preview / 80 logo for the builder.""" +import os, sys +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +import iconpack as ip +from PIL import Image, ImageDraw +from paths import PROJ, DOCS +ASSETS = os.path.join(PROJ, 'assets') +OUT = os.path.join(PROJ, 'ui_images') +os.makedirs(OUT, exist_ok=True) + +SPEC = [('back', 'backarrow.png', 18, 29), + ('ic_help', 'MdiLightbulbOutline.png', 31, 31), + ('ic_replace', 'MdiBandage.png', 31, 31), + ('ic_about', 'MdiInformationOutline.png', 31, 31), + ('ic_check', 'MdiClipboardTextSearchOutline.png', 31, 31), + ('ic_rollback', 'MdiReplyAllOutline.png', 31, 31), + ('ic_qq', 'qq.png', 31, 31)] +for name, png, w, h in SPEC: + p = os.path.join(ASSETS, png) + if not os.path.exists(p): + print(' missing', png); continue + img = Image.open(p).convert('RGBA') + if img.size != (w, h): + img = img.resize((w, h), Image.LANCZOS) + blob = ip.encode(img, ip.CF_ARGB8888, w, h, 0) + open(os.path.join(OUT, name + '.bin'), 'wb').write(blob) + print(' %-12s <- %-34s %dx%d %d bytes' % (name, png, w, h, len(blob))) + +L = 80 +img = Image.open(os.path.join(ASSETS, 'cover.png')).convert('RGBA').resize((L, L), Image.LANCZOS) +mask = Image.new('L', (L, L), 0) +ImageDraw.Draw(mask).ellipse([0, 0, L - 1, L - 1], fill=255) +img.putalpha(mask) +open(os.path.join(OUT, 'logo.bin'), 'wb').write(ip.encode(img, ip.CF_ARGB8888, L, L, 0)) +img.save(os.path.join(DOCS, 'logo_preview_480.png')) +print(' %-12s <- cover.png(circular) %dx%d' % ('logo', L, L)) +print('->', OUT) diff --git a/s5e-icons/tools/s5e_verify.py b/s5e-icons/tools/s5e_verify.py new file mode 100644 index 0000000..3f4b6e0 --- /dev/null +++ b/s5e-icons/tools/s5e_verify.py @@ -0,0 +1,120 @@ +# -*- coding: utf-8 -*- +"""Independent end-to-end check of the delivered .face. + +Nothing here trusts the build script's bookkeeping: the Lua table is parsed out of +the face and every offset is validated against the *firmware images* (stock and +patched) and the ui_images directory. +""" +import os, re, sys, json, hashlib, struct +sys.stdout.reconfigure(encoding='utf-8', errors='replace') +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from romfs import Romfs + +from paths import PROJ, INPUTS, WORK, DIST +WF = PROJ +NEW = os.path.join(INPUTS, 'target') +PAT = os.path.join(WORK, 's5e_from_os4') +FACE = os.path.join(DIST, 'S5eIcons.face') +IMAGES = {'app': ('vela_app.bin', 'vela_app_os4.bin'), 'health': ('vela_health.bin', 'vela_health_os4.bin')} +MANIFEST = os.path.join(PAT, 'manifest.json') + +face = open(FACE, 'rb').read() +# 直接从 .face 里把 Lua 抠出来校验(不依赖构建目录) +_end = face.find(b'return ui\n') +if _end < 0: + raise SystemExit('face 里找不到 Lua 结尾') +_start = face.rfind(b'--[[', 0, _end) +lua = face[_start:_end + len(b'return ui\n')] +if not lua.startswith(b'--[[--') or b'local UIIMG = {' not in lua: + raise SystemExit('Lua 提取失败:%r ... %r' % (lua[:24], lua[-24:])) +fails = [] + + +def ck(cond, msg): + print((' OK ' if cond else ' FAIL ') + msg) + if not cond: + fails.append(msg) + + +print('== 头部 ==') +ck(face[0:4] == b'\x5a\xa5\x34\x12', 'magic 5a a5 34 12') +ck(face[40:50] == b'462150101\x00', 'id = %r' % face[40:50]) +ck(face[5] == 0, 'b5 = %d(固件自带表盘 b5 也是 0)' % face[5]) +a16, = struct.unpack(' 0, 'Lua 偏移 0x%x' % off_lua) +txt = lua.decode('utf-8') + +print('== UI 图片(从 .face 里的 Lua 表读出来校验)==') +ui = re.findall(r'\{ (\d+), (\d+), "([a-z_]+)" \}', txt) +ck(len(ui) == 8, 'UIIMG 条目 %d 条' % len(ui)) +files = {f[:-4]: open(os.path.join(WF, 'ui_images', f), 'rb').read() + for f in os.listdir(os.path.join(WF, 'ui_images')) if f.endswith('.bin')} +for o, l, n in ui: + o, l = int(o), int(l) + ck(n in files and face[o:o + l] == files[n], '%(n)-12s @0x%(o)06x len=%(l)d 与 ui_images/%(n)s.bin 一致' % dict(n=n, o=o, l=l)) + +print('== 负载表 ==') +pay = re.findall(r'\{ "(app|health)", 0x([0-9a-f]+), (\d+), (\d+) \}', txt) +blocks = re.findall(r'^ (pay|rb) = \{', txt, re.M) +ck(blocks == ['pay', 'rb'], 'pay / rb 两个表都在') +half = len(pay) // 2 +exp = json.load(open(MANIFEST, encoding='utf-8'))['records'] +ck(half == len(exp) and len(pay) == 2 * len(exp), + 'pay %d 条 + rb %d 条(manifest 里 %d 条)' % (half, len(pay) - half, len(exp))) +pay_recs = [dict(dev=d, off=int(o, 16), ln=int(l), res=int(r)) for d, o, l, r in pay[:half]] +rb_recs = [dict(dev=d, off=int(o, 16), ln=int(l), res=int(r)) for d, o, l, r in pay[half:]] + +stock, patched, sizes = {}, {}, {} +for dev, (sf, pf) in IMAGES.items(): + rs, rp = Romfs(os.path.join(NEW, sf)), Romfs(os.path.join(PAT, pf)) + stock[dev] = rs.data + patched[dev] = rp.data + sizes[dev] = rs.sb_size + +bad = 0 +for i, (p, r) in enumerate(zip(pay_recs, rb_recs)): + assert p['dev'] == r['dev'] and p['off'] == r['off'] and p['ln'] == r['ln'], 'record %d 表错位' % i + d, o, l = p['dev'], p['off'], p['ln'] + ok = (face[p['res']:p['res'] + l] == patched[d][o:o + l] and + face[r['res']:r['res'] + l] == stock[d][o:o + l] and + o % 4 == 0 and o + l <= sizes[d] and # RLE 素材的长度可以是任意值,只要偏移对齐 + len(face[p['res']:p['res'] + l]) == l and len(face[r['res']:r['res'] + l]) == l) + if not ok: + bad += 1 + print(' FAIL %-22s %s off=0x%x len=%d' % (d, p['dev'], o, l)) +ck(bad == 0, '%d 条:负载字节 == 改后镜像,回滚字节 == 原厂镜像,且都 4 对齐、不越界' % half) +devs = {} +for p in pay_recs: + devs[p['dev']] = devs.get(p['dev'], 0) + 1 +exp_devs = {} +for r in exp: + exp_devs[r['dev']] = exp_devs.get(r['dev'], 0) + 1 +ck(devs == exp_devs, '分区分布 %s(manifest %s)' % (devs, exp_devs)) + +print('== 负载文件在 .face 里连续存放 ==') +pbin = open(os.path.join(WF, 'payload', 'payload.bin'), 'rb').read() +rbin = open(os.path.join(WF, 'payload_rollback', 'rollback.bin'), 'rb').read() +ck(face.count(pbin) == 1 and face.count(rbin) == 1, + 'payload %d B / rollback %d B 各出现 1 次' % (len(pbin), len(rbin))) +rel = sorted(x['res'] - (face.find(pbin) + 15) for x in pay_recs) +ck(rel[0] == 0, '第一条负载相对偏移为 0') +ck(face[pay_recs[0]['res'] - 15:pay_recs[0]['res']] == pbin[:15], 'magic 紧贴第一条负载') + +print() +if fails: + print('!! %d 项未通过' % len(fails)) + sys.exit(1) +print('全部通过 —— %s (%d B, sha256 %s)' % (os.path.basename(FACE), len(face), + hashlib.sha256(face).hexdigest()[:16])) diff --git a/s5e-icons/ui_images/back.bin b/s5e-icons/ui_images/back.bin new file mode 100644 index 0000000..76eb40c Binary files /dev/null and b/s5e-icons/ui_images/back.bin differ diff --git a/s5e-icons/ui_images/ic_about.bin b/s5e-icons/ui_images/ic_about.bin new file mode 100644 index 0000000..2476014 Binary files /dev/null and b/s5e-icons/ui_images/ic_about.bin differ diff --git a/s5e-icons/ui_images/ic_check.bin b/s5e-icons/ui_images/ic_check.bin new file mode 100644 index 0000000..177ead3 Binary files /dev/null and b/s5e-icons/ui_images/ic_check.bin differ diff --git a/s5e-icons/ui_images/ic_help.bin b/s5e-icons/ui_images/ic_help.bin new file mode 100644 index 0000000..9ed2ddb Binary files /dev/null and b/s5e-icons/ui_images/ic_help.bin differ diff --git a/s5e-icons/ui_images/ic_qq.bin b/s5e-icons/ui_images/ic_qq.bin new file mode 100644 index 0000000..7db71e4 Binary files /dev/null and b/s5e-icons/ui_images/ic_qq.bin differ diff --git a/s5e-icons/ui_images/ic_replace.bin b/s5e-icons/ui_images/ic_replace.bin new file mode 100644 index 0000000..a231ebd Binary files /dev/null and b/s5e-icons/ui_images/ic_replace.bin differ diff --git a/s5e-icons/ui_images/ic_rollback.bin b/s5e-icons/ui_images/ic_rollback.bin new file mode 100644 index 0000000..19debcb Binary files /dev/null and b/s5e-icons/ui_images/ic_rollback.bin differ diff --git a/s5e-icons/ui_images/logo.bin b/s5e-icons/ui_images/logo.bin new file mode 100644 index 0000000..cca18bb Binary files /dev/null and b/s5e-icons/ui_images/logo.bin differ