diff --git a/.gitignore b/.gitignore index 9e96e7f..36e4e02 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,9 @@ # ==== 体积大 / 有版权 / 可再生成的东西,不进仓库 ==== +# 本仓库只保留 S4e(o62lte)相关项目;S5e(p62lte)的项目另行维护 +# 注意:文件夹仍在本地磁盘上,只是不进版本控制 +s5e-*/ + # 固件分区镜像 + 字体(版权属小米;从官方 OTA 包 / vela_font.bin 自行获取) **/inputs/**/*.bin **/inputs/fonts/ diff --git a/s5e-charge/.gitignore b/s5e-charge/.gitignore deleted file mode 100644 index 13a0346..0000000 --- a/s5e-charge/.gitignore +++ /dev/null @@ -1,10 +0,0 @@ -# 大文件 / 中间产物:不进仓库 -.work/ -inputs/ -vendor/ -__pycache__/ -*.pyc -payload/ -payload_restore/ -blob_replace/ -blob_restore/ diff --git a/s5e-charge/README.md b/s5e-charge/README.md deleted file mode 100644 index 4dd876a..0000000 --- a/s5e-charge/README.md +++ /dev/null @@ -1,228 +0,0 @@ -# S5Chg —— 把 S5 41mm(q63) 的充电动画搬到 S5 eSIM 46mm - -把 **Xiaomi Watch S5 41mm(q63)** `vela_system.bin` 里的 `charging/charge0..60.bin`(**464×464**), -逐帧**等长覆盖**写进 **Xiaomi Watch S5 eSIM 46mm(p62lte)** `/dev/system` 的同名 61 个槽位 -(**480×480**,I8 + RLE)。 - -> 关系:`s4e-charge` 是「S5 41mm → S4 eSIM」的母项目(只读参考)。本项目沿用它的全部结构 -> (两遍构建回填偏移、指纹表拆盘、逐字节等长覆盖、独立校验),只把目标换成 S5 eSIM、 -> 帧数改成 61→61、界面重做成 480×480、放大从"同尺寸"改成"464→480 放大"。 - -## 改了什么 - -| | | -|---|---| -| 目标 | `inputs/target/vela_system.bin`(S5 eSIM 46mm `/dev/system`)→ `charging/charge0..60.bin`,**61 帧 480×480** I8+RLE,槽位 143,832 ~ 213,511 B(**各不相同**) | -| 源 | `inputs/source/vela_system.bin`(q63 S5 41mm)→ `charging/charge0..60.bin`,**61 帧 464×464** | -| 映射 | **帧号 1:1(n → n)**。两边都是 61 帧,**没有** S4e 那版的 `round(n/56*60)` 比例重采样 | -| 放大 | 源帧 RGB 用 **Lanczos** 放大到 480×480,再把每个像素**吸附回源帧自己的 256 色调色板**(最近的欧氏距离)→ 颜色零新增、零量化,空间上比最近邻放大平滑 | -| 本次 | **61/61 全换**,**每一帧都是"原调色板 + Lanczos"**(没有一帧被降级:没有暗区合并、没有降色数、没有留原厂) | -| 编码 | 压缩 8.69 MB,槽位共 11.27 MB → **还剩 2.57 MB 余量**(最紧的一帧 `charge47` 也余 10,488 B) | -| 校验 | 61/61 回解**逐像素**等于计划图、RLE 恰好解出 usize、inode 表/superblock 逐位不变、槽位外 0 字节变化、回放一致 | - -等长的做法(硬约束,一步都不能省):`解码源帧 → Lanczos 放大 480×480 → 吸附回源调色板 → -LVGL RLE 压缩 → 用合法空指令补齐到原 csize`。补齐用 `00 00`(重复 0 次)这类**产出 0 像素** -的指令;另外 `usize_tail()` 会在解码长度比 `usize` 少时**重复最后一个字节**补足 -(踩过的坑:RLE 输出比 `usize` 少 1 个像素,设备端整帧不画)。 - -## 交付物 - -| 文件 | 字节数 | id / b5 | sha256 | -|---|---|---|---| -| `dist/S5Chg.face` | **11,986,752** | `462150103` / b5=0 | `577147341d363811965b51f46088ed98ea62c946cc8b7d205264577fab4d8066` | -| `dist/S5ChgRestore.face` | **11,986,752** | `462150103` / b5=0 | `8dbfc87acde8b54543fa7f2b54c1b0d7360634c83d6cf139a78df762ff078f2c` | - -两份盘**共用同一个表盘 id**(`462150103`):装其中一份就会顶掉另一份,设备上始终只占一份空间。 -- `S5Chg.face`:`pay` = 替换后的 61 帧,`rb` = **原厂**前 64 B 指纹(只给版本校验用) -- `S5ChgRestore.face`:`pay` = **原厂**61 帧(逐字节),`rb` = 替换后前 64 B 指纹 - -预览与报告: -- `docs/decode_preview.png` —— **直接从交付物 .face 里解出来的 61 帧**总览(最硬的证据) -- `docs/face_before_after.png` —— 抽样 5 帧:上排 q63 原帧(464),下排交付物解出的帧(480) -- `docs/before_after.png` —— 生成负载时的对照图(上=源帧,下=磁盘上改后镜像解出的帧) -- `docs/ui_*.png` / `docs/ui_all.png` —— 480×480 界面(9 个规格页 + 8 个还原盘文案变体) -- `docs/selfcheck_report.txt` —— `tools/verify_all.py` 的完整输出(33/33) -- `docs/selfcheck_run.txt` —— **整条重建流水线的原始输出**(8 步,逐步 EXIT=0) - -## 真机怎么做 - -1. 装 `dist/S5Chg.face` -2. 表盘「快速帮助 → 环境预检查」(**只读**,一个字节都不写):确认 `/dev/system` 可读、 - 卷名是 `system`、61 条负载不越界,并对首/中/尾三条做抽样版本校验(`原厂✓` / `已替换✓`) -3. 首页「开始替换」→ 确认页 → **确认替换** → 等 `成功 61 失败 0`(61 条 ≈ 11.3 MB) -4. 「重启手表」→ 插上充电器看整段动画 -5. 还原:装 `dist/S5ChgRestore.face` →「开始还原」→ 重启 - -输出怎么读(「正在替换」页):每 10 条一行 `> 写入 n/61 成功 x 失败 y`;结束时 -`> 共 61 条,成功 x 失败 y`。 - -**判断成功的唯一标准是 `成功 61 / 失败 0`**;如果出现 `> 版本不符跳过 n 条`,说明设备上的 -`/dev/system` 不是算偏移时用的那一版固件 —— 此时**不要重启**,装对应版本的盘或先还原。 - -写入前每条都做版本校验:读设备上该偏移的前 64 B,必须等于「原厂前 64 B」或「替换后前 64 B」, -**两边都不是就判「版本不符」并跳过 `/dev/system` 剩余记录**(绝不把字节写进别人的文件里)。 -写入后整条回读比对,失败自动重试 3 次 + 退化成整条一次 dd。 - -## 重建 - -```powershell -# 需要 Python 3 + Pillow + numpy;inputs/ 与 vendor/ 要自己准备(见下) -python tools\make_charge_payload.py all # 61 帧负载 + 整分区镜像(自带 4 项镜像级自证) -python tools\make_dd_payload.py replace # 负载 + 指纹 + blob_replace/ -python tools\gen_ui_lua.py replace # 两遍构建 -> dist\S5Chg.face(0 漂移校验) -python tools\verify_anim.py replace # 逐盘校验(11 项) -python tools\make_dd_payload.py restore -python tools\gen_ui_lua.py restore # -> dist\S5ChgRestore.face -python tools\verify_anim.py restore -python tools\verify_all.py # 独立总校验(33 项)+ 预览图 + selfcheck_report.txt -``` - -或者一条命令跑完全部 8 步(逐步校验、任一步失败即中止,输出留档到 `docs/selfcheck_run.txt`): - -```powershell -& ".\tools\selfcheck.ps1" -``` - -`make_charge_payload.py` 不加 `all` 时只跑 `[0,14,28,42,56]` 这 5 帧(轻量试跑)。 - -界面相关(改了 `tools/pages.py` / `main.lua` 之后): - -```powershell -python tools\make_ui_images.py # assets/ -> ui_images/*.bin(480 档尺寸) -python tools\render_ui.py # 480×480 预览(有标签溢框会直接报出来) -python tools\check_clip.py # 真实渲染像素判圆屏切边(0 越界才算过) -``` - -### `inputs/` 与 `vendor/` 不进仓库 - -`.gitignore` 里已经排除(本项目 `.gitignore` 原样沿用,未改动): - -| 路径 | 是什么 | -|---|---| -| `inputs/target/vela_system.bin` | 目标固件 p62lte 的 system 分区镜像(84,429,824 B) | -| `inputs/source/vela_system.bin` | 源固件 q63 的 system 分区镜像(100,630,528 B) | -| `inputs/fonts/MiSans-*.ttf` | 可选;只用于本机渲染预览(缺了就退回系统字体,只影响预览) | -| `vendor/LuaDevTemplate-main/` | 官方模板(含 `Compiler.exe`)—— 用默认路径即可,**不要设 `ICON_EDIT_VENDOR`** | - -固件分区镜像就是官方 OTA 包(ZIP)里的 `vela_system.bin` 条目,解压出来放到上表位置即可。 - -## 自证输出(全部真实执行过) - -**① 61 条负载长度逐条 == 原槽位长度** -``` -目标 S5 eSIM(p62lte) 61 帧(0..60,槽位合计 11.27 MB) -源 S5 41mm(q63) 61 帧(0..60,10.63 MB) -映射:n -> n(1:1) 输出分辨率:480x480(源 464x464 -> Lanczos 放大后吸附回源调色板) - 帧 47 <- q63 第 47 帧 槽位 144563 B 编码 134051 B(留白 10488) ... 原调色板+Lanczos -回解自检通过 61 / 61(解码长度恰好 usize 且逐像素等于计划图) -编码方式分布:{'原调色板+Lanczos': 61} -``` -`tools/verify_all.py`:`[OK] replace:61 条长度 == 原厂槽位长度 不符 0 条` / -`[OK] restore:61 条长度 == 原厂槽位长度 不符 0 条`; -`tools/verify_anim.py`:`pay 检查 61 条:长度全对 True` - -**② 每条负载解出来都是 480×480(不能解码失败)** -``` -[OK] replace:61 条都解出 480x480 且 RLE 恰好 usize 不符 0 条 -[OK] restore:61 条都解出 480x480 且 RLE 恰好 usize 不符 0 条 -``` -(`usize` 必须**恰好** 231,424 = 1024 + 480×480,多一字节设备端 INVALID、少一字节不画) - -**③ 改后整分区镜像的 inode 表 / superblock 逐位不变** -``` -inode 表逐位不变:True(2150 条) -superblock 一致:True(sb_size=84429200 / 84429200,sb_cksum=3275251810 / 3275251810) -逐字节差异:共 11585280 字节,全部落在 61 个槽位内:True(槽位外 0 字节) -[OK] inode 表逐位不变(2150 条) / [OK] superblock 逐位不变 / [OK] 除 61 个槽位外没有任何字节变化 -``` - -**④ 独立校验脚本全绿** -``` -=== tools/verify_all.py(只读 dist/*.face + inputs/*/vela_system.bin,不读 .work/)=== -61 帧平均差:mean 0.189 max 0.260(第 17 帧) <- 与"独立做的 Lanczos 放大"比对 -最像的源帧号与自身不一致的帧:0 个 <- 证明第 n 帧就是 q63 第 n 帧 -[OK] replace 盘 rb == 原厂前 64 B(对固件镜像比对) 不符 0 条 -[OK] restore 盘 pay == 原厂整条字节(逐字节) 不符 0 条 -[OK] restore 盘 rb == replace 盘 pay 前 64 B(两份交付物互证) 不符 0 条 -RESULT: 全部通过 —— 33 项通过,0 项失败 - -=== tools/verify_anim.py replace / restore === -S5Chg:pay 61 条,rb 61 条 pay 检查:长度全对 True,可解码 True,字节一致 True -全部通过 —— 11 项通过,0 项失败 (restore 同样 11/11) - -=== tools/check_clip.py(480×480 圆屏,含 replace/restore 两套文案)=== -检查了 17 个页面(含 replace/restore 两套文案):越界页面 0 -RESULT: PASS(0 越界、0 折行) -``` - -**⑤ 两张 .face 的 sha256** -``` -S5Chg.face 11986752 b4=0 b5=0 id=b'462150103\x00' sha256[:16]=577147341d363811 -S5ChgRestore.face 11986752 b4=0 b5=0 id=b'462150103\x00' sha256[:16]=8dbfc87acde8b545 -S5Chg.face sha256=577147341d363811965b51f46088ed98ea62c946cc8b7d205264577fab4d8066 -S5ChgRestore.face sha256=8dbfc87acde8b54543fa7f2b54c1b0d7360634c83d6cf139a78df762ff078f2c -``` -完整重建 + 自证的原始输出(8 步全部 EXIT=0)留档在 `docs/selfcheck_run.txt`, -显式自证清单在 `docs/selfcheck_report.txt`(33/33)。重跑整条流水线: - -```powershell -& ".\tools\selfcheck.ps1" # = 上面 8 条命令,逐步校验,任一步失败即中止 -``` - -## 已知边界 / 没做的部分 - -1. **没有一帧颜色数被降级**,也没有一帧保留原厂:61/61 都是「原调色板 + Lanczos」。 - 全帧色数 251~256(即源调色板的实际用量),吸附误差 0.189/255(最大 0.260,第 17 帧)。 -2. **没做锐化**:放大只用 Lanczos + 调色板吸附。这是刻意的 —— 锐化会增加索引噪点、 - 直接吃掉 RLE 余量(最紧的 `charge47` 只剩 10,488 B)。 -3. **真机未验证 S5 eSIM 的 `/dev/system` 可写**:这一点在 S4 eSIM 上实测通过(同源平台、 - 同一套 dd 流程),S5 eSIM 上只能先靠只读的「环境预检查」确认可读 + 偏移对版。 - `system/drain/*`、`ota/anim*`、`setupwizard/*` 同理可改,但同样未在本机验证。 -4. **偏移锁固件版本**:换固件必须重跑 `make_charge_payload.py all` → `make_dd_payload.py` → - `gen_ui_lua.py`。用错版本时表盘会判「版本不符」并跳过 `/dev/system` 剩余记录, - 不会把字节写进别人的文件里。 -5. **`Compiler.exe` 的设备表里没有 480 档**(枚举 DeviceType 只有 17 个已知值,圆表且预览 - 326×326 的只有 362 / 462)→ 这里用 `DeviceType=462` + 326×326 预览图,预览只是表盘列表 - 里的缩略图,与实际 480×480 画面无关(界面尺寸由 `main.lua` 与 `.fprj` 的 480 决定)。 -6. **负载文件必须原样连续放在 resource.bin 里**,偏移才等于 `文件基址 + 魔数长度 + 相对偏移`; - `gen_ui_lua.py` 会断言这一点并做两遍 0 漂移校验。魔数长度是 15 B(曾把 15 写成 16 导致全部偏移差 1)。 -7. **写入速度**:偏移都是 512 对齐,所以 195 KB 的记录按 `bs=512` + `bs=4` + `bs=1` 三段写, - 不是逐字节;`sync` + 整条回读比对,失败重试 3 次后改用整条一次 dd。 -8. **没动的**:`inode` 表、目录项、superblock、任何非 `charging/charge*.bin` 的文件; - 仓库里其它项目(`s4e-*`、`s5e-icons`、`s5e-pressure-anim`)与 git(未 commit、未改 `.gitignore`)。 - -## 目录 - -``` -main.lua 表盘源码(含 __UIIMG__ / __PAY__ / __RB__ / __ROLE__ 占位符,构建时回填) -tools/ 全部脚本(见下表) -ui_images/ 界面素材(LVGL v9 .bin,8 张,480 档尺寸) -assets/ 设计素材 PNG -dist/ 交付的 .face -docs/ 预览图、对照图、selfcheck_report.txt -inputs/ 固件分区镜像 + 字体(不进仓库,见上) -vendor/ LuaDevTemplate-main(含 Compiler.exe,不进仓库) -.work/ 构建工作区(产物,可删) -``` - -| 脚本 | 作用 | 相对 s4e-charge | -|---|---|---| -| `tools/paths.py` | 所有路径的唯一入口 | 沿用(未改) | -| `tools/romfs.py` | ROMFS 分区解析 | 沿用(未改) | -| `tools/lvgl_bin.py` | LVGL v9 RLE 编解码 + `pad_rle_safe` | 沿用(未改) | -| `tools/iconpack.py` | ARGB8888/I8 图片编码(界面素材用) | 沿用(未改) | -| `tools/lua_balance.py` | Lua 块配平检查 | 沿用(未改) | -| `tools/make_charge_payload.py` | **改**:目标/源换成 S5e + q63,61→61(n→n),输出 480×480,Lanczos+调色板吸附 | 重写 | -| `tools/make_dd_payload.py` | **改**:魔数 `S5C-PAYv1` / `S5C-FPv1`,脚本文案与设备名 | 小改 | -| `tools/make_overlays.py` | **改**:DeviceType 462 / Screen 480 / 预览 326(按首页规格真渲染) | 改 | -| `tools/build_faces.py` | **改**:盘名 S5Chg / S5ChgRestore,id 462150103 | 小改 | -| `tools/finalize_faces.py` | **改**:同上(b5=0 的忠实产物 + 改 id) | 小改 | -| `tools/gen_ui_lua.py` | **改**:盘名/角色名 | 小改 | -| `tools/verify_anim.py` | **改**:盘名 + 480×480 判据 | 小改 | -| `tools/verify_all.py` | **新增**:独立总校验(33 项,不读 `.work/`)+ 预览图 + 报告 | 新增 | -| `tools/selfcheck.ps1` | **新增**:一条命令跑完上面 8 步(逐步校验、失败即中止、输出留档) | 新增 | -| `tools/pages.py` | **改**:480×480 页面规格(含还原盘文案变体) | 重写 | -| `tools/render_ui.py` | **改**:画布 466→480,输出到 `docs/`,报标签溢框 | 小改 | -| `tools/check_clip.py` | **改**:圆屏 240/240,两套文案都过一遍 | 改 | -| `tools/make_ui_images.py` | **改**:素材尺寸换 480 档(31/18×29/80) | 改 | -| `main.lua` | **改**:480 几何、S5Chg 文案、版本不符**跳过分区剩余记录** | 重写 | diff --git a/s5e-charge/assets/MdiBandage.png b/s5e-charge/assets/MdiBandage.png deleted file mode 100644 index 26abc5b..0000000 Binary files a/s5e-charge/assets/MdiBandage.png and /dev/null differ diff --git a/s5e-charge/assets/MdiClipboardTextSearchOutline.png b/s5e-charge/assets/MdiClipboardTextSearchOutline.png deleted file mode 100644 index 85ea2da..0000000 Binary files a/s5e-charge/assets/MdiClipboardTextSearchOutline.png and /dev/null differ diff --git a/s5e-charge/assets/MdiInformationOutline.png b/s5e-charge/assets/MdiInformationOutline.png deleted file mode 100644 index 3af2a29..0000000 Binary files a/s5e-charge/assets/MdiInformationOutline.png and /dev/null differ diff --git a/s5e-charge/assets/MdiLightbulbOutline.png b/s5e-charge/assets/MdiLightbulbOutline.png deleted file mode 100644 index 7d7819c..0000000 Binary files a/s5e-charge/assets/MdiLightbulbOutline.png and /dev/null differ diff --git a/s5e-charge/assets/MdiReplyAllOutline.png b/s5e-charge/assets/MdiReplyAllOutline.png deleted file mode 100644 index f419de0..0000000 Binary files a/s5e-charge/assets/MdiReplyAllOutline.png and /dev/null differ diff --git a/s5e-charge/assets/backarrow.png b/s5e-charge/assets/backarrow.png deleted file mode 100644 index 9e55859..0000000 Binary files a/s5e-charge/assets/backarrow.png and /dev/null differ diff --git a/s5e-charge/assets/cover.png b/s5e-charge/assets/cover.png deleted file mode 100644 index 195e7e1..0000000 Binary files a/s5e-charge/assets/cover.png and /dev/null differ diff --git a/s5e-charge/assets/override/activities.png b/s5e-charge/assets/override/activities.png deleted file mode 100644 index ae5c552..0000000 Binary files a/s5e-charge/assets/override/activities.png and /dev/null differ diff --git a/s5e-charge/assets/override/interconnect.png b/s5e-charge/assets/override/interconnect.png deleted file mode 100644 index 5739b3b..0000000 Binary files a/s5e-charge/assets/override/interconnect.png and /dev/null differ diff --git a/s5e-charge/assets/override/sports_course.png b/s5e-charge/assets/override/sports_course.png deleted file mode 100644 index 4b95e98..0000000 Binary files a/s5e-charge/assets/override/sports_course.png and /dev/null differ diff --git a/s5e-charge/assets/override/voice_aivs.png b/s5e-charge/assets/override/voice_aivs.png deleted file mode 100644 index 228914f..0000000 Binary files a/s5e-charge/assets/override/voice_aivs.png and /dev/null differ diff --git a/s5e-charge/assets/qq.png b/s5e-charge/assets/qq.png deleted file mode 100644 index 3982762..0000000 Binary files a/s5e-charge/assets/qq.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/background.png b/s5e-charge/assets/walkie_talkie/background.png deleted file mode 100644 index 879c98f..0000000 Binary files a/s5e-charge/assets/walkie_talkie/background.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/cancel_btn.png b/s5e-charge/assets/walkie_talkie/cancel_btn.png deleted file mode 100644 index cc56188..0000000 Binary files a/s5e-charge/assets/walkie_talkie/cancel_btn.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/check.png b/s5e-charge/assets/walkie_talkie/check.png deleted file mode 100644 index bc89d28..0000000 Binary files a/s5e-charge/assets/walkie_talkie/check.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/checked.png b/s5e-charge/assets/walkie_talkie/checked.png deleted file mode 100644 index 64bf2b9..0000000 Binary files a/s5e-charge/assets/walkie_talkie/checked.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/close_btn.png b/s5e-charge/assets/walkie_talkie/close_btn.png deleted file mode 100644 index a872247..0000000 Binary files a/s5e-charge/assets/walkie_talkie/close_btn.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/confirm_btn.png b/s5e-charge/assets/walkie_talkie/confirm_btn.png deleted file mode 100644 index 932d711..0000000 Binary files a/s5e-charge/assets/walkie_talkie/confirm_btn.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/delete_icon.png b/s5e-charge/assets/walkie_talkie/delete_icon.png deleted file mode 100644 index b3d948d..0000000 Binary files a/s5e-charge/assets/walkie_talkie/delete_icon.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/guidance/finish_guidance.png b/s5e-charge/assets/walkie_talkie/guidance/finish_guidance.png deleted file mode 100644 index 932d711..0000000 Binary files a/s5e-charge/assets/walkie_talkie/guidance/finish_guidance.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/history_anim/anim0.png b/s5e-charge/assets/walkie_talkie/history_anim/anim0.png deleted file mode 100644 index cdbf172..0000000 Binary files a/s5e-charge/assets/walkie_talkie/history_anim/anim0.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/history_anim/anim1.png b/s5e-charge/assets/walkie_talkie/history_anim/anim1.png deleted file mode 100644 index 9eb6f8b..0000000 Binary files a/s5e-charge/assets/walkie_talkie/history_anim/anim1.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/history_anim/anim10.png b/s5e-charge/assets/walkie_talkie/history_anim/anim10.png deleted file mode 100644 index 927d24a..0000000 Binary files a/s5e-charge/assets/walkie_talkie/history_anim/anim10.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/history_anim/anim11.png b/s5e-charge/assets/walkie_talkie/history_anim/anim11.png deleted file mode 100644 index 38d3985..0000000 Binary files a/s5e-charge/assets/walkie_talkie/history_anim/anim11.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/history_anim/anim12.png b/s5e-charge/assets/walkie_talkie/history_anim/anim12.png deleted file mode 100644 index 32c452c..0000000 Binary files a/s5e-charge/assets/walkie_talkie/history_anim/anim12.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/history_anim/anim13.png b/s5e-charge/assets/walkie_talkie/history_anim/anim13.png deleted file mode 100644 index b4338f7..0000000 Binary files a/s5e-charge/assets/walkie_talkie/history_anim/anim13.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/history_anim/anim14.png b/s5e-charge/assets/walkie_talkie/history_anim/anim14.png deleted file mode 100644 index 6dd42f1..0000000 Binary files a/s5e-charge/assets/walkie_talkie/history_anim/anim14.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/history_anim/anim15.png b/s5e-charge/assets/walkie_talkie/history_anim/anim15.png deleted file mode 100644 index e4795f0..0000000 Binary files a/s5e-charge/assets/walkie_talkie/history_anim/anim15.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/history_anim/anim16.png b/s5e-charge/assets/walkie_talkie/history_anim/anim16.png deleted file mode 100644 index 5cdb8ad..0000000 Binary files a/s5e-charge/assets/walkie_talkie/history_anim/anim16.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/history_anim/anim17.png b/s5e-charge/assets/walkie_talkie/history_anim/anim17.png deleted file mode 100644 index 9eb6f8b..0000000 Binary files a/s5e-charge/assets/walkie_talkie/history_anim/anim17.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/history_anim/anim18.png b/s5e-charge/assets/walkie_talkie/history_anim/anim18.png deleted file mode 100644 index cdbf172..0000000 Binary files a/s5e-charge/assets/walkie_talkie/history_anim/anim18.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/history_anim/anim2.png b/s5e-charge/assets/walkie_talkie/history_anim/anim2.png deleted file mode 100644 index 5cdb8ad..0000000 Binary files a/s5e-charge/assets/walkie_talkie/history_anim/anim2.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/history_anim/anim3.png b/s5e-charge/assets/walkie_talkie/history_anim/anim3.png deleted file mode 100644 index e4795f0..0000000 Binary files a/s5e-charge/assets/walkie_talkie/history_anim/anim3.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/history_anim/anim4.png b/s5e-charge/assets/walkie_talkie/history_anim/anim4.png deleted file mode 100644 index 6dd42f1..0000000 Binary files a/s5e-charge/assets/walkie_talkie/history_anim/anim4.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/history_anim/anim5.png b/s5e-charge/assets/walkie_talkie/history_anim/anim5.png deleted file mode 100644 index b4338f7..0000000 Binary files a/s5e-charge/assets/walkie_talkie/history_anim/anim5.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/history_anim/anim6.png b/s5e-charge/assets/walkie_talkie/history_anim/anim6.png deleted file mode 100644 index 32c452c..0000000 Binary files a/s5e-charge/assets/walkie_talkie/history_anim/anim6.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/history_anim/anim7.png b/s5e-charge/assets/walkie_talkie/history_anim/anim7.png deleted file mode 100644 index 38d3985..0000000 Binary files a/s5e-charge/assets/walkie_talkie/history_anim/anim7.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/history_anim/anim8.png b/s5e-charge/assets/walkie_talkie/history_anim/anim8.png deleted file mode 100644 index 927d24a..0000000 Binary files a/s5e-charge/assets/walkie_talkie/history_anim/anim8.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/history_anim/anim9.png b/s5e-charge/assets/walkie_talkie/history_anim/anim9.png deleted file mode 100644 index 6d5503a..0000000 Binary files a/s5e-charge/assets/walkie_talkie/history_anim/anim9.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/history_anim_right/anim0.png b/s5e-charge/assets/walkie_talkie/history_anim_right/anim0.png deleted file mode 100644 index cdbf172..0000000 Binary files a/s5e-charge/assets/walkie_talkie/history_anim_right/anim0.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/history_anim_right/anim1.png b/s5e-charge/assets/walkie_talkie/history_anim_right/anim1.png deleted file mode 100644 index 5d99f04..0000000 Binary files a/s5e-charge/assets/walkie_talkie/history_anim_right/anim1.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/history_anim_right/anim10.png b/s5e-charge/assets/walkie_talkie/history_anim_right/anim10.png deleted file mode 100644 index 493b920..0000000 Binary files a/s5e-charge/assets/walkie_talkie/history_anim_right/anim10.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/history_anim_right/anim11.png b/s5e-charge/assets/walkie_talkie/history_anim_right/anim11.png deleted file mode 100644 index e596207..0000000 Binary files a/s5e-charge/assets/walkie_talkie/history_anim_right/anim11.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/history_anim_right/anim12.png b/s5e-charge/assets/walkie_talkie/history_anim_right/anim12.png deleted file mode 100644 index 44ccabf..0000000 Binary files a/s5e-charge/assets/walkie_talkie/history_anim_right/anim12.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/history_anim_right/anim13.png b/s5e-charge/assets/walkie_talkie/history_anim_right/anim13.png deleted file mode 100644 index cc6d96c..0000000 Binary files a/s5e-charge/assets/walkie_talkie/history_anim_right/anim13.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/history_anim_right/anim14.png b/s5e-charge/assets/walkie_talkie/history_anim_right/anim14.png deleted file mode 100644 index 709fe42..0000000 Binary files a/s5e-charge/assets/walkie_talkie/history_anim_right/anim14.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/history_anim_right/anim15.png b/s5e-charge/assets/walkie_talkie/history_anim_right/anim15.png deleted file mode 100644 index 08c909e..0000000 Binary files a/s5e-charge/assets/walkie_talkie/history_anim_right/anim15.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/history_anim_right/anim16.png b/s5e-charge/assets/walkie_talkie/history_anim_right/anim16.png deleted file mode 100644 index 5b5a34a..0000000 Binary files a/s5e-charge/assets/walkie_talkie/history_anim_right/anim16.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/history_anim_right/anim17.png b/s5e-charge/assets/walkie_talkie/history_anim_right/anim17.png deleted file mode 100644 index 5d99f04..0000000 Binary files a/s5e-charge/assets/walkie_talkie/history_anim_right/anim17.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/history_anim_right/anim18.png b/s5e-charge/assets/walkie_talkie/history_anim_right/anim18.png deleted file mode 100644 index cdbf172..0000000 Binary files a/s5e-charge/assets/walkie_talkie/history_anim_right/anim18.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/history_anim_right/anim2.png b/s5e-charge/assets/walkie_talkie/history_anim_right/anim2.png deleted file mode 100644 index 5b5a34a..0000000 Binary files a/s5e-charge/assets/walkie_talkie/history_anim_right/anim2.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/history_anim_right/anim3.png b/s5e-charge/assets/walkie_talkie/history_anim_right/anim3.png deleted file mode 100644 index 08c909e..0000000 Binary files a/s5e-charge/assets/walkie_talkie/history_anim_right/anim3.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/history_anim_right/anim4.png b/s5e-charge/assets/walkie_talkie/history_anim_right/anim4.png deleted file mode 100644 index 709fe42..0000000 Binary files a/s5e-charge/assets/walkie_talkie/history_anim_right/anim4.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/history_anim_right/anim5.png b/s5e-charge/assets/walkie_talkie/history_anim_right/anim5.png deleted file mode 100644 index cc6d96c..0000000 Binary files a/s5e-charge/assets/walkie_talkie/history_anim_right/anim5.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/history_anim_right/anim6.png b/s5e-charge/assets/walkie_talkie/history_anim_right/anim6.png deleted file mode 100644 index 44ccabf..0000000 Binary files a/s5e-charge/assets/walkie_talkie/history_anim_right/anim6.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/history_anim_right/anim7.png b/s5e-charge/assets/walkie_talkie/history_anim_right/anim7.png deleted file mode 100644 index e596207..0000000 Binary files a/s5e-charge/assets/walkie_talkie/history_anim_right/anim7.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/history_anim_right/anim8.png b/s5e-charge/assets/walkie_talkie/history_anim_right/anim8.png deleted file mode 100644 index 493b920..0000000 Binary files a/s5e-charge/assets/walkie_talkie/history_anim_right/anim8.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/history_anim_right/anim9.png b/s5e-charge/assets/walkie_talkie/history_anim_right/anim9.png deleted file mode 100644 index 2286ae4..0000000 Binary files a/s5e-charge/assets/walkie_talkie/history_anim_right/anim9.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/icon_plus.png b/s5e-charge/assets/walkie_talkie/icon_plus.png deleted file mode 100644 index a1117ab..0000000 Binary files a/s5e-charge/assets/walkie_talkie/icon_plus.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/image/Speaker0.png b/s5e-charge/assets/walkie_talkie/image/Speaker0.png deleted file mode 100644 index fc91da7..0000000 Binary files a/s5e-charge/assets/walkie_talkie/image/Speaker0.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/image/Speaker1.png b/s5e-charge/assets/walkie_talkie/image/Speaker1.png deleted file mode 100644 index 66fc559..0000000 Binary files a/s5e-charge/assets/walkie_talkie/image/Speaker1.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/image/Speaker10.png b/s5e-charge/assets/walkie_talkie/image/Speaker10.png deleted file mode 100644 index 2bb9b59..0000000 Binary files a/s5e-charge/assets/walkie_talkie/image/Speaker10.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/image/Speaker100.png b/s5e-charge/assets/walkie_talkie/image/Speaker100.png deleted file mode 100644 index 521ed3e..0000000 Binary files a/s5e-charge/assets/walkie_talkie/image/Speaker100.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/image/Speaker101.png b/s5e-charge/assets/walkie_talkie/image/Speaker101.png deleted file mode 100644 index 9b955dd..0000000 Binary files a/s5e-charge/assets/walkie_talkie/image/Speaker101.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/image/Speaker102.png b/s5e-charge/assets/walkie_talkie/image/Speaker102.png deleted file mode 100644 index 7825c03..0000000 Binary files a/s5e-charge/assets/walkie_talkie/image/Speaker102.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/image/Speaker11.png b/s5e-charge/assets/walkie_talkie/image/Speaker11.png deleted file mode 100644 index c4415dd..0000000 Binary files a/s5e-charge/assets/walkie_talkie/image/Speaker11.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/image/Speaker12.png b/s5e-charge/assets/walkie_talkie/image/Speaker12.png deleted file mode 100644 index 844818b..0000000 Binary files a/s5e-charge/assets/walkie_talkie/image/Speaker12.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/image/Speaker13.png b/s5e-charge/assets/walkie_talkie/image/Speaker13.png deleted file mode 100644 index 89029fd..0000000 Binary files a/s5e-charge/assets/walkie_talkie/image/Speaker13.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/image/Speaker14.png b/s5e-charge/assets/walkie_talkie/image/Speaker14.png deleted file mode 100644 index 221f19a..0000000 Binary files a/s5e-charge/assets/walkie_talkie/image/Speaker14.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/image/Speaker15.png b/s5e-charge/assets/walkie_talkie/image/Speaker15.png deleted file mode 100644 index 4423df7..0000000 Binary files a/s5e-charge/assets/walkie_talkie/image/Speaker15.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/image/Speaker16.png b/s5e-charge/assets/walkie_talkie/image/Speaker16.png deleted file mode 100644 index bd8aa8a..0000000 Binary files a/s5e-charge/assets/walkie_talkie/image/Speaker16.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/image/Speaker17.png b/s5e-charge/assets/walkie_talkie/image/Speaker17.png deleted file mode 100644 index 9ea2e8b..0000000 Binary files a/s5e-charge/assets/walkie_talkie/image/Speaker17.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/image/Speaker18.png b/s5e-charge/assets/walkie_talkie/image/Speaker18.png deleted file mode 100644 index fb5a10b..0000000 Binary files a/s5e-charge/assets/walkie_talkie/image/Speaker18.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/image/Speaker19.png b/s5e-charge/assets/walkie_talkie/image/Speaker19.png deleted file mode 100644 index 3ad3bf7..0000000 Binary files a/s5e-charge/assets/walkie_talkie/image/Speaker19.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/image/Speaker2.png b/s5e-charge/assets/walkie_talkie/image/Speaker2.png deleted file mode 100644 index 8601636..0000000 Binary files a/s5e-charge/assets/walkie_talkie/image/Speaker2.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/image/Speaker20.png b/s5e-charge/assets/walkie_talkie/image/Speaker20.png deleted file mode 100644 index b6ea25b..0000000 Binary files a/s5e-charge/assets/walkie_talkie/image/Speaker20.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/image/Speaker21.png b/s5e-charge/assets/walkie_talkie/image/Speaker21.png deleted file mode 100644 index 14200fd..0000000 Binary files a/s5e-charge/assets/walkie_talkie/image/Speaker21.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/image/Speaker22.png b/s5e-charge/assets/walkie_talkie/image/Speaker22.png deleted file mode 100644 index 3d9c48f..0000000 Binary files a/s5e-charge/assets/walkie_talkie/image/Speaker22.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/image/Speaker23.png b/s5e-charge/assets/walkie_talkie/image/Speaker23.png deleted file mode 100644 index e91fd3d..0000000 Binary files a/s5e-charge/assets/walkie_talkie/image/Speaker23.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/image/Speaker24.png b/s5e-charge/assets/walkie_talkie/image/Speaker24.png deleted file mode 100644 index 393fc07..0000000 Binary files a/s5e-charge/assets/walkie_talkie/image/Speaker24.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/image/Speaker25.png b/s5e-charge/assets/walkie_talkie/image/Speaker25.png deleted file mode 100644 index fa9e68e..0000000 Binary files a/s5e-charge/assets/walkie_talkie/image/Speaker25.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/image/Speaker26.png b/s5e-charge/assets/walkie_talkie/image/Speaker26.png deleted file mode 100644 index 6d43f7d..0000000 Binary files a/s5e-charge/assets/walkie_talkie/image/Speaker26.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/image/Speaker27.png b/s5e-charge/assets/walkie_talkie/image/Speaker27.png deleted file mode 100644 index 0a65418..0000000 Binary files a/s5e-charge/assets/walkie_talkie/image/Speaker27.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/image/Speaker28.png b/s5e-charge/assets/walkie_talkie/image/Speaker28.png deleted file mode 100644 index b0160e4..0000000 Binary files a/s5e-charge/assets/walkie_talkie/image/Speaker28.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/image/Speaker29.png b/s5e-charge/assets/walkie_talkie/image/Speaker29.png deleted file mode 100644 index 1a8c275..0000000 Binary files a/s5e-charge/assets/walkie_talkie/image/Speaker29.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/image/Speaker3.png b/s5e-charge/assets/walkie_talkie/image/Speaker3.png deleted file mode 100644 index 142da0d..0000000 Binary files a/s5e-charge/assets/walkie_talkie/image/Speaker3.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/image/Speaker30.png b/s5e-charge/assets/walkie_talkie/image/Speaker30.png deleted file mode 100644 index c07d68b..0000000 Binary files a/s5e-charge/assets/walkie_talkie/image/Speaker30.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/image/Speaker31.png b/s5e-charge/assets/walkie_talkie/image/Speaker31.png deleted file mode 100644 index b5bf877..0000000 Binary files a/s5e-charge/assets/walkie_talkie/image/Speaker31.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/image/Speaker32.png b/s5e-charge/assets/walkie_talkie/image/Speaker32.png deleted file mode 100644 index c7635aa..0000000 Binary files a/s5e-charge/assets/walkie_talkie/image/Speaker32.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/image/Speaker33.png b/s5e-charge/assets/walkie_talkie/image/Speaker33.png deleted file mode 100644 index 55c5c23..0000000 Binary files a/s5e-charge/assets/walkie_talkie/image/Speaker33.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/image/Speaker34.png b/s5e-charge/assets/walkie_talkie/image/Speaker34.png deleted file mode 100644 index 8291262..0000000 Binary files a/s5e-charge/assets/walkie_talkie/image/Speaker34.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/image/Speaker35.png b/s5e-charge/assets/walkie_talkie/image/Speaker35.png deleted file mode 100644 index 7299baa..0000000 Binary files a/s5e-charge/assets/walkie_talkie/image/Speaker35.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/image/Speaker36.png b/s5e-charge/assets/walkie_talkie/image/Speaker36.png deleted file mode 100644 index ab8b08e..0000000 Binary files a/s5e-charge/assets/walkie_talkie/image/Speaker36.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/image/Speaker37.png b/s5e-charge/assets/walkie_talkie/image/Speaker37.png deleted file mode 100644 index 5f76111..0000000 Binary files a/s5e-charge/assets/walkie_talkie/image/Speaker37.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/image/Speaker38.png b/s5e-charge/assets/walkie_talkie/image/Speaker38.png deleted file mode 100644 index 41cb88e..0000000 Binary files a/s5e-charge/assets/walkie_talkie/image/Speaker38.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/image/Speaker39.png b/s5e-charge/assets/walkie_talkie/image/Speaker39.png deleted file mode 100644 index 72a5863..0000000 Binary files a/s5e-charge/assets/walkie_talkie/image/Speaker39.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/image/Speaker4.png b/s5e-charge/assets/walkie_talkie/image/Speaker4.png deleted file mode 100644 index c7ad479..0000000 Binary files a/s5e-charge/assets/walkie_talkie/image/Speaker4.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/image/Speaker40.png b/s5e-charge/assets/walkie_talkie/image/Speaker40.png deleted file mode 100644 index 81c7391..0000000 Binary files a/s5e-charge/assets/walkie_talkie/image/Speaker40.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/image/Speaker41.png b/s5e-charge/assets/walkie_talkie/image/Speaker41.png deleted file mode 100644 index 9881ad6..0000000 Binary files a/s5e-charge/assets/walkie_talkie/image/Speaker41.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/image/Speaker42.png b/s5e-charge/assets/walkie_talkie/image/Speaker42.png deleted file mode 100644 index 7e9ec32..0000000 Binary files a/s5e-charge/assets/walkie_talkie/image/Speaker42.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/image/Speaker43.png b/s5e-charge/assets/walkie_talkie/image/Speaker43.png deleted file mode 100644 index 216afb2..0000000 Binary files a/s5e-charge/assets/walkie_talkie/image/Speaker43.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/image/Speaker44.png b/s5e-charge/assets/walkie_talkie/image/Speaker44.png deleted file mode 100644 index 873728e..0000000 Binary files a/s5e-charge/assets/walkie_talkie/image/Speaker44.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/image/Speaker45.png b/s5e-charge/assets/walkie_talkie/image/Speaker45.png deleted file mode 100644 index 70535de..0000000 Binary files a/s5e-charge/assets/walkie_talkie/image/Speaker45.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/image/Speaker46.png b/s5e-charge/assets/walkie_talkie/image/Speaker46.png deleted file mode 100644 index 43563a3..0000000 Binary files a/s5e-charge/assets/walkie_talkie/image/Speaker46.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/image/Speaker47.png b/s5e-charge/assets/walkie_talkie/image/Speaker47.png deleted file mode 100644 index f7bd1b7..0000000 Binary files a/s5e-charge/assets/walkie_talkie/image/Speaker47.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/image/Speaker48.png b/s5e-charge/assets/walkie_talkie/image/Speaker48.png deleted file mode 100644 index 6a942f9..0000000 Binary files a/s5e-charge/assets/walkie_talkie/image/Speaker48.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/image/Speaker49.png b/s5e-charge/assets/walkie_talkie/image/Speaker49.png deleted file mode 100644 index f65235c..0000000 Binary files a/s5e-charge/assets/walkie_talkie/image/Speaker49.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/image/Speaker5.png b/s5e-charge/assets/walkie_talkie/image/Speaker5.png deleted file mode 100644 index 8681f10..0000000 Binary files a/s5e-charge/assets/walkie_talkie/image/Speaker5.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/image/Speaker50.png b/s5e-charge/assets/walkie_talkie/image/Speaker50.png deleted file mode 100644 index be90a63..0000000 Binary files a/s5e-charge/assets/walkie_talkie/image/Speaker50.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/image/Speaker51.png b/s5e-charge/assets/walkie_talkie/image/Speaker51.png deleted file mode 100644 index d6de73f..0000000 Binary files a/s5e-charge/assets/walkie_talkie/image/Speaker51.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/image/Speaker52.png b/s5e-charge/assets/walkie_talkie/image/Speaker52.png deleted file mode 100644 index 8b58b2d..0000000 Binary files a/s5e-charge/assets/walkie_talkie/image/Speaker52.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/image/Speaker53.png b/s5e-charge/assets/walkie_talkie/image/Speaker53.png deleted file mode 100644 index 7b3b206..0000000 Binary files a/s5e-charge/assets/walkie_talkie/image/Speaker53.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/image/Speaker54.png b/s5e-charge/assets/walkie_talkie/image/Speaker54.png deleted file mode 100644 index 2367008..0000000 Binary files a/s5e-charge/assets/walkie_talkie/image/Speaker54.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/image/Speaker55.png b/s5e-charge/assets/walkie_talkie/image/Speaker55.png deleted file mode 100644 index dbdf016..0000000 Binary files a/s5e-charge/assets/walkie_talkie/image/Speaker55.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/image/Speaker56.png b/s5e-charge/assets/walkie_talkie/image/Speaker56.png deleted file mode 100644 index 1d3eacb..0000000 Binary files a/s5e-charge/assets/walkie_talkie/image/Speaker56.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/image/Speaker57.png b/s5e-charge/assets/walkie_talkie/image/Speaker57.png deleted file mode 100644 index 97c19e3..0000000 Binary files a/s5e-charge/assets/walkie_talkie/image/Speaker57.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/image/Speaker58.png b/s5e-charge/assets/walkie_talkie/image/Speaker58.png deleted file mode 100644 index a7195d0..0000000 Binary files a/s5e-charge/assets/walkie_talkie/image/Speaker58.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/image/Speaker59.png b/s5e-charge/assets/walkie_talkie/image/Speaker59.png deleted file mode 100644 index 3e36cba..0000000 Binary files a/s5e-charge/assets/walkie_talkie/image/Speaker59.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/image/Speaker6.png b/s5e-charge/assets/walkie_talkie/image/Speaker6.png deleted file mode 100644 index 2503467..0000000 Binary files a/s5e-charge/assets/walkie_talkie/image/Speaker6.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/image/Speaker60.png b/s5e-charge/assets/walkie_talkie/image/Speaker60.png deleted file mode 100644 index cb0e1b9..0000000 Binary files a/s5e-charge/assets/walkie_talkie/image/Speaker60.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/image/Speaker61.png b/s5e-charge/assets/walkie_talkie/image/Speaker61.png deleted file mode 100644 index 93b8905..0000000 Binary files a/s5e-charge/assets/walkie_talkie/image/Speaker61.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/image/Speaker62.png b/s5e-charge/assets/walkie_talkie/image/Speaker62.png deleted file mode 100644 index d171dab..0000000 Binary files a/s5e-charge/assets/walkie_talkie/image/Speaker62.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/image/Speaker63.png b/s5e-charge/assets/walkie_talkie/image/Speaker63.png deleted file mode 100644 index 3bdfd0f..0000000 Binary files a/s5e-charge/assets/walkie_talkie/image/Speaker63.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/image/Speaker64.png b/s5e-charge/assets/walkie_talkie/image/Speaker64.png deleted file mode 100644 index 392093f..0000000 Binary files a/s5e-charge/assets/walkie_talkie/image/Speaker64.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/image/Speaker65.png b/s5e-charge/assets/walkie_talkie/image/Speaker65.png deleted file mode 100644 index e26ca7c..0000000 Binary files a/s5e-charge/assets/walkie_talkie/image/Speaker65.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/image/Speaker66.png b/s5e-charge/assets/walkie_talkie/image/Speaker66.png deleted file mode 100644 index 57aff14..0000000 Binary files a/s5e-charge/assets/walkie_talkie/image/Speaker66.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/image/Speaker67.png b/s5e-charge/assets/walkie_talkie/image/Speaker67.png deleted file mode 100644 index 2b7281a..0000000 Binary files a/s5e-charge/assets/walkie_talkie/image/Speaker67.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/image/Speaker68.png b/s5e-charge/assets/walkie_talkie/image/Speaker68.png deleted file mode 100644 index cdb5592..0000000 Binary files a/s5e-charge/assets/walkie_talkie/image/Speaker68.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/image/Speaker69.png b/s5e-charge/assets/walkie_talkie/image/Speaker69.png deleted file mode 100644 index dc7eba5..0000000 Binary files a/s5e-charge/assets/walkie_talkie/image/Speaker69.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/image/Speaker7.png b/s5e-charge/assets/walkie_talkie/image/Speaker7.png deleted file mode 100644 index 054a489..0000000 Binary files a/s5e-charge/assets/walkie_talkie/image/Speaker7.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/image/Speaker70.png b/s5e-charge/assets/walkie_talkie/image/Speaker70.png deleted file mode 100644 index 6bbe93d..0000000 Binary files a/s5e-charge/assets/walkie_talkie/image/Speaker70.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/image/Speaker71.png b/s5e-charge/assets/walkie_talkie/image/Speaker71.png deleted file mode 100644 index bc451fb..0000000 Binary files a/s5e-charge/assets/walkie_talkie/image/Speaker71.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/image/Speaker72.png b/s5e-charge/assets/walkie_talkie/image/Speaker72.png deleted file mode 100644 index 69b5ee7..0000000 Binary files a/s5e-charge/assets/walkie_talkie/image/Speaker72.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/image/Speaker73.png b/s5e-charge/assets/walkie_talkie/image/Speaker73.png deleted file mode 100644 index 909de00..0000000 Binary files a/s5e-charge/assets/walkie_talkie/image/Speaker73.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/image/Speaker74.png b/s5e-charge/assets/walkie_talkie/image/Speaker74.png deleted file mode 100644 index a2d65b2..0000000 Binary files a/s5e-charge/assets/walkie_talkie/image/Speaker74.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/image/Speaker75.png b/s5e-charge/assets/walkie_talkie/image/Speaker75.png deleted file mode 100644 index 77d3cf6..0000000 Binary files a/s5e-charge/assets/walkie_talkie/image/Speaker75.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/image/Speaker76.png b/s5e-charge/assets/walkie_talkie/image/Speaker76.png deleted file mode 100644 index 4bacc59..0000000 Binary files a/s5e-charge/assets/walkie_talkie/image/Speaker76.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/image/Speaker77.png b/s5e-charge/assets/walkie_talkie/image/Speaker77.png deleted file mode 100644 index 3527380..0000000 Binary files a/s5e-charge/assets/walkie_talkie/image/Speaker77.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/image/Speaker78.png b/s5e-charge/assets/walkie_talkie/image/Speaker78.png deleted file mode 100644 index f9cfe31..0000000 Binary files a/s5e-charge/assets/walkie_talkie/image/Speaker78.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/image/Speaker79.png b/s5e-charge/assets/walkie_talkie/image/Speaker79.png deleted file mode 100644 index 4583948..0000000 Binary files a/s5e-charge/assets/walkie_talkie/image/Speaker79.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/image/Speaker8.png b/s5e-charge/assets/walkie_talkie/image/Speaker8.png deleted file mode 100644 index 6b885ec..0000000 Binary files a/s5e-charge/assets/walkie_talkie/image/Speaker8.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/image/Speaker80.png b/s5e-charge/assets/walkie_talkie/image/Speaker80.png deleted file mode 100644 index 6dbe5d3..0000000 Binary files a/s5e-charge/assets/walkie_talkie/image/Speaker80.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/image/Speaker81.png b/s5e-charge/assets/walkie_talkie/image/Speaker81.png deleted file mode 100644 index cdff971..0000000 Binary files a/s5e-charge/assets/walkie_talkie/image/Speaker81.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/image/Speaker82.png b/s5e-charge/assets/walkie_talkie/image/Speaker82.png deleted file mode 100644 index 304ccea..0000000 Binary files a/s5e-charge/assets/walkie_talkie/image/Speaker82.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/image/Speaker83.png b/s5e-charge/assets/walkie_talkie/image/Speaker83.png deleted file mode 100644 index fe62e25..0000000 Binary files a/s5e-charge/assets/walkie_talkie/image/Speaker83.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/image/Speaker84.png b/s5e-charge/assets/walkie_talkie/image/Speaker84.png deleted file mode 100644 index 263f40c..0000000 Binary files a/s5e-charge/assets/walkie_talkie/image/Speaker84.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/image/Speaker85.png b/s5e-charge/assets/walkie_talkie/image/Speaker85.png deleted file mode 100644 index 060ae88..0000000 Binary files a/s5e-charge/assets/walkie_talkie/image/Speaker85.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/image/Speaker86.png b/s5e-charge/assets/walkie_talkie/image/Speaker86.png deleted file mode 100644 index c8fec2a..0000000 Binary files a/s5e-charge/assets/walkie_talkie/image/Speaker86.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/image/Speaker87.png b/s5e-charge/assets/walkie_talkie/image/Speaker87.png deleted file mode 100644 index 053ee6d..0000000 Binary files a/s5e-charge/assets/walkie_talkie/image/Speaker87.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/image/Speaker88.png b/s5e-charge/assets/walkie_talkie/image/Speaker88.png deleted file mode 100644 index 15505f5..0000000 Binary files a/s5e-charge/assets/walkie_talkie/image/Speaker88.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/image/Speaker89.png b/s5e-charge/assets/walkie_talkie/image/Speaker89.png deleted file mode 100644 index 162e637..0000000 Binary files a/s5e-charge/assets/walkie_talkie/image/Speaker89.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/image/Speaker9.png b/s5e-charge/assets/walkie_talkie/image/Speaker9.png deleted file mode 100644 index 03b9b1e..0000000 Binary files a/s5e-charge/assets/walkie_talkie/image/Speaker9.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/image/Speaker90.png b/s5e-charge/assets/walkie_talkie/image/Speaker90.png deleted file mode 100644 index ceec461..0000000 Binary files a/s5e-charge/assets/walkie_talkie/image/Speaker90.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/image/Speaker91.png b/s5e-charge/assets/walkie_talkie/image/Speaker91.png deleted file mode 100644 index b3b35ae..0000000 Binary files a/s5e-charge/assets/walkie_talkie/image/Speaker91.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/image/Speaker92.png b/s5e-charge/assets/walkie_talkie/image/Speaker92.png deleted file mode 100644 index 3464b7d..0000000 Binary files a/s5e-charge/assets/walkie_talkie/image/Speaker92.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/image/Speaker93.png b/s5e-charge/assets/walkie_talkie/image/Speaker93.png deleted file mode 100644 index de800f6..0000000 Binary files a/s5e-charge/assets/walkie_talkie/image/Speaker93.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/image/Speaker94.png b/s5e-charge/assets/walkie_talkie/image/Speaker94.png deleted file mode 100644 index 51e6c2a..0000000 Binary files a/s5e-charge/assets/walkie_talkie/image/Speaker94.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/image/Speaker95.png b/s5e-charge/assets/walkie_talkie/image/Speaker95.png deleted file mode 100644 index a22dffe..0000000 Binary files a/s5e-charge/assets/walkie_talkie/image/Speaker95.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/image/Speaker96.png b/s5e-charge/assets/walkie_talkie/image/Speaker96.png deleted file mode 100644 index f7151aa..0000000 Binary files a/s5e-charge/assets/walkie_talkie/image/Speaker96.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/image/Speaker97.png b/s5e-charge/assets/walkie_talkie/image/Speaker97.png deleted file mode 100644 index eae1262..0000000 Binary files a/s5e-charge/assets/walkie_talkie/image/Speaker97.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/image/Speaker98.png b/s5e-charge/assets/walkie_talkie/image/Speaker98.png deleted file mode 100644 index 328931c..0000000 Binary files a/s5e-charge/assets/walkie_talkie/image/Speaker98.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/image/Speaker99.png b/s5e-charge/assets/walkie_talkie/image/Speaker99.png deleted file mode 100644 index 11a494e..0000000 Binary files a/s5e-charge/assets/walkie_talkie/image/Speaker99.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/image/go_next.png b/s5e-charge/assets/walkie_talkie/image/go_next.png deleted file mode 100644 index e9a1d81..0000000 Binary files a/s5e-charge/assets/walkie_talkie/image/go_next.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/image/stop.png b/s5e-charge/assets/walkie_talkie/image/stop.png deleted file mode 100644 index b611709..0000000 Binary files a/s5e-charge/assets/walkie_talkie/image/stop.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/launcher.png b/s5e-charge/assets/walkie_talkie/launcher.png deleted file mode 100644 index 9a611a9..0000000 Binary files a/s5e-charge/assets/walkie_talkie/launcher.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/launcher1.png b/s5e-charge/assets/walkie_talkie/launcher1.png deleted file mode 100644 index 60aa469..0000000 Binary files a/s5e-charge/assets/walkie_talkie/launcher1.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/listen.png b/s5e-charge/assets/walkie_talkie/listen.png deleted file mode 100644 index e06f591..0000000 Binary files a/s5e-charge/assets/walkie_talkie/listen.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/login_success.png b/s5e-charge/assets/walkie_talkie/login_success.png deleted file mode 100644 index 6e8c826..0000000 Binary files a/s5e-charge/assets/walkie_talkie/login_success.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/next_btn.png b/s5e-charge/assets/walkie_talkie/next_btn.png deleted file mode 100644 index 415abc0..0000000 Binary files a/s5e-charge/assets/walkie_talkie/next_btn.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/poc/default_icon.png b/s5e-charge/assets/walkie_talkie/poc/default_icon.png deleted file mode 100644 index d5c33f0..0000000 Binary files a/s5e-charge/assets/walkie_talkie/poc/default_icon.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/poc/group_icon1.png b/s5e-charge/assets/walkie_talkie/poc/group_icon1.png deleted file mode 100644 index 9f22404..0000000 Binary files a/s5e-charge/assets/walkie_talkie/poc/group_icon1.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/poc/group_icon2.png b/s5e-charge/assets/walkie_talkie/poc/group_icon2.png deleted file mode 100644 index 0adef53..0000000 Binary files a/s5e-charge/assets/walkie_talkie/poc/group_icon2.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/poc/group_icon3.png b/s5e-charge/assets/walkie_talkie/poc/group_icon3.png deleted file mode 100644 index 4f1560b..0000000 Binary files a/s5e-charge/assets/walkie_talkie/poc/group_icon3.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/poc/group_icon4.png b/s5e-charge/assets/walkie_talkie/poc/group_icon4.png deleted file mode 100644 index 5bb064a..0000000 Binary files a/s5e-charge/assets/walkie_talkie/poc/group_icon4.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/poc/group_icon5.png b/s5e-charge/assets/walkie_talkie/poc/group_icon5.png deleted file mode 100644 index abd170d..0000000 Binary files a/s5e-charge/assets/walkie_talkie/poc/group_icon5.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/poc/member_icon.png b/s5e-charge/assets/walkie_talkie/poc/member_icon.png deleted file mode 100644 index d5c33f0..0000000 Binary files a/s5e-charge/assets/walkie_talkie/poc/member_icon.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/poc/no_sound.png b/s5e-charge/assets/walkie_talkie/poc/no_sound.png deleted file mode 100644 index 2244475..0000000 Binary files a/s5e-charge/assets/walkie_talkie/poc/no_sound.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/poc/say_button_mic_icon.png b/s5e-charge/assets/walkie_talkie/poc/say_button_mic_icon.png deleted file mode 100644 index 64e5ea1..0000000 Binary files a/s5e-charge/assets/walkie_talkie/poc/say_button_mic_icon.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/poc/sound.png b/s5e-charge/assets/walkie_talkie/poc/sound.png deleted file mode 100644 index 9b7b453..0000000 Binary files a/s5e-charge/assets/walkie_talkie/poc/sound.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/poc/talk_listen.png b/s5e-charge/assets/walkie_talkie/poc/talk_listen.png deleted file mode 100644 index c9a9688..0000000 Binary files a/s5e-charge/assets/walkie_talkie/poc/talk_listen.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/poc/toast_listen.png b/s5e-charge/assets/walkie_talkie/poc/toast_listen.png deleted file mode 100644 index c9a9688..0000000 Binary files a/s5e-charge/assets/walkie_talkie/poc/toast_listen.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/poc/toast_say.png b/s5e-charge/assets/walkie_talkie/poc/toast_say.png deleted file mode 100644 index f5b5dec..0000000 Binary files a/s5e-charge/assets/walkie_talkie/poc/toast_say.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/poc/tools.png b/s5e-charge/assets/walkie_talkie/poc/tools.png deleted file mode 100644 index e7cf9be..0000000 Binary files a/s5e-charge/assets/walkie_talkie/poc/tools.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/poc/voice_icon.png b/s5e-charge/assets/walkie_talkie/poc/voice_icon.png deleted file mode 100644 index 85e366d..0000000 Binary files a/s5e-charge/assets/walkie_talkie/poc/voice_icon.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/poc/voice_icon_rt.png b/s5e-charge/assets/walkie_talkie/poc/voice_icon_rt.png deleted file mode 100644 index 501320a..0000000 Binary files a/s5e-charge/assets/walkie_talkie/poc/voice_icon_rt.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/speak.png b/s5e-charge/assets/walkie_talkie/speak.png deleted file mode 100644 index 1b715e3..0000000 Binary files a/s5e-charge/assets/walkie_talkie/speak.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/walkie_talkie.png b/s5e-charge/assets/walkie_talkie/walkie_talkie.png deleted file mode 100644 index 9a611a9..0000000 Binary files a/s5e-charge/assets/walkie_talkie/walkie_talkie.png and /dev/null differ diff --git a/s5e-charge/assets/walkie_talkie/walkie_toast_icon.png b/s5e-charge/assets/walkie_talkie/walkie_toast_icon.png deleted file mode 100644 index 81f4e3f..0000000 Binary files a/s5e-charge/assets/walkie_talkie/walkie_toast_icon.png and /dev/null differ diff --git a/s5e-charge/dist/S5-Pt3.face b/s5e-charge/dist/S5-Pt3.face deleted file mode 100644 index 5e21b0d..0000000 Binary files a/s5e-charge/dist/S5-Pt3.face and /dev/null differ diff --git a/s5e-charge/dist/S5ChgRestore.face b/s5e-charge/dist/S5ChgRestore.face deleted file mode 100644 index a149336..0000000 Binary files a/s5e-charge/dist/S5ChgRestore.face and /dev/null differ diff --git a/s5e-charge/docs/before_after.png b/s5e-charge/docs/before_after.png deleted file mode 100644 index f915420..0000000 Binary files a/s5e-charge/docs/before_after.png and /dev/null differ diff --git a/s5e-charge/docs/decode_preview.png b/s5e-charge/docs/decode_preview.png deleted file mode 100644 index 2391033..0000000 Binary files a/s5e-charge/docs/decode_preview.png and /dev/null differ diff --git a/s5e-charge/docs/face_before_after.png b/s5e-charge/docs/face_before_after.png deleted file mode 100644 index b9d1f19..0000000 Binary files a/s5e-charge/docs/face_before_after.png and /dev/null differ diff --git a/s5e-charge/docs/logo_preview_480.png b/s5e-charge/docs/logo_preview_480.png deleted file mode 100644 index d3b1071..0000000 Binary files a/s5e-charge/docs/logo_preview_480.png and /dev/null differ diff --git a/s5e-charge/docs/selfcheck_report.txt b/s5e-charge/docs/selfcheck_report.txt deleted file mode 100644 index 074aa08..0000000 --- a/s5e-charge/docs/selfcheck_report.txt +++ /dev/null @@ -1,67 +0,0 @@ -============================================================================== -S5eChg 独立总校验(只读 dist/*.face + inputs/*/vela_system.bin) -============================================================================== -目标镜像 vela_system.bin:84429824 字节,charging/ 槽位 61 个 -源镜像 vela_system.bin:61 帧 - ---- A 交付物形态 --- - S5Chg.face 11987588 字节 sha256=83e7ac4d7f83f685b68632a3608ac6739d58e9d33d911d92fa495f4c5bb53882 - sha256[:16]=83e7ac4d7f83f685 - [OK ] replace 魔数正确 - [OK ] replace id == 462150103 b'462150103\x00' - [OK ] replace b5 == 0 b5=0 - [OK ] replace 体积 < 20MB - [OK ] replace 占位符已回填 - S5ChgRestore.face 11987588 字节 sha256=098cb9570436079d492fe96fe841ae03903431f379feeb22c1230660d780284f - sha256[:16]=098cb9570436079d - [OK ] restore 魔数正确 - [OK ] restore id == 462150103 b'462150103\x00' - [OK ] restore b5 == 0 b5=0 - [OK ] restore 体积 < 20MB - [OK ] restore 占位符已回填 - ---- B Lua 表(从 .face 反解)--- - [OK ] replace 找到 pay / rb 表 - replace:pay 61 条 / rb 61 条 - [OK ] replace pay 条数 == 61 61 - [OK ] replace rb 条数 == 61 61 - [OK ] replace Lua 里的 ROLE == replace - [OK ] restore 找到 pay / rb 表 - restore:pay 61 条 / rb 61 条 - [OK ] restore pay 条数 == 61 61 - [OK ] restore rb 条数 == 61 61 - [OK ] restore Lua 里的 ROLE == restore - ---- C 等长 + 目标偏移 / D 可解码(480x480,恰好 usize)--- - [OK ] replace:61 条长度 == 原厂槽位长度 不符 0 条 - [OK ] replace:目标偏移一一对应 chargeN 错位 0 条 - [OK ] replace:61 条都解出 480x480 且 RLE 恰好 usize 不符 0 条 - [OK ] restore:61 条长度 == 原厂槽位长度 不符 0 条 - [OK ] restore:目标偏移一一对应 chargeN 错位 0 条 - [OK ] restore:61 条都解出 480x480 且 RLE 恰好 usize 不符 0 条 - ---- E 帧内容:与 q63 源帧的独立 Lanczos 放大比对 --- - 61 帧平均差:mean 0.189 max 0.260(第 17 帧) - [OK ] 每一帧与独立 Lanczos 基准的平均差 < 1.0/255 max=0.260 - 最像的源帧号与自身不一致的帧:0 个 - [OK ] 交付物第 n 帧就是 q63 第 n 帧(最多容忍 2 帧近邻歧义) 0 个不一致 - ---- F 版本指纹 / 还原字节 --- - [OK ] replace 盘 rb == 原厂前 64 B(对固件镜像比对) 不符 0 条 - [OK ] restore 盘 pay == 原厂整条字节(逐字节) 不符 0 条 - [OK ] restore 盘 rb == replace 盘 pay 前 64 B(两份交付物互证) 不符 0 条 - ---- G 整镜像(用 replace 盘负载重建改后镜像)--- - [OK ] inode 表逐位不变(2150 条) - [OK ] superblock 逐位不变 size=84429200 cksum=3275251810 - [OK ] 分区长度不变 - [OK ] 除 61 个槽位外没有任何字节变化 差异 11585280 字节,槽位外 0 字节 - [info] 与构建中间产物逐字节一致:True - ---- H 预览图(交付物解出来的帧)--- - 61 帧总览:D:\UserData\Documents\GitHub\o62lte-customising-plan\s5e-charge\docs\decode_preview.png - 抽样对照:D:\UserData\Documents\GitHub\o62lte-customising-plan\s5e-charge\docs\face_before_after.png - -============================================================================== -RESULT: 全部通过 —— 33 项通过,0 项失败 -============================================================================== diff --git a/s5e-charge/docs/selfcheck_run.txt b/s5e-charge/docs/selfcheck_run.txt deleted file mode 100644 index 6752de1..0000000 Binary files a/s5e-charge/docs/selfcheck_run.txt and /dev/null differ diff --git a/s5e-charge/docs/ui_about.png b/s5e-charge/docs/ui_about.png deleted file mode 100644 index 40b0796..0000000 Binary files a/s5e-charge/docs/ui_about.png and /dev/null differ diff --git a/s5e-charge/docs/ui_all.png b/s5e-charge/docs/ui_all.png deleted file mode 100644 index 55471a3..0000000 Binary files a/s5e-charge/docs/ui_all.png and /dev/null differ diff --git a/s5e-charge/docs/ui_alt_about.png b/s5e-charge/docs/ui_alt_about.png deleted file mode 100644 index 85c6972..0000000 Binary files a/s5e-charge/docs/ui_alt_about.png and /dev/null differ diff --git a/s5e-charge/docs/ui_alt_confirm.png b/s5e-charge/docs/ui_alt_confirm.png deleted file mode 100644 index f500e25..0000000 Binary files a/s5e-charge/docs/ui_alt_confirm.png and /dev/null differ diff --git a/s5e-charge/docs/ui_alt_confirm_btn.png b/s5e-charge/docs/ui_alt_confirm_btn.png deleted file mode 100644 index 86923bd..0000000 Binary files a/s5e-charge/docs/ui_alt_confirm_btn.png and /dev/null differ diff --git a/s5e-charge/docs/ui_alt_help_d1.png b/s5e-charge/docs/ui_alt_help_d1.png deleted file mode 100644 index b0c9b0f..0000000 Binary files a/s5e-charge/docs/ui_alt_help_d1.png and /dev/null differ diff --git a/s5e-charge/docs/ui_alt_help_tip.png b/s5e-charge/docs/ui_alt_help_tip.png deleted file mode 100644 index 3406280..0000000 Binary files a/s5e-charge/docs/ui_alt_help_tip.png and /dev/null differ diff --git a/s5e-charge/docs/ui_alt_home.png b/s5e-charge/docs/ui_alt_home.png deleted file mode 100644 index cbe649a..0000000 Binary files a/s5e-charge/docs/ui_alt_home.png and /dev/null differ diff --git a/s5e-charge/docs/ui_alt_home_item2.png b/s5e-charge/docs/ui_alt_home_item2.png deleted file mode 100644 index 9bb88ad..0000000 Binary files a/s5e-charge/docs/ui_alt_home_item2.png and /dev/null differ diff --git a/s5e-charge/docs/ui_alt_running.png b/s5e-charge/docs/ui_alt_running.png deleted file mode 100644 index 2c3be6c..0000000 Binary files a/s5e-charge/docs/ui_alt_running.png and /dev/null differ diff --git a/s5e-charge/docs/ui_confirm.png b/s5e-charge/docs/ui_confirm.png deleted file mode 100644 index d431aed..0000000 Binary files a/s5e-charge/docs/ui_confirm.png and /dev/null differ diff --git a/s5e-charge/docs/ui_help.png b/s5e-charge/docs/ui_help.png deleted file mode 100644 index 5ea872f..0000000 Binary files a/s5e-charge/docs/ui_help.png and /dev/null differ diff --git a/s5e-charge/docs/ui_help_err.png b/s5e-charge/docs/ui_help_err.png deleted file mode 100644 index 280f198..0000000 Binary files a/s5e-charge/docs/ui_help_err.png and /dev/null differ diff --git a/s5e-charge/docs/ui_help_ok.png b/s5e-charge/docs/ui_help_ok.png deleted file mode 100644 index e0431d3..0000000 Binary files a/s5e-charge/docs/ui_help_ok.png and /dev/null differ diff --git a/s5e-charge/docs/ui_help_tip.png b/s5e-charge/docs/ui_help_tip.png deleted file mode 100644 index 8984672..0000000 Binary files a/s5e-charge/docs/ui_help_tip.png and /dev/null differ diff --git a/s5e-charge/docs/ui_home.png b/s5e-charge/docs/ui_home.png deleted file mode 100644 index 6453c98..0000000 Binary files a/s5e-charge/docs/ui_home.png and /dev/null differ diff --git a/s5e-charge/docs/ui_running.png b/s5e-charge/docs/ui_running.png deleted file mode 100644 index 8d42334..0000000 Binary files a/s5e-charge/docs/ui_running.png and /dev/null differ diff --git a/s5e-charge/docs/ui_running_end.png b/s5e-charge/docs/ui_running_end.png deleted file mode 100644 index e4340cb..0000000 Binary files a/s5e-charge/docs/ui_running_end.png and /dev/null differ diff --git a/s5e-charge/flash.sh b/s5e-charge/flash.sh deleted file mode 100644 index aff5738..0000000 --- a/s5e-charge/flash.sh +++ /dev/null @@ -1,319 +0,0 @@ -#!/bin/sh -# S5 41mm 充电动画 -> S5 eSIM 46mm(只写负载) -# 把 flash.sh 与 payload/ 放到同一个目录,例如 /data/s5panim/ -# 然后: sh /data/s5panim/flash.sh 最后: reboot -# 还原用 restore.sh 或安装还原盘 S5ChgRestore.face - -DEV=/dev/system -BASE=/data/s5panim/payload - -echo "target check" -ls -l $DEV - -echo "== charge0 == -dd if=$BASE/000_charge0.bin of=$DEV bs=1 seek=84231168 count=197959 -dd if=$DEV of=/tmp/chk.bin bs=1 skip=84231168 count=197959 -cmp /tmp/chk.bin $BASE/000_charge0.bin - -echo "== charge1 == -dd if=$BASE/001_charge1.bin of=$DEV bs=4 seek=21008000 count=49712 -dd if=$DEV of=/tmp/chk.bin bs=4 skip=21008000 count=49712 -cmp /tmp/chk.bin $BASE/001_charge1.bin - -echo "== charge2 == -dd if=$BASE/002_charge2.bin of=$DEV bs=4 seek=20459008 count=49835 -dd if=$DEV of=/tmp/chk.bin bs=4 skip=20459008 count=49835 -cmp /tmp/chk.bin $BASE/002_charge2.bin - -echo "== charge3 == -dd if=$BASE/003_charge3.bin of=$DEV bs=1 seek=79569408 count=198921 -dd if=$DEV of=/tmp/chk.bin bs=1 skip=79569408 count=198921 -cmp /tmp/chk.bin $BASE/003_charge3.bin - -echo "== charge4 == -dd if=$BASE/004_charge4.bin of=$DEV bs=4 seek=19341952 count=49966 -dd if=$DEV of=/tmp/chk.bin bs=4 skip=19341952 count=49966 -cmp /tmp/chk.bin $BASE/004_charge4.bin - -echo "== charge5 == -dd if=$BASE/005_charge5.bin of=$DEV bs=1 seek=75496960 count=199227 -dd if=$DEV of=/tmp/chk.bin bs=1 skip=75496960 count=199227 -cmp /tmp/chk.bin $BASE/005_charge5.bin - -echo "== charge6 == -dd if=$BASE/006_charge6.bin of=$DEV bs=1 seek=73385984 count=197829 -dd if=$DEV of=/tmp/chk.bin bs=1 skip=73385984 count=197829 -cmp /tmp/chk.bin $BASE/006_charge6.bin - -echo "== charge7 == -dd if=$BASE/007_charge7.bin of=$DEV bs=1 seek=72987648 count=196511 -dd if=$DEV of=/tmp/chk.bin bs=1 skip=72987648 count=196511 -cmp /tmp/chk.bin $BASE/007_charge7.bin - -echo "== charge8 == -dd if=$BASE/008_charge8.bin of=$DEV bs=4 seek=18197888 count=48897 -dd if=$DEV of=/tmp/chk.bin bs=4 skip=18197888 count=48897 -cmp /tmp/chk.bin $BASE/008_charge8.bin - -echo "== charge9 == -dd if=$BASE/009_charge9.bin of=$DEV bs=1 seek=72595456 count=196017 -dd if=$DEV of=/tmp/chk.bin bs=1 skip=72595456 count=196017 -cmp /tmp/chk.bin $BASE/009_charge9.bin - -echo "== charge10 == -dd if=$BASE/010_charge10.bin of=$DEV bs=4 seek=20958080 count=49823 -dd if=$DEV of=/tmp/chk.bin bs=4 skip=20958080 count=49823 -cmp /tmp/chk.bin $BASE/010_charge10.bin - -echo "== charge11 == -dd if=$BASE/011_charge11.bin of=$DEV bs=4 seek=20908800 count=49236 -dd if=$DEV of=/tmp/chk.bin bs=4 skip=20908800 count=49236 -cmp /tmp/chk.bin $BASE/011_charge11.bin - -echo "== charge12 == -dd if=$BASE/012_charge12.bin of=$DEV bs=4 seek=20860032 count=48700 -dd if=$DEV of=/tmp/chk.bin bs=4 skip=20860032 count=48700 -cmp /tmp/chk.bin $BASE/012_charge12.bin - -echo "== charge13 == -dd if=$BASE/013_charge13.bin of=$DEV bs=1 seek=83244032 count=195933 -dd if=$DEV of=/tmp/chk.bin bs=1 skip=83244032 count=195933 -cmp /tmp/chk.bin $BASE/013_charge13.bin - -echo "== charge14 == -dd if=$BASE/014_charge14.bin of=$DEV bs=4 seek=20761856 count=49022 -dd if=$DEV of=/tmp/chk.bin bs=4 skip=20761856 count=49022 -cmp /tmp/chk.bin $BASE/014_charge14.bin - -echo "== charge15 == -dd if=$BASE/015_charge15.bin of=$DEV bs=4 seek=20713088 count=48723 -dd if=$DEV of=/tmp/chk.bin bs=4 skip=20713088 count=48723 -cmp /tmp/chk.bin $BASE/015_charge15.bin - -echo "== charge16 == -dd if=$BASE/016_charge16.bin of=$DEV bs=1 seek=82649088 count=203070 -dd if=$DEV of=/tmp/chk.bin bs=1 skip=82649088 count=203070 -cmp /tmp/chk.bin $BASE/016_charge16.bin - -echo "== charge17 == -dd if=$BASE/017_charge17.bin of=$DEV bs=1 seek=82446336 count=202394 -dd if=$DEV of=/tmp/chk.bin bs=1 skip=82446336 count=202394 -cmp /tmp/chk.bin $BASE/017_charge17.bin - -echo "== charge18 == -dd if=$BASE/018_charge18.bin of=$DEV bs=1 seek=82244608 count=201551 -dd if=$DEV of=/tmp/chk.bin bs=1 skip=82244608 count=201551 -cmp /tmp/chk.bin $BASE/018_charge18.bin - -echo "== charge19 == -dd if=$BASE/019_charge19.bin of=$DEV bs=1 seek=82035712 count=208701 -dd if=$DEV of=/tmp/chk.bin bs=1 skip=82035712 count=208701 -cmp /tmp/chk.bin $BASE/019_charge19.bin - -echo "== charge20 == -dd if=$BASE/020_charge20.bin of=$DEV bs=1 seek=81623040 count=212929 -dd if=$DEV of=/tmp/chk.bin bs=1 skip=81623040 count=212929 -cmp /tmp/chk.bin $BASE/020_charge20.bin - -echo "== charge21 == -dd if=$BASE/021_charge21.bin of=$DEV bs=1 seek=81409024 count=213511 -dd if=$DEV of=/tmp/chk.bin bs=1 skip=81409024 count=213511 -cmp /tmp/chk.bin $BASE/021_charge21.bin - -echo "== charge22 == -dd if=$BASE/022_charge22.bin of=$DEV bs=4 seek=20299520 count=52636 -dd if=$DEV of=/tmp/chk.bin bs=4 skip=20299520 count=52636 -cmp /tmp/chk.bin $BASE/022_charge22.bin - -echo "== charge23 == -dd if=$BASE/023_charge23.bin of=$DEV bs=1 seek=80989696 count=208113 -dd if=$DEV of=/tmp/chk.bin bs=1 skip=80989696 count=208113 -cmp /tmp/chk.bin $BASE/023_charge23.bin - -echo "== charge24 == -dd if=$BASE/024_charge24.bin of=$DEV bs=1 seek=80780288 count=209043 -dd if=$DEV of=/tmp/chk.bin bs=1 skip=80780288 count=209043 -cmp /tmp/chk.bin $BASE/024_charge24.bin - -echo "== charge25 == -dd if=$BASE/025_charge25.bin of=$DEV bs=4 seek=20144128 count=50926 -dd if=$DEV of=/tmp/chk.bin bs=4 skip=20144128 count=50926 -cmp /tmp/chk.bin $BASE/025_charge25.bin - -echo "== charge26 == -dd if=$BASE/026_charge26.bin of=$DEV bs=4 seek=20094080 count=50034 -dd if=$DEV of=/tmp/chk.bin bs=4 skip=20094080 count=50034 -cmp /tmp/chk.bin $BASE/026_charge26.bin - -echo "== charge27 == -dd if=$BASE/027_charge27.bin of=$DEV bs=1 seek=80176128 count=199722 -dd if=$DEV of=/tmp/chk.bin bs=1 skip=80176128 count=199722 -cmp /tmp/chk.bin $BASE/027_charge27.bin - -echo "== charge28 == -dd if=$BASE/028_charge28.bin of=$DEV bs=4 seek=19993344 count=50579 -dd if=$DEV of=/tmp/chk.bin bs=4 skip=19993344 count=50579 -cmp /tmp/chk.bin $BASE/028_charge28.bin - -echo "== charge29 == -dd if=$BASE/029_charge29.bin of=$DEV bs=1 seek=79768576 count=204746 -dd if=$DEV of=/tmp/chk.bin bs=1 skip=79768576 count=204746 -cmp /tmp/chk.bin $BASE/029_charge29.bin - -echo "== charge30 == -dd if=$BASE/030_charge30.bin of=$DEV bs=4 seek=19840896 count=51412 -dd if=$DEV of=/tmp/chk.bin bs=4 skip=19840896 count=51412 -cmp /tmp/chk.bin $BASE/030_charge30.bin - -echo "== charge31 == -dd if=$BASE/031_charge31.bin of=$DEV bs=1 seek=79160320 count=203001 -dd if=$DEV of=/tmp/chk.bin bs=1 skip=79160320 count=203001 -cmp /tmp/chk.bin $BASE/031_charge31.bin - -echo "== charge32 == -dd if=$BASE/032_charge32.bin of=$DEV bs=1 seek=78957056 count=203131 -dd if=$DEV of=/tmp/chk.bin bs=1 skip=78957056 count=203131 -cmp /tmp/chk.bin $BASE/032_charge32.bin - -echo "== charge33 == -dd if=$BASE/033_charge33.bin of=$DEV bs=4 seek=19687680 count=51562 -dd if=$DEV of=/tmp/chk.bin bs=4 skip=19687680 count=51562 -cmp /tmp/chk.bin $BASE/033_charge33.bin - -echo "== charge34 == -dd if=$BASE/034_charge34.bin of=$DEV bs=4 seek=19636864 count=50739 -dd if=$DEV of=/tmp/chk.bin bs=4 skip=19636864 count=50739 -cmp /tmp/chk.bin $BASE/034_charge34.bin - -echo "== charge35 == -dd if=$BASE/035_charge35.bin of=$DEV bs=4 seek=19587328 count=49490 -dd if=$DEV of=/tmp/chk.bin bs=4 skip=19587328 count=49490 -cmp /tmp/chk.bin $BASE/035_charge35.bin - -echo "== charge36 == -dd if=$BASE/036_charge36.bin of=$DEV bs=4 seek=19537792 count=49525 -dd if=$DEV of=/tmp/chk.bin bs=4 skip=19537792 count=49525 -cmp /tmp/chk.bin $BASE/036_charge36.bin - -echo "== charge37 == -dd if=$BASE/037_charge37.bin of=$DEV bs=1 seek=77954048 count=196965 -dd if=$DEV of=/tmp/chk.bin bs=1 skip=77954048 count=196965 -cmp /tmp/chk.bin $BASE/037_charge37.bin - -echo "== charge38 == -dd if=$BASE/038_charge38.bin of=$DEV bs=4 seek=19439616 count=48761 -dd if=$DEV of=/tmp/chk.bin bs=4 skip=19439616 count=48761 -cmp /tmp/chk.bin $BASE/038_charge38.bin - -echo "== charge39 == -dd if=$BASE/039_charge39.bin of=$DEV bs=1 seek=77568000 count=189981 -dd if=$DEV of=/tmp/chk.bin bs=1 skip=77568000 count=189981 -cmp /tmp/chk.bin $BASE/039_charge39.bin - -echo "== charge40 == -dd if=$BASE/040_charge40.bin of=$DEV bs=1 seek=77182976 count=184335 -dd if=$DEV of=/tmp/chk.bin bs=1 skip=77182976 count=184335 -cmp /tmp/chk.bin $BASE/040_charge40.bin - -echo "== charge41 == -dd if=$BASE/041_charge41.bin of=$DEV bs=1 seek=76999680 count=183226 -dd if=$DEV of=/tmp/chk.bin bs=1 skip=76999680 count=183226 -cmp /tmp/chk.bin $BASE/041_charge41.bin - -echo "== charge42 == -dd if=$BASE/042_charge42.bin of=$DEV bs=4 seek=19204736 count=45059 -dd if=$DEV of=/tmp/chk.bin bs=4 skip=19204736 count=45059 -cmp /tmp/chk.bin $BASE/042_charge42.bin - -echo "== charge43 == -dd if=$BASE/043_charge43.bin of=$DEV bs=1 seek=76642304 count=176325 -dd if=$DEV of=/tmp/chk.bin bs=1 skip=76642304 count=176325 -cmp /tmp/chk.bin $BASE/043_charge43.bin - -echo "== charge44 == -dd if=$BASE/044_charge44.bin of=$DEV bs=4 seek=19117696 count=42805 -dd if=$DEV of=/tmp/chk.bin bs=4 skip=19117696 count=42805 -cmp /tmp/chk.bin $BASE/044_charge44.bin - -echo "== charge45 == -dd if=$BASE/045_charge45.bin of=$DEV bs=1 seek=76307968 count=162630 -dd if=$DEV of=/tmp/chk.bin bs=1 skip=76307968 count=162630 -cmp /tmp/chk.bin $BASE/045_charge45.bin - -echo "== charge46 == -dd if=$BASE/046_charge46.bin of=$DEV bs=4 seek=19041024 count=35958 -dd if=$DEV of=/tmp/chk.bin bs=4 skip=19041024 count=35958 -cmp /tmp/chk.bin $BASE/046_charge46.bin - -echo "== charge47 == -dd if=$BASE/047_charge47.bin of=$DEV bs=1 seek=76019200 count=144563 -dd if=$DEV of=/tmp/chk.bin bs=1 skip=76019200 count=144563 -cmp /tmp/chk.bin $BASE/047_charge47.bin - -echo "== charge48 == -dd if=$BASE/048_charge48.bin of=$DEV bs=1 seek=75868672 count=150019 -dd if=$DEV of=/tmp/chk.bin bs=1 skip=75868672 count=150019 -cmp /tmp/chk.bin $BASE/048_charge48.bin - -echo "== charge49 == -dd if=$BASE/049_charge49.bin of=$DEV bs=1 seek=75696640 count=171665 -dd if=$DEV of=/tmp/chk.bin bs=1 skip=75696640 count=171665 -cmp /tmp/chk.bin $BASE/049_charge49.bin - -echo "== charge50 == -dd if=$BASE/050_charge50.bin of=$DEV bs=4 seek=18829824 count=44284 -dd if=$DEV of=/tmp/chk.bin bs=4 skip=18829824 count=44284 -cmp /tmp/chk.bin $BASE/050_charge50.bin - -echo "== charge51 == -dd if=$BASE/051_charge51.bin of=$DEV bs=1 seek=75135488 count=183445 -dd if=$DEV of=/tmp/chk.bin bs=1 skip=75135488 count=183445 -cmp /tmp/chk.bin $BASE/051_charge51.bin - -echo "== charge52 == -dd if=$BASE/052_charge52.bin of=$DEV bs=4 seek=18737280 count=46476 -dd if=$DEV of=/tmp/chk.bin bs=4 skip=18737280 count=46476 -cmp /tmp/chk.bin $BASE/052_charge52.bin - -echo "== charge53 == -dd if=$BASE/053_charge53.bin of=$DEV bs=1 seek=74759680 count=189246 -dd if=$DEV of=/tmp/chk.bin bs=1 skip=74759680 count=189246 -cmp /tmp/chk.bin $BASE/053_charge53.bin - -echo "== charge54 == -dd if=$BASE/054_charge54.bin of=$DEV bs=1 seek=74567680 count=191681 -dd if=$DEV of=/tmp/chk.bin bs=1 skip=74567680 count=191681 -cmp /tmp/chk.bin $BASE/054_charge54.bin - -echo "== charge55 == -dd if=$BASE/055_charge55.bin of=$DEV bs=1 seek=74374656 count=192782 -dd if=$DEV of=/tmp/chk.bin bs=1 skip=74374656 count=192782 -cmp /tmp/chk.bin $BASE/055_charge55.bin - -echo "== charge56 == -dd if=$BASE/056_charge56.bin of=$DEV bs=1 seek=74179584 count=195037 -dd if=$DEV of=/tmp/chk.bin bs=1 skip=74179584 count=195037 -cmp /tmp/chk.bin $BASE/056_charge56.bin - -echo "== charge57 == -dd if=$BASE/057_charge57.bin of=$DEV bs=1 seek=73982464 count=197081 -dd if=$DEV of=/tmp/chk.bin bs=1 skip=73982464 count=197081 -cmp /tmp/chk.bin $BASE/057_charge57.bin - -echo "== charge58 == -dd if=$BASE/058_charge58.bin of=$DEV bs=1 seek=73783808 count=198594 -dd if=$DEV of=/tmp/chk.bin bs=1 skip=73783808 count=198594 -cmp /tmp/chk.bin $BASE/058_charge58.bin - -echo "== charge59 == -dd if=$BASE/059_charge59.bin of=$DEV bs=1 seek=73584128 count=199473 -dd if=$DEV of=/tmp/chk.bin bs=1 skip=73584128 count=199473 -cmp /tmp/chk.bin $BASE/059_charge59.bin - -echo "== charge60 == -dd if=$BASE/060_charge60.bin of=$DEV bs=1 seek=73184256 count=201487 -dd if=$DEV of=/tmp/chk.bin bs=1 skip=73184256 count=201487 -cmp /tmp/chk.bin $BASE/060_charge60.bin - -rm /tmp/chk.bin -echo "all done - run reboot to apply" diff --git a/s5e-charge/main.lua b/s5e-charge/main.lua deleted file mode 100644 index e56bc46..0000000 --- a/s5e-charge/main.lua +++ /dev/null @@ -1,774 +0,0 @@ ---[[---------------------------------------------------------------------------- - S5Chg —— S5 41mm(q63) 充电动画 -> S5 eSIM 46mm(p62lte) - - 写入目标:/dev/system(ROMFS,卷名 system)里的 charging/charge0..60.bin - 61 帧、480x480、I8 + RLE(cf=0x0a, flags=0x08),每帧槽位长度各不相同 - (143,832 ~ 213,511 B)—— 负载**逐字节等长**,所以 inode 表与后面所有文件的 - 绝对偏移一个字节都不动。 - - 界面按 480x480 设计稿落地(tools/pages.py,本机用真 MiSans 渲染 + 真实像素判 - 圆屏切边,17 个页面含 replace/restore 两套文案:0 越界、0 折行)。 - - 两份盘共用同一个 watchface id 462150103: - S5Chg.face 只装"替换后"的 61 帧 + 原厂 64B 指纹 - S5ChgRestore.face 只装"原厂"的 61 帧 + 替换后 64B 指纹 - 装其中一份就顶掉另一份,设备上始终只占一份空间。 - - 真机已验证的底层能力(沿用 S5->S4 那个表盘): - * 宿主不调用 ui.init -> 加载期建界面 - * SCRIPT_PATH 为空 -> 用 debug.getinfo 反推目录 - * .face 不解包,资源仍在 resource.bin 里 -> 按偏移读出来用 - * /dev/system 可写,dd 读写校验通过 - * 没有 utf8 标准库 -> 自带按位 UTF-8 解码(本文件未用到,留作提醒) -------------------------------------------------------------------------------]] - -local lvgl = require("lvgl") - -local W, H = 480, 480 -local DEV, BLK = "/dev/system", 4 -local TMPH = "/tmp/s5c_head.bin" -- 版本校验读出的前 64 字节 -local TMPR = "/tmp/s5c_back.bin" -- 写完之后回读出来的那一份 -local TMPB = "/tmp/s5c_blob.bin" -- 待写入的负载(从 resource.bin 抠出来) - --- 等长覆盖只保证「偏移」512 对齐;RLE 素材的长度常常不是 4 的倍数, --- 那种记录必须退化成 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 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 内偏移, /dev/system 内偏移, 长度 } —— 由脚本回填 --- 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/s5c_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) - ---------------------------------------------------------------------- 按下高亮 --- 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, ok = 0, errs = 0, vers = 0, - skipRest = false, variant = 'replace' } --- 从哪个入口进"开始替换"确认页,确认按钮就执行对应操作: --- 首页「开始替换」 -> 'replace' --- 帮助页「回滚修改」 -> 'tip'(拆盘后本盘没有回滚数据,只提示装另一份盘) -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 - --- 时钟:居中不用 text_align(它要的是 lv_text_align_t,传 ALIGN.CENTER 越界会被忽略 --- -> 实测变成左对齐)。改为按真字体量出的宽度手工算 x:'10:24' 29px 量得 78 -local function clockLabel(parent) - 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 - --- 标题:框 y=36 h=46,字号 31 行高 42 -> 从框顶画即可(LVGL 标签从框顶起排) -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 是关键:LVGL 主题会给 Object 加内边距,不清零的话子元素会整体右下偏移 -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 - -- 标题 29px 行高 40,框高 40 -> 从框顶画就是垂直居中 - 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 - --- 大按钮 258x82 圆角 41:圆角把四角收进圆内(本机像素判据实测 0 越界) -local function bigButton(parent, y, text) - local b = nil - pcall(function() - b = parent:Object { x = 111, y = y, w = 258, h = 82, pad_all = 0, - bg_color = C_PLATE, border_width = 0, radius = 41 } - end) - if b then setClickable(b) end - -- 按钮文字:框 (71,22,126,40) 相对底板;'确认替换'/'重启手表' 29px 量得 116 - -- -> 居中 x = 111 + (258-116)/2 - 111 = 71 - 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 - --- 读设备上某偏移处的 len 字节(只读)。偏移 512 对齐时用分段大块,快两个数量级。 -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 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 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 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 h = ddRead(0, 48) - if not h or #h < 48 then - setDesc1('预检查失败:/dev/system 读不到。', C_ERR) - return false - end - if h:sub(1, 8) ~= "-rom1fs-" then - setDesc1('预检查失败:设备节点不可读\n或分区头不是 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 - -- superblock 的 size 是**大端**(romfs 规范:__be32),所以这里必须用 ">I4"。 - -- (S4e 那版写成 " 越界判断形同虚设;这里修正。) - local size = string.unpack(">I4", h, 9) - local bad, total = 0, 0 - for i = 1, #list do - local r = list[i] - local b = ddBlk(r[2], r[3]) - if (r[2] % b) ~= 0 or (r[3] % b) ~= 0 then bad = bad + 1 end - if (r[2] + r[3]) > 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 probe = slice(list[1][1], list[1][3]) - if not probe or #probe ~= list[1][3] then - setDesc1('预检查失败:resource.bin 读取异常。', C_ERR) - return false - end - -- 抽样版本校验(只读):首 / 中 / 尾三条,比对设备上真实的 64 字节与本盘的 - -- 「要写进去的字节」或「另一侧指纹」—— 能确认负载偏移就是给这一版固件算的。 - local marks = { 1, math.max(1, #list // 2), #list } - local samp, okAll = {}, true - for i = 1, #marks do - local k = marks[i] - local rec, oth = list[k], (PAYTBL.rb or {})[k] - local got = ddRead(rec[2], 64) - local mine = slice(rec[1], 64) - local other = oth and slice(oth[1], 64) or nil - if not got or #got < 64 then - samp[#samp + 1] = '×' - okAll = false - elseif mine and got == mine then - samp[#samp + 1] = '已替换✓' - elseif other and got == other then - samp[#samp + 1] = '原厂✓' - else - samp[#samp + 1] = '抽样×' - okAll = false - end - end - local head = string.format('预检查通过 | %d 条 | %.2fMB', #list, total / 1048576) - setDesc1(head .. '\n/dev/system ' .. table.concat(samp, ' '), 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 - -- 日志框 h=277、行高 35 -> 最多显示 7 行;留 7 行保证最后一行完整可见 - while #logLines > 7 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 - --- 一条记录三步:① 版本校验(设备上那段必须等于「原厂」或「替换后」的前 64 B) --- ② 写入 ③ 整段回读比对 --- 版本两边都不是 -> 'ver',并且**跳过 /dev/system 剩余记录**(换固件后最安全的行为: --- 绝不把字节写进别人的文件里)。os.execute 的返回值在这台机器上不可靠,所以判据 --- 一律是内容比对。 -local function writeOne(rec, want) - local data = slice(rec[1], rec[3]) - if not data or #data ~= rec[3] then return 'err' end - local n = 64 - if want then - if want[3] < n then n = want[3] end - local other = slice(want[1], n) - local cur = ddRead(rec[2], n) - if not cur or #cur ~= n or not other or #other ~= n then return 'err' end - if cur ~= data:sub(1, n) and cur ~= other then return 'ver' 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 'ok' -end - -local function startJob(variant) - if job.run then return end - local list = PAYTBL.pay or {} - if #list == 0 then - setDesc1('负载表为空,已中止。', C_ERR) - return - end - job.run, job.list, job.i, job.ok, job.errs, job.vers = true, list, 0, 0, 0, 0 - job.skipRest, job.variant = false, variant - logLines = {} - logOut('> 开始替换') - logOut(string.format('> 61 帧充电动画 / 共 %d 条', #list)) - 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 - logOut('> 替换完毕,请重启手表。') - logOut(string.format('> 共 %d 条,成功 %d 失败 %d', n, job.ok, job.errs)) - if job.vers > 0 then - logOut(string.format('> 版本不符跳过 %d 条', job.vers)) - logOut('> 请勿重启,先装回匹配版本。') - else - logOut('> 若发现问题,请回滚修改。') - end - showReboot() - return - end - if job.skipRest then - job.vers = job.vers + 1 - else - local rb = PAYTBL.rb and PAYTBL.rb[job.i] - local r = writeOne(job.list[job.i], rb) - if r == 'ver' then - job.vers = job.vers + 1 - job.skipRest = true -- 版本不符:跳过 /dev/system 剩余记录 - logOut('> /dev/system 版本不符,已跳过剩余记录') - elseif r == 'ok' then - job.ok = job.ok + 1 - 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) - -- 盘内标题 = 显示名(不再用内部代号 S5Chg);x 按真 MiSans 31px 量出的宽度居中: - -- 'S5e Pt.3' = 126 -> x = (480-126)/2 = 177 - -- 'S5e Pt.3 · 还原' = 226 -> x = (480-226)/2 = 127 - if ROLE == 'restore' then - pageTitle(p, 'S5e Pt.3 · 还原', 127, 228) - else - pageTitle(p, 'S5e Pt.3', 177, 128) - end - listItem(p, 88, 'ic_help', '快速帮助', function() show(pages.help) end) - listItem(p, 186, 'ic_replace', ROLE == 'restore' and '开始还原' or '开始替换', 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 = 60, y = 182, w = 372, h = 76, - text = '检查 /dev/system 分区与\n61 帧充电动画的完整性。', - text_color = C_GRAY, font_size = 25, text_font = F_BODY }) - - -- 拆盘之后:本盘只装一份负载,「回滚/恢复」由另一份盘负责 - listItem(p, 264, 'ic_rollback', '回滚修改', function() - if ROLE == 'restore' then - setDesc1('本盘负责还原,换回动画\n请装 S5e Pt.3。', C_GRAY) - else - setDesc1('本盘只负责替换,请装\nS5e Pt.3 · 还原。', C_GRAY) - end - end) - lab(p, { x = 60, y = 355, w = 372, h = 36, - text = ROLE == 'restore' and '换回动画请装 S5e Pt.3。' - or '还原请装 S5e Pt.3 · 还原。', - 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, ROLE == 'restore' and '开始还原' or '开始替换', 188, 124) - setClickable(t) - bind(t, goBack) - - -- 多色文本**不能**用 recolor:AP 里那几处 recolor 是图片重着色(bg_img_recolor) - -- 和表盘编辑器的主题重着色,不是文本属性;实测标记会被原样显示出来。 - -- 改为拆成多个标签 + 手工算 x(宽度用真 MiSans 25px 量出),框宽 ≥ 文本宽 - -- -> 保证不折行(折行会让手工算 x 的片段错位),并且整框都在圆内。 - lab(p, { x = 52, y = 93, w = 340, h = 36, - text = '替换充电动画之前,请先确认', - text_color = C_GRAY, font_size = 25, text_font = F_BODY }) - lab(p, { x = 52, y = 126, w = 380, h = 36, - text = '自己的系统版本号为 3.112.035!', - text_color = C_GRAY, font_size = 25, text_font = F_BODY }) - lab(p, { x = 52, y = 208, w = 280, h = 36, text = '请确认自己的手表型号为', - text_color = C_GRAY, font_size = 25, text_font = F_BODY }) - lab(p, { x = 52, y = 241, w = 405, h = 36, text = 'Xiaomi Watch S5 eSIM 46mm !', - text_color = C_WHITE, font_size = 25, text_font = F_BODY }) - lab(p, { x = 52, y = 331, w = 330, h = 36, text = '搞机有风险,变砖后果自负。', - text_color = C_GRAY, font_size = 25, text_font = F_BODY }) - - local b = bigButton(p, 376, ROLE == 'restore' and '确认还原' or '确认替换') - 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) - - -- 信息卡:底板 + 圆形 logo + 大标题 + 版本号 - 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 - -- 29px 量得:'S5e Pt.3' = 119 -> x = 54 + (373-119)/2 = 181 - -- 'S5e Pt.3 · 还原' = 212 -> x = 54 + (373-212)/2 = 134.5 -> 134 - if ROLE == 'restore' then - lab(p, { x = 134, y = 198, w = 214, h = 40, text = 'S5e Pt.3 · 还原', - text_color = C_WHITE, font_size = 29, text_font = F_ITEM }) - else - lab(p, { x = 181, y = 198, w = 121, h = 40, text = 'S5e Pt.3', - text_color = C_WHITE, font_size = 29, text_font = F_ITEM }) - end - -- '1.0.0' 21px 量得 49 -> 居中 x = 54 + (373-49)/2 = 216 - 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 }) - - -- QQ 联系方式:结构与列表项一致(无动作,所以不给它绑点击) - 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) - -- 本页没有返回图标(规格:顶栏同首页),所以标题要屏幕居中: - -- '正在替换' 31px 量得 124 -> x = 240 - 124/2 = 178 - 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, 376, '重启手表') - -- 替换过程中不显示,替换完再显示 - 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-charge/restore.sh b/s5e-charge/restore.sh deleted file mode 100644 index 7b18d49..0000000 --- a/s5e-charge/restore.sh +++ /dev/null @@ -1,316 +0,0 @@ -#!/bin/sh -# 把 S5 eSIM 46mm 的充电动画写回原厂(还原盘) - -DEV=/dev/system -BASE=/data/s5panim/payload_restore - -echo "target check" -ls -l $DEV - -echo "== charge0 == -dd if=$BASE/000_charge0.bin of=$DEV bs=1 seek=84231168 count=197959 -dd if=$DEV of=/tmp/chk.bin bs=1 skip=84231168 count=197959 -cmp /tmp/chk.bin $BASE/000_charge0.bin - -echo "== charge1 == -dd if=$BASE/001_charge1.bin of=$DEV bs=4 seek=21008000 count=49712 -dd if=$DEV of=/tmp/chk.bin bs=4 skip=21008000 count=49712 -cmp /tmp/chk.bin $BASE/001_charge1.bin - -echo "== charge2 == -dd if=$BASE/002_charge2.bin of=$DEV bs=4 seek=20459008 count=49835 -dd if=$DEV of=/tmp/chk.bin bs=4 skip=20459008 count=49835 -cmp /tmp/chk.bin $BASE/002_charge2.bin - -echo "== charge3 == -dd if=$BASE/003_charge3.bin of=$DEV bs=1 seek=79569408 count=198921 -dd if=$DEV of=/tmp/chk.bin bs=1 skip=79569408 count=198921 -cmp /tmp/chk.bin $BASE/003_charge3.bin - -echo "== charge4 == -dd if=$BASE/004_charge4.bin of=$DEV bs=4 seek=19341952 count=49966 -dd if=$DEV of=/tmp/chk.bin bs=4 skip=19341952 count=49966 -cmp /tmp/chk.bin $BASE/004_charge4.bin - -echo "== charge5 == -dd if=$BASE/005_charge5.bin of=$DEV bs=1 seek=75496960 count=199227 -dd if=$DEV of=/tmp/chk.bin bs=1 skip=75496960 count=199227 -cmp /tmp/chk.bin $BASE/005_charge5.bin - -echo "== charge6 == -dd if=$BASE/006_charge6.bin of=$DEV bs=1 seek=73385984 count=197829 -dd if=$DEV of=/tmp/chk.bin bs=1 skip=73385984 count=197829 -cmp /tmp/chk.bin $BASE/006_charge6.bin - -echo "== charge7 == -dd if=$BASE/007_charge7.bin of=$DEV bs=1 seek=72987648 count=196511 -dd if=$DEV of=/tmp/chk.bin bs=1 skip=72987648 count=196511 -cmp /tmp/chk.bin $BASE/007_charge7.bin - -echo "== charge8 == -dd if=$BASE/008_charge8.bin of=$DEV bs=4 seek=18197888 count=48897 -dd if=$DEV of=/tmp/chk.bin bs=4 skip=18197888 count=48897 -cmp /tmp/chk.bin $BASE/008_charge8.bin - -echo "== charge9 == -dd if=$BASE/009_charge9.bin of=$DEV bs=1 seek=72595456 count=196017 -dd if=$DEV of=/tmp/chk.bin bs=1 skip=72595456 count=196017 -cmp /tmp/chk.bin $BASE/009_charge9.bin - -echo "== charge10 == -dd if=$BASE/010_charge10.bin of=$DEV bs=4 seek=20958080 count=49823 -dd if=$DEV of=/tmp/chk.bin bs=4 skip=20958080 count=49823 -cmp /tmp/chk.bin $BASE/010_charge10.bin - -echo "== charge11 == -dd if=$BASE/011_charge11.bin of=$DEV bs=4 seek=20908800 count=49236 -dd if=$DEV of=/tmp/chk.bin bs=4 skip=20908800 count=49236 -cmp /tmp/chk.bin $BASE/011_charge11.bin - -echo "== charge12 == -dd if=$BASE/012_charge12.bin of=$DEV bs=4 seek=20860032 count=48700 -dd if=$DEV of=/tmp/chk.bin bs=4 skip=20860032 count=48700 -cmp /tmp/chk.bin $BASE/012_charge12.bin - -echo "== charge13 == -dd if=$BASE/013_charge13.bin of=$DEV bs=1 seek=83244032 count=195933 -dd if=$DEV of=/tmp/chk.bin bs=1 skip=83244032 count=195933 -cmp /tmp/chk.bin $BASE/013_charge13.bin - -echo "== charge14 == -dd if=$BASE/014_charge14.bin of=$DEV bs=4 seek=20761856 count=49022 -dd if=$DEV of=/tmp/chk.bin bs=4 skip=20761856 count=49022 -cmp /tmp/chk.bin $BASE/014_charge14.bin - -echo "== charge15 == -dd if=$BASE/015_charge15.bin of=$DEV bs=4 seek=20713088 count=48723 -dd if=$DEV of=/tmp/chk.bin bs=4 skip=20713088 count=48723 -cmp /tmp/chk.bin $BASE/015_charge15.bin - -echo "== charge16 == -dd if=$BASE/016_charge16.bin of=$DEV bs=1 seek=82649088 count=203070 -dd if=$DEV of=/tmp/chk.bin bs=1 skip=82649088 count=203070 -cmp /tmp/chk.bin $BASE/016_charge16.bin - -echo "== charge17 == -dd if=$BASE/017_charge17.bin of=$DEV bs=1 seek=82446336 count=202394 -dd if=$DEV of=/tmp/chk.bin bs=1 skip=82446336 count=202394 -cmp /tmp/chk.bin $BASE/017_charge17.bin - -echo "== charge18 == -dd if=$BASE/018_charge18.bin of=$DEV bs=1 seek=82244608 count=201551 -dd if=$DEV of=/tmp/chk.bin bs=1 skip=82244608 count=201551 -cmp /tmp/chk.bin $BASE/018_charge18.bin - -echo "== charge19 == -dd if=$BASE/019_charge19.bin of=$DEV bs=1 seek=82035712 count=208701 -dd if=$DEV of=/tmp/chk.bin bs=1 skip=82035712 count=208701 -cmp /tmp/chk.bin $BASE/019_charge19.bin - -echo "== charge20 == -dd if=$BASE/020_charge20.bin of=$DEV bs=1 seek=81623040 count=212929 -dd if=$DEV of=/tmp/chk.bin bs=1 skip=81623040 count=212929 -cmp /tmp/chk.bin $BASE/020_charge20.bin - -echo "== charge21 == -dd if=$BASE/021_charge21.bin of=$DEV bs=1 seek=81409024 count=213511 -dd if=$DEV of=/tmp/chk.bin bs=1 skip=81409024 count=213511 -cmp /tmp/chk.bin $BASE/021_charge21.bin - -echo "== charge22 == -dd if=$BASE/022_charge22.bin of=$DEV bs=4 seek=20299520 count=52636 -dd if=$DEV of=/tmp/chk.bin bs=4 skip=20299520 count=52636 -cmp /tmp/chk.bin $BASE/022_charge22.bin - -echo "== charge23 == -dd if=$BASE/023_charge23.bin of=$DEV bs=1 seek=80989696 count=208113 -dd if=$DEV of=/tmp/chk.bin bs=1 skip=80989696 count=208113 -cmp /tmp/chk.bin $BASE/023_charge23.bin - -echo "== charge24 == -dd if=$BASE/024_charge24.bin of=$DEV bs=1 seek=80780288 count=209043 -dd if=$DEV of=/tmp/chk.bin bs=1 skip=80780288 count=209043 -cmp /tmp/chk.bin $BASE/024_charge24.bin - -echo "== charge25 == -dd if=$BASE/025_charge25.bin of=$DEV bs=4 seek=20144128 count=50926 -dd if=$DEV of=/tmp/chk.bin bs=4 skip=20144128 count=50926 -cmp /tmp/chk.bin $BASE/025_charge25.bin - -echo "== charge26 == -dd if=$BASE/026_charge26.bin of=$DEV bs=4 seek=20094080 count=50034 -dd if=$DEV of=/tmp/chk.bin bs=4 skip=20094080 count=50034 -cmp /tmp/chk.bin $BASE/026_charge26.bin - -echo "== charge27 == -dd if=$BASE/027_charge27.bin of=$DEV bs=1 seek=80176128 count=199722 -dd if=$DEV of=/tmp/chk.bin bs=1 skip=80176128 count=199722 -cmp /tmp/chk.bin $BASE/027_charge27.bin - -echo "== charge28 == -dd if=$BASE/028_charge28.bin of=$DEV bs=4 seek=19993344 count=50579 -dd if=$DEV of=/tmp/chk.bin bs=4 skip=19993344 count=50579 -cmp /tmp/chk.bin $BASE/028_charge28.bin - -echo "== charge29 == -dd if=$BASE/029_charge29.bin of=$DEV bs=1 seek=79768576 count=204746 -dd if=$DEV of=/tmp/chk.bin bs=1 skip=79768576 count=204746 -cmp /tmp/chk.bin $BASE/029_charge29.bin - -echo "== charge30 == -dd if=$BASE/030_charge30.bin of=$DEV bs=4 seek=19840896 count=51412 -dd if=$DEV of=/tmp/chk.bin bs=4 skip=19840896 count=51412 -cmp /tmp/chk.bin $BASE/030_charge30.bin - -echo "== charge31 == -dd if=$BASE/031_charge31.bin of=$DEV bs=1 seek=79160320 count=203001 -dd if=$DEV of=/tmp/chk.bin bs=1 skip=79160320 count=203001 -cmp /tmp/chk.bin $BASE/031_charge31.bin - -echo "== charge32 == -dd if=$BASE/032_charge32.bin of=$DEV bs=1 seek=78957056 count=203131 -dd if=$DEV of=/tmp/chk.bin bs=1 skip=78957056 count=203131 -cmp /tmp/chk.bin $BASE/032_charge32.bin - -echo "== charge33 == -dd if=$BASE/033_charge33.bin of=$DEV bs=4 seek=19687680 count=51562 -dd if=$DEV of=/tmp/chk.bin bs=4 skip=19687680 count=51562 -cmp /tmp/chk.bin $BASE/033_charge33.bin - -echo "== charge34 == -dd if=$BASE/034_charge34.bin of=$DEV bs=4 seek=19636864 count=50739 -dd if=$DEV of=/tmp/chk.bin bs=4 skip=19636864 count=50739 -cmp /tmp/chk.bin $BASE/034_charge34.bin - -echo "== charge35 == -dd if=$BASE/035_charge35.bin of=$DEV bs=4 seek=19587328 count=49490 -dd if=$DEV of=/tmp/chk.bin bs=4 skip=19587328 count=49490 -cmp /tmp/chk.bin $BASE/035_charge35.bin - -echo "== charge36 == -dd if=$BASE/036_charge36.bin of=$DEV bs=4 seek=19537792 count=49525 -dd if=$DEV of=/tmp/chk.bin bs=4 skip=19537792 count=49525 -cmp /tmp/chk.bin $BASE/036_charge36.bin - -echo "== charge37 == -dd if=$BASE/037_charge37.bin of=$DEV bs=1 seek=77954048 count=196965 -dd if=$DEV of=/tmp/chk.bin bs=1 skip=77954048 count=196965 -cmp /tmp/chk.bin $BASE/037_charge37.bin - -echo "== charge38 == -dd if=$BASE/038_charge38.bin of=$DEV bs=4 seek=19439616 count=48761 -dd if=$DEV of=/tmp/chk.bin bs=4 skip=19439616 count=48761 -cmp /tmp/chk.bin $BASE/038_charge38.bin - -echo "== charge39 == -dd if=$BASE/039_charge39.bin of=$DEV bs=1 seek=77568000 count=189981 -dd if=$DEV of=/tmp/chk.bin bs=1 skip=77568000 count=189981 -cmp /tmp/chk.bin $BASE/039_charge39.bin - -echo "== charge40 == -dd if=$BASE/040_charge40.bin of=$DEV bs=1 seek=77182976 count=184335 -dd if=$DEV of=/tmp/chk.bin bs=1 skip=77182976 count=184335 -cmp /tmp/chk.bin $BASE/040_charge40.bin - -echo "== charge41 == -dd if=$BASE/041_charge41.bin of=$DEV bs=1 seek=76999680 count=183226 -dd if=$DEV of=/tmp/chk.bin bs=1 skip=76999680 count=183226 -cmp /tmp/chk.bin $BASE/041_charge41.bin - -echo "== charge42 == -dd if=$BASE/042_charge42.bin of=$DEV bs=4 seek=19204736 count=45059 -dd if=$DEV of=/tmp/chk.bin bs=4 skip=19204736 count=45059 -cmp /tmp/chk.bin $BASE/042_charge42.bin - -echo "== charge43 == -dd if=$BASE/043_charge43.bin of=$DEV bs=1 seek=76642304 count=176325 -dd if=$DEV of=/tmp/chk.bin bs=1 skip=76642304 count=176325 -cmp /tmp/chk.bin $BASE/043_charge43.bin - -echo "== charge44 == -dd if=$BASE/044_charge44.bin of=$DEV bs=4 seek=19117696 count=42805 -dd if=$DEV of=/tmp/chk.bin bs=4 skip=19117696 count=42805 -cmp /tmp/chk.bin $BASE/044_charge44.bin - -echo "== charge45 == -dd if=$BASE/045_charge45.bin of=$DEV bs=1 seek=76307968 count=162630 -dd if=$DEV of=/tmp/chk.bin bs=1 skip=76307968 count=162630 -cmp /tmp/chk.bin $BASE/045_charge45.bin - -echo "== charge46 == -dd if=$BASE/046_charge46.bin of=$DEV bs=4 seek=19041024 count=35958 -dd if=$DEV of=/tmp/chk.bin bs=4 skip=19041024 count=35958 -cmp /tmp/chk.bin $BASE/046_charge46.bin - -echo "== charge47 == -dd if=$BASE/047_charge47.bin of=$DEV bs=1 seek=76019200 count=144563 -dd if=$DEV of=/tmp/chk.bin bs=1 skip=76019200 count=144563 -cmp /tmp/chk.bin $BASE/047_charge47.bin - -echo "== charge48 == -dd if=$BASE/048_charge48.bin of=$DEV bs=1 seek=75868672 count=150019 -dd if=$DEV of=/tmp/chk.bin bs=1 skip=75868672 count=150019 -cmp /tmp/chk.bin $BASE/048_charge48.bin - -echo "== charge49 == -dd if=$BASE/049_charge49.bin of=$DEV bs=1 seek=75696640 count=171665 -dd if=$DEV of=/tmp/chk.bin bs=1 skip=75696640 count=171665 -cmp /tmp/chk.bin $BASE/049_charge49.bin - -echo "== charge50 == -dd if=$BASE/050_charge50.bin of=$DEV bs=4 seek=18829824 count=44284 -dd if=$DEV of=/tmp/chk.bin bs=4 skip=18829824 count=44284 -cmp /tmp/chk.bin $BASE/050_charge50.bin - -echo "== charge51 == -dd if=$BASE/051_charge51.bin of=$DEV bs=1 seek=75135488 count=183445 -dd if=$DEV of=/tmp/chk.bin bs=1 skip=75135488 count=183445 -cmp /tmp/chk.bin $BASE/051_charge51.bin - -echo "== charge52 == -dd if=$BASE/052_charge52.bin of=$DEV bs=4 seek=18737280 count=46476 -dd if=$DEV of=/tmp/chk.bin bs=4 skip=18737280 count=46476 -cmp /tmp/chk.bin $BASE/052_charge52.bin - -echo "== charge53 == -dd if=$BASE/053_charge53.bin of=$DEV bs=1 seek=74759680 count=189246 -dd if=$DEV of=/tmp/chk.bin bs=1 skip=74759680 count=189246 -cmp /tmp/chk.bin $BASE/053_charge53.bin - -echo "== charge54 == -dd if=$BASE/054_charge54.bin of=$DEV bs=1 seek=74567680 count=191681 -dd if=$DEV of=/tmp/chk.bin bs=1 skip=74567680 count=191681 -cmp /tmp/chk.bin $BASE/054_charge54.bin - -echo "== charge55 == -dd if=$BASE/055_charge55.bin of=$DEV bs=1 seek=74374656 count=192782 -dd if=$DEV of=/tmp/chk.bin bs=1 skip=74374656 count=192782 -cmp /tmp/chk.bin $BASE/055_charge55.bin - -echo "== charge56 == -dd if=$BASE/056_charge56.bin of=$DEV bs=1 seek=74179584 count=195037 -dd if=$DEV of=/tmp/chk.bin bs=1 skip=74179584 count=195037 -cmp /tmp/chk.bin $BASE/056_charge56.bin - -echo "== charge57 == -dd if=$BASE/057_charge57.bin of=$DEV bs=1 seek=73982464 count=197081 -dd if=$DEV of=/tmp/chk.bin bs=1 skip=73982464 count=197081 -cmp /tmp/chk.bin $BASE/057_charge57.bin - -echo "== charge58 == -dd if=$BASE/058_charge58.bin of=$DEV bs=1 seek=73783808 count=198594 -dd if=$DEV of=/tmp/chk.bin bs=1 skip=73783808 count=198594 -cmp /tmp/chk.bin $BASE/058_charge58.bin - -echo "== charge59 == -dd if=$BASE/059_charge59.bin of=$DEV bs=1 seek=73584128 count=199473 -dd if=$DEV of=/tmp/chk.bin bs=1 skip=73584128 count=199473 -cmp /tmp/chk.bin $BASE/059_charge59.bin - -echo "== charge60 == -dd if=$BASE/060_charge60.bin of=$DEV bs=1 seek=73184256 count=201487 -dd if=$DEV of=/tmp/chk.bin bs=1 skip=73184256 count=201487 -cmp /tmp/chk.bin $BASE/060_charge60.bin - -rm /tmp/chk.bin -echo "restore done - run reboot to apply" diff --git a/s5e-charge/tools/build_faces.py b/s5e-charge/tools/build_faces.py deleted file mode 100644 index 254ee80..0000000 --- a/s5e-charge/tools/build_faces.py +++ /dev/null @@ -1,105 +0,0 @@ -"""overlay + 官方模板 -> Compiler.exe -> .face(两份盘共用同一个 watchface id) - -要点(都是踩过的坑): - * preview.png 必须是 326x326,Compiler.exe 会直接拒绝别的尺寸 - * watchfaceId 要带机型家族前缀:S5 家族 = 462150xxx - * 用 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 = {'S5Chg': ('462150103', 'S5 41mm 充电动画 -> S5 eSIM 46mm(只写负载)'), - 'S5ChgRestore': ('462150103', 'S5 eSIM 46mm 充电动画还原盘(只写原厂字节)')} - - -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 normalize_fprj(dst): - """把工作区里的 .fprj 归一成 .fprj,并返回它的路径。 - - 我们拷进去时就是 .fprj,但 vendor 的 build_face.ps1 会先跑 - sync_watchface_config.ps1:它按 config 的 projectName(现在是**显示名** S5e Pt.3)找 - .fprj,找不到就把唯一那份改名过去(Title 也被改写成 projectName)。 - 编完这里再改回 .fprj —— 工作区里始终只有一份 .fprj(放两份会被 Compiler.exe - 并成双倍资源,实测资源整体翻倍),文件名也保持项目标识不变。 - """ - name = os.path.basename(dst) - d = os.path.join(dst, 'watchface', 'fprj') - fs = sorted(f for f in os.listdir(d) if f.endswith('.fprj')) - if len(fs) != 1: - raise SystemExit('%s: watchface/fprj 下的 .fprj 应恰好 1 份,实际:%s' % (name, fs)) - want = os.path.join(d, name + '.fprj') - if fs[0] != name + '.fprj': - os.replace(os.path.join(d, fs[0]), want) - return want - - -def build(dst): - ps = os.path.join(dst, 'scripts', 'build_face.ps1') - # 显式给 -FaceName:vendor 脚本默认用 config 的 projectName 当输出名,而 projectName 现在 - # 是**显示名**(S5e Pt.3),交付/中间文件名必须还是 NAME.face。 - r = subprocess.run(['powershell', '-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', ps, - '-FaceName', os.path.basename(dst) + '.face'], - capture_output=True, text=True, errors='replace') - normalize_fprj(dst) # vendor 可能把 .fprj 改名成显示名,编完改回 .fprj - 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/s5e-charge/tools/check_clip.py b/s5e-charge/tools/check_clip.py deleted file mode 100644 index 31321a1..0000000 --- a/s5e-charge/tools/check_clip.py +++ /dev/null @@ -1,74 +0,0 @@ -"""用真实渲染结果判"会不会被圆屏切掉"(480x480 档)。 - -做法:渲染一张**不套圆形遮罩**的 RGBA,然后量所有非透明像素到圆心的距离。 -这样自动考虑字体实际字宽、折行、圆角、图片缩放 —— 比按元素外框估算准得多。 -replace 盘与 restore 盘的文案都过一遍(PAGES_ALT),只换字不改坐标,所以文案变长 -就必须重新量 —— 这里就是那道闸。 -""" -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, _rgb -import render_ui as R -from pages import PAGES, PAGES_ALT - -CX = CY = 240.0 -RAD = 240.0 - - -def render_raw(items): - 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) - 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 - - -R.OVERFLOW.clear() -cases = list(PAGES.items()) + [('alt:' + k, v) for k, v in PAGES_ALT.items()] -print('%-16s %8s %11s %s' % ('page', 'max r', 'outside px', 'worst point / note')) -print('-' * 78) -bad = 0 -for name, items in cases: - 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: - dd = math.hypot(x - CX, y - CY) - if dd > worst: - worst, wpt = dd, (x, y) - if dd > RAD: - outside += 1 - note = '全部在圆内' if worst <= RAD else '会被切 %d px' % outside - print('%-16s %8.1f %11d %s %s%s' % (name, worst, outside, wpt, note, - '' if outside == 0 else ' <-- CLIPPED')) - if outside: - bad += 1 -print('-' * 78) -print('圆心 (%.0f,%.0f) 半径 %.0f 画布 %dx%d 圆屏' % (CX, CY, RAD, W, H)) -if R.OVERFLOW: - print('!! 有标签超出框高(会折行/被裁,先修文案或框):') - for o in R.OVERFLOW: - print(' ', o) - bad += len(R.OVERFLOW) -print('检查了 %d 个页面(含 replace/restore 两套文案):越界页面 %d' % (len(cases), bad)) -print('RESULT: %s' % ('PASS(0 越界、0 折行)' if bad == 0 else 'FAIL')) -sys.exit(1 if bad else 0) diff --git a/s5e-charge/tools/finalize_faces.py b/s5e-charge/tools/finalize_faces.py deleted file mode 100644 index e795035..0000000 --- a/s5e-charge/tools/finalize_faces.py +++ /dev/null @@ -1,82 +0,0 @@ -"""产出最终 .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 那份) - 体积原因:本盘各 ~12 MB,不再额外复制工具链变体(要的话去掉下面的 SKIP_TOOLCHAIN 即可) -""" -import os, sys, shutil, subprocess, hashlib -from paths import WORK, DIST - -BUILD = os.path.join(WORK, 'build') -TARGETS = {'S5Chg': '462150103', 'S5ChgRestore': '462150103'} -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 - # vendor 的 build_face.ps1 会先跑 sync_watchface_config.ps1,它按 config 的 projectName - # (现在是**显示名** S5e Pt.3)把 .fprj 改名成 <显示名>.fprj。build_faces.py 编完 - # 会改回来;这里再兜一次,保证只取工作区里唯一那份 .fprj(放两份会让资源翻倍)。 - fdir = os.path.join(w, 'watchface', 'fprj') - fs = sorted(f for f in os.listdir(fdir) if f.endswith('.fprj')) - if len(fs) != 1: - raise SystemExit('%s: watchface/fprj 下的 .fprj 应恰好 1 份,实际:%s' % (name, fs)) - fprj = os.path.join(fdir, fs[0]) - if fs[0] != name + '.fprj': - os.replace(fprj, os.path.join(fdir, name + '.fprj')) - fprj = os.path.join(fdir, 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/s5e-charge/tools/gen_ui_lua.py b/s5e-charge/tools/gen_ui_lua.py deleted file mode 100644 index 6ef4858..0000000 --- a/s5e-charge/tools/gen_ui_lua.py +++ /dev/null @@ -1,127 +0,0 @@ -"""规范化构建:表盘源码 + 资源 -> .face(两遍,自动回填 resource.bin 内的偏移) - - main.lua <- **源码**,含 __UIIMG__ / __PAY__ / __RB__ / __ROLE__ 占位符 - .work/overlay/ <- 中间产物(回填后的 Lua 落在这里) - .work/build/ <- 编译工作区 - dist/.face <- 交付物 - -两份盘(replace / restore)共用同一套流程,只是负载来源不同: - blob_replace/{payload,fp}.bin -> S5Chg.face - blob_restore/{payload,fp}.bin -> S5ChgRestore.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 = 'S5Chg' if ROLE == 'replace' else 'S5ChgRestore' -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/s5e-charge/tools/iconpack.py b/s5e-charge/tools/iconpack.py deleted file mode 100644 index 68c8672..0000000 --- a/s5e-charge/tools/iconpack.py +++ /dev/null @@ -1,447 +0,0 @@ -"""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/s5e-charge/tools/lua_balance.py b/s5e-charge/tools/lua_balance.py deleted file mode 100644 index a33ebba..0000000 --- a/s5e-charge/tools/lua_balance.py +++ /dev/null @@ -1,72 +0,0 @@ -"""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(' n)**。S4e 那版是 57 帧按比例重采样(n -> round(n/56*60)), - 这一版两边都是 61 帧,**不要照抄那个比例** —— 逐帧对应才不会出现错帧/跳帧。 - -放大与编码策略(按优先级逐级降级,选第一条塞进槽位的): - 1. 原调色板 + Lanczos:把源帧用 Lanczos 放大到 480x480(RGB),再把每个像素**吸附回源帧 - 自己的调色板**(256 项里找最近的)。颜色只可能来自源帧的调色板 —— 空间上比最近邻放大 - 平滑,颜色上零新增。实测 61/61 帧都塞得进,且体积与最近邻放大几乎一样(0.996x)。 - 2. 原调色板 + Lanczos + 暗区合并:把亮度 < T 的像素全归到一个最暗的索引上换 RLE 长度, - T 从小到大试(只影响接近黑的雾面区,亮环不动)。这是兜底,本项目实际没用上。 - 3. 原调色板 + 最近邻索引放大(保底、无插值)。 - 4. 整帧重新量化(颜色数从 256 逐级往下试)—— 最后兜底。 - -等长要点(这是命根子): - 1) 数据总长必须**恰好**等于原槽位长度(ROMFS 后面所有偏移都是绝对值) - 2) 解码长度必须**恰好**等于 usize(少一字节设备端整帧不画;多一字节越界 -> INVALID) - —— usize_tail 用「重复最后一个字节」补齐,正是因为踩过「RLE 输出比 usize 少 1 个像素」的坑 - 3) pad_rle_safe 只用「产出 0 像素」的合法空指令(00 00 / 80)补齐到 csize -""" -import os, sys, json, struct, shutil, hashlib -sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) -from paths import INPUTS, WORK, DOCS -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') # S5 eSIM 46mm (p62lte) -SOURCE = os.path.join(INPUTS, 'source', 'vela_system.bin') # S5 41mm (q63) -OUTDIR = os.path.join(WORK, 'charge') -PATCHED = os.path.join(OUTDIR, 'vela_system_charge.bin') -DIRP = 'charging/' -W = H = 480 -SAMPLE = None if (len(sys.argv) > 1 and sys.argv[1] == 'all') else [0, 14, 28, 42, 56] -THRS = (4, 8, 12, 16, 20, 24, 32, 40, 48, 64, 96, 128, 192, 255) -FALLBACK_COLORS = (256, 192, 128, 96, 64, 48, 32) -DRAW_DOCS = True - - -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 decode_fast(blob): - """(header, palette 256x4 BGRA, index HxW) —— 用库里的 rle_decompress,索引取出用 numpy。""" - h = L.parse_header(blob) - method, csize, usize = struct.unpack(' Lanczos 放大到 (w,h) 的 RGB(质量基准,也用于吸附)。""" - im = Image.fromarray(src_rgb(pal, idx).astype(np.uint8), 'RGB').resize((w, h), Image.LANCZOS) - return np.asarray(im, np.int32) - - -def snap(rgb, pal, chunk=8192): - """每个像素在 256 项调色板里的最近色索引(欧氏距离,分块 brute force)。""" - p = rgb_tab(pal) - flat = rgb.reshape(-1, 3).astype(np.int32) - out = np.empty(len(flat), np.uint8) - for i in range(0, len(flat), chunk): - d = flat[i:i + chunk, None, :] - p[None, :, :] - out[i:i + chunk] = np.argmin((d * d).sum(axis=2), axis=1) - return out.reshape(rgb.shape[:2]) - - -def nn_idx(idx, w, h): - """最近邻索引放大(不做任何插值)。""" - sh, sw = idx.shape - ys = np.clip(np.floor((np.arange(h) + 0.5) * sh / h).astype(np.int64), 0, sh - 1) - xs = np.clip(np.floor((np.arange(w) + 0.5) * sw / w).astype(np.int64), 0, sw - 1) - return idx[ys[:, None], xs[None, :]] - - -def dark_index(pal, idx, thr): - """暗区里选一个代表索引:取其中颜色最暗的那个。""" - vals = np.unique(idx[np.asarray(rgb_tab(pal)[idx].max(axis=2) < thr)]) - if len(vals) == 0: - return None - return int(vals[rgb_tab(pal)[vals].max(axis=1).argmin()]) - - -def pal4(q): - pal = list(q.getpalette() or []) + [0] * (768 - len(q.getpalette() or [])) - 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 usize_tail(comp, raw_len, usize): - """把「解码出来的裸字节数」补到恰好 usize。 - - 踩过的坑:RLE 输出比 usize **少一个像素**时设备端整帧不画。这里的补法是 - 「再重复最后一个字节 N 次」(ctrl=N, unit=last)—— 视觉上只是把最后一个像素的 - 游程延长,不会像补 0 那样在末尾画一条黑边;也不会多解(正好补齐 usize)。 - """ - short = usize - raw_len - if short <= 0: - return comp - unit = comp[-1] if comp else 0 - ex = bytearray() - while short > 0: - n = min(short, 127) - ex += bytes((n, unit)) - short -= n - return comp + bytes(ex) - - -def encode_frame(pal, idx, w, h, csize, usize): - """返回 (comp, idx_out, how)。comp 已补到「恰好解码出 usize 字节」。""" - ref = lanczos_ref(pal, idx, w, h) - # --- 1) 原调色板 + Lanczos 吸附(本项目 61/61 都走这条) - cand_idx = snap(ref, pal) - for thr in (0,) + THRS: - if thr <= 0: - i2 = cand_idx - else: - m = ref.max(axis=2) < thr - if not m.any(): - i2 = cand_idx - else: - di = dark_index(pal, cand_idx, thr) - i2 = cand_idx if di is None else np.where(m, di, cand_idx).astype(np.uint8) - c = L.rle_compress(pal.tobytes() + i2.tobytes()) - c = usize_tail(c, 1024 + w * h, usize) - if len(c) <= csize: - how = '原调色板+Lanczos' if thr <= 0 else '原调色板+Lanczos+暗区<%d' % thr - return c, i2, how - # --- 2) 最近邻索引放大(无插值) - i2 = nn_idx(idx, w, h) - c = usize_tail(L.rle_compress(pal.tobytes() + i2.tobytes()), 1024 + w * h, usize) - if len(c) <= csize: - return c, i2, '原调色板+最近邻放大' - # --- 3) 整帧重新量化,颜色数逐级往下 - for ncol in FALLBACK_COLORS: - q = Image.fromarray(ref.astype(np.uint8), 'RGB').quantize( - colors=ncol, method=Image.MEDIANCUT, dither=Image.NONE) - i2 = np.asarray(q, np.uint8) - c = usize_tail(L.rle_compress(pal4(q) + i2.tobytes()), 1024 + w * h, usize) - if len(c) <= csize: - return c, i2, '重新量化 %d 色' % ncol - raise AssertionError('帧塞不进槽位(连 %d 色都不行)' % FALLBACK_COLORS[-1]) - - -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('目标 S5 eSIM(p62lte) %d 帧(%d..%d,槽位合计 %.2f MB)' % ( - len(tl), tl[0], tl[-1], sum(len(slots[n][2]) for n in tl) / 1048576)) - print('源 S5 41mm(q63) %d 帧(%d..%d,%.2f MB)' % ( - len(sl), sl[0], sl[-1], sum(len(srcs[n][2]) for n in sl) / 1048576)) - assert tl == sl, '两边帧号不一致,不能做 1:1 覆盖' - todo = tl if SAMPLE is None else [n for n in SAMPLE if n in slots] - print('映射:n -> n(1:1) 本次要改的帧:%s' % todo) - print('输出分辨率:%dx%d(源 464x464 -> Lanczos 放大后吸附回源调色板)\n' % (W, H)) - - records, decode_ck, used, samples = [], 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('/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'\x00S5C-PAYv1\x00\xa5\x5a\xc3\x3c' # 15 B,必须全局唯一 -MAGIC_FP = b'\x00S5C-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 充电动画 -> S5 eSIM 46mm(只写负载)', - '把 flash.sh 与 payload/ 放到同一个目录,例如 /data/s5panim/', - '然后: sh /data/s5panim/flash.sh 最后: reboot', - '还原用 restore.sh 或安装还原盘 S5ChgRestore.face'], - '/data/s5panim/payload', True, - 'rm /tmp/chk.bin\necho "all done - run reboot to apply"') -else: - script('restore.sh', lua_rows, - ['把 S5 eSIM 46mm 的充电动画写回原厂(还原盘)'], - '/data/s5panim/payload_restore', True, - 'rm /tmp/chk.bin\necho "restore done - run reboot to apply"') - -json.dump(dict(device='Xiaomi Watch S5 eSIM 46mm (p62lte)', 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/s5e-charge/tools/make_overlays.py b/s5e-charge/tools/make_overlays.py deleted file mode 100644 index 96afe38..0000000 --- a/s5e-charge/tools/make_overlays.py +++ /dev/null @@ -1,99 +0,0 @@ -# -*- coding: utf-8 -*- -"""为官方 LuaDevTemplate 生成可编译的 overlay。两份盘:S5Chg(替换)/ S5ChgRestore(还原)。 - -要点(都是踩过的坑): - * Compiler.exe 的设备表里没有 480 档 -> 用 DeviceType=462(S5 家族,圆表)+ 480x480 的 - Screen;preview.png 必须是 326x326,别的尺寸直接拒绝 - * 预览图按 tools/pages.py 的首页规格真渲染(真 MiSans 字体)再套圆形 alpha 缩到 326 - * 两份盘共用同一个 watchface id 462150103:装一份就顶掉另一份 -""" -import os, sys, json, shutil -from PIL import Image, ImageDraw -from paths import PROJ, WORK -import render_ui as R -from pages import PAGES - -ROLE = (sys.argv[1] if len(sys.argv) > 1 else 'replace').lower() -NAME = 'S5Chg' if ROLE == 'replace' else 'S5ChgRestore' # 路径/标识(不要改) -OUT = os.path.join(WORK, 'overlay') -ASSETS = os.path.join(PROJ, 'assets') - -DEVICE_TYPE = "462" # S5 家族(Compiler.exe 只认 362 / 462 这两个圆表档) -ENTRY_WIDGET = "app_lua%2Fmain.lua" -PREVIEW_SIZE = 326 # Compiler.exe 对圆表设备强制 326x326 -SCREEN_SIZE = 480 -WF_ID = "462150103" # 两份盘共用同一个 id:装一份就顶掉另一份 -TITLE = 'S5e Pt.3' # 手表上显示的表盘名(两份盘同一个名字)= fprj 的 - - -def make_preview(path): - """480 首页 -> 圆形 alpha -> 326x326(Compiler.exe 只收这个尺寸)。""" - R.W = R.H = SCREEN_SIZE - img = Image.new('RGBA', (SCREEN_SIZE, SCREEN_SIZE), (0, 0, 0, 0)) - d = ImageDraw.Draw(img) - for it in PAGES['home']: - 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_SIZE, SCREEN_SIZE), (0, 0, 0, 255)) - base.alpha_composite(img) - mask = Image.new('L', (SCREEN_SIZE, SCREEN_SIZE), 0) - ImageDraw.Draw(mask).ellipse([0, 0, SCREEN_SIZE - 1, SCREEN_SIZE - 1], fill=255) - base.putalpha(mask) - base.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": TITLE, "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'), TITLE) - 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/s5e-charge/tools/make_ui_images.py b/s5e-charge/tools/make_ui_images.py deleted file mode 100644 index 5531e41..0000000 --- a/s5e-charge/tools/make_ui_images.py +++ /dev/null @@ -1,58 +0,0 @@ -"""把 assets/ 里的 UI 素材转成 LVGL v9 二进制图片(.bin),供表盘运行时加载。 - -尺寸取自 tools/pages.py 的 480 档规格:列表图标 31x31、返回箭头 18x29、关于页 logo 80x80。 -格式(已在真机验证): - 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') - -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), -] - -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:封面图缩到 80x80,并加圆形遮罩 -# (封面是圆形图案 + 黑色方角,不裁成圆的话会在墨绿卡片上露出黑方块) -logo_src = os.path.join(ASSETS, 'cover.png') -if os.path.exists(logo_src): - L = 80 - 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_480.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/s5e-charge/tools/pages.py b/s5e-charge/tools/pages.py deleted file mode 100644 index 1fa9e15..0000000 --- a/s5e-charge/tools/pages.py +++ /dev/null @@ -1,207 +0,0 @@ -# -*- coding: utf-8 -*- -"""Xiaomi Watch S5 eSIM 46mm(480x480)页面规格 + 本机渲染器。 - -设计语言与 466x466 的 S4e 版一致,整体按 480/466 放大并重新居中;坐标就是 -main.lua 里实际用的坐标(tools/render_ui.py 严格按本文件渲染,tools/check_clip.py -拿**真实渲染像素**判圆屏是否切边),所以预览图是诚实的。 - -CSS -> 实际能力 的映射(真机验证过): - left/top/width/height -> x / y / w / h(480 画布绝对坐标) - background -> bg_color - border-radius -> radius - color (alpha 1) -> text_color '#rrggbb' - color (alpha 0.6) -> 白 60% 叠在纯黑底上 = #999999 - font-weight 600 -> MiSans-Semibold ; 500 -> MiSans-Medium - 多色文本 -> **不能**用 LVGL recolor(会被原样显示),拆成多个标签 + 手工算 x -""" -import os - -from paths import PROJ, DOCS -ROOT = PROJ -ASSETS = os.path.join(ROOT, 'assets') -W = H = 480 -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' - -# 列表项几何(480 档;= 466 档 × 480/466 后取整) -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 -# 说明文字的公共框(比 466 档宽一点:中文 25px 一行放得下,且右边界仍在圆内) -DESC_X, DESC_W = 60, 372 -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])), - # 标题行高 40(29px 字体),框高 40 -> 垂直居中 y = 26 + (40-40)/2 = 26 - 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): - 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): - """大按钮:258x82 圆角 41 —— 圆角把四角收进圆内(本机像素判据实测 0 越界)。""" - 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), - ] - - -# ---------------------------------------------------------------- 1. 首页 -# 标题按真字体宽度居中:'S5e Pt.3' 31px 量得 126 -> x = (480-126)/2 = 177 -PAGE_HOME = [clock(), title('S5e Pt.3', 177, 128), - *item(88, 'ic_help', '快速帮助'), - *item(186, 'ic_replace', '开始替换'), - *item(285, 'ic_about', '关于')] - -# ---------------------------------------------------------------- 2. 快速帮助 -_HELP_HEAD = [clock(), back_arrow(167), title('快速帮助', 188, 124)] -# 说明文字固定 2 行(显式换行),框高 76 = 2 × 行高 35 + 余量 -HELP_DESC_DEF = dict(kind='label', x=DESC_X, y=182, w=DESC_W, h=76, - text='检查 /dev/system 分区与\n61 帧充电动画的完整性。', size=25, - weight=S500, color=GRAY, align='left', vtop=True) -HELP_DESC_OK = dict(kind='label', x=DESC_X, y=182, w=DESC_W, h=76, - text='预检查通过 | 61 条 | 11.27MB\n您可以返回主页开始修改。', - size=25, weight=S500, color=OK, align='left', vtop=True) -HELP_DESC_ERR = dict(kind='label', x=DESC_X, y=182, w=DESC_W, h=76, - text='预检查失败:设备节点不可读\n或分区头不是 ROMFS。', - size=25, weight=S500, color=ERR, align='left', vtop=True) -HELP_DESC_TIP = dict(kind='label', x=DESC_X, y=182, w=DESC_W, h=76, - text='本盘只负责替换,请装\nS5e Pt.3 · 还原。', - size=25, weight=S500, color=GRAY, align='left', vtop=True) -HELP_D1 = dict(kind='label', x=DESC_X, y=355, w=DESC_W, h=36, - text='还原请装 S5e Pt.3 · 还原。', size=25, weight=S500, color=GRAY, - align='left', vtop=True) -HELP_DESC_TIP_RB = dict(kind='label', x=DESC_X, y=182, w=DESC_W, h=76, - text='本盘负责还原,换回动画\n请装 S5e Pt.3。', - size=25, weight=S500, color=GRAY, align='left', vtop=True) -HELP_D1_RB = dict(kind='label', x=DESC_X, y=355, w=DESC_W, h=36, - text='换回动画请装 S5e Pt.3。', size=25, weight=S500, color=GRAY, - align='left', vtop=True) - - -def _help_page(desc, d1=None): - return (_HELP_HEAD + item(88, 'ic_check', '环境预检查') + [desc] - + item(264, 'ic_rollback', '回滚修改') + [d1 or HELP_D1]) - - -PAGE_HELP = _help_page(HELP_DESC_DEF) -PAGE_HELP_OK = _help_page(HELP_DESC_OK) -PAGE_HELP_ERR = _help_page(HELP_DESC_ERR) -PAGE_HELP_TIP = _help_page(HELP_DESC_TIP) - -# ---------------------------------------------------------------- 3. 开始替换(警告确认页) -# 每个片段都用真 MiSans 25px 量过宽度,框宽 ≥ 文本宽 -> 保证**不折行**(折行会让 -# 手工算 x 的多色片段错位),并且整框都在圆内(check_clip 用渲染像素实测)。 -WARN_LABELS = [ - dict(kind='label', x=52, y=93, w=340, h=36, - text='替换充电动画之前,请先确认', size=25, weight=S500, color=GRAY, - align='left', vtop=True), - dict(kind='label', x=52, y=126, w=380, h=36, - text='自己的系统版本号为 3.112.035!', size=25, weight=S500, color=GRAY, - align='left', vtop=True), - dict(kind='label', x=52, y=208, w=280, h=36, text='请确认自己的手表型号为', - size=25, weight=S500, color=GRAY, align='left', vtop=True), - dict(kind='label', x=52, y=241, w=405, h=36, text='Xiaomi Watch S5 eSIM 46mm !', - size=25, weight=S500, color=WHITE, align='left', vtop=True), - dict(kind='label', x=52, y=331, w=330, 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, '确认替换')) - -# ---------------------------------------------------------------- 4. 替换中 -LOG_MID = ('> 开始替换\n' - '> 读取 resource.bin\n' - '> 写入 40/61 失败 0\n' - '> 替换完毕,请重启手表。\n' - '> 若发现问题,请回滚修改。') -LOG_END = ('> 开始替换\n' - '> 读取 resource.bin\n' - '> 写入 61/61 失败 0\n' - '> 共 61 条,失败 0 条\n' - '> 版本不符跳过 0 条') -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_RUNNING_END = [clock(), title('正在替换', 178, 124), - dict(kind='label', x=67, y=93, w=348, h=277, text=LOG_END, size=25, - weight=S500, color=WHITE, align='left', vtop=True), - *big_button(376, '重启手表')] - -# ---------------------------------------------------------------- 5. 关于 -# 卡片 373 宽:'S5e Pt.3' 29px 量得 119 -> x = 54 + (373-119)/2 = 181(整) -# '1.0.0' 21px 量得 49 -> x = 54 + (373-49)/2 = 216 -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=181, y=198, w=121, h=40, text='S5e Pt.3', - 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, - 'help_tip': PAGE_HELP_TIP, 'confirm': PAGE_CONFIRM, 'running': PAGE_RUNNING, - 'running_end': PAGE_RUNNING_END, 'about': PAGE_ABOUT} - - -# ---------------------------------------------------------------- 还原盘(ROLE=restore)的文案 -# 同一套坐标,只换文字。文字比替换盘长的(首页标题 / 关于卡片大标题:加了 " · 还原") -# 必须把 x / w 一起重新按真字体宽度居中 —— check_clip.py 会把 PAGES_ALT 一起渲染并测量。 -def _swap(items, old, new, x=None, w=None): - out = [] - for it in items: - if it.get('kind') == 'label' and it['text'] == old: - it = dict(it, text=new) - if x is not None: - it['x'] = x - if w is not None: - it['w'] = w - out.append(it) - return out - - -PAGES_ALT = { - 'home': _swap(PAGE_HOME, 'S5e Pt.3', 'S5e Pt.3 · 还原', x=127, w=228), - 'home_item2': _swap(PAGE_HOME, '开始替换', '开始还原'), - 'help_d1': _help_page(HELP_DESC_DEF, HELP_D1_RB), - 'help_tip': _help_page(HELP_DESC_TIP_RB, HELP_D1_RB), - 'confirm': _swap(PAGE_CONFIRM, '开始替换', '开始还原'), - 'confirm_btn': _swap(PAGE_CONFIRM, '确认替换', '确认还原'), - 'running': _swap(PAGE_RUNNING, '正在替换', '正在回滚'), - 'about': _swap(PAGE_ABOUT, 'S5e Pt.3', 'S5e Pt.3 · 还原', x=134, w=214), -} diff --git a/s5e-charge/tools/paths.py b/s5e-charge/tools/paths.py deleted file mode 100644 index a2da22a..0000000 --- a/s5e-charge/tools/paths.py +++ /dev/null @@ -1,35 +0,0 @@ -"""项目里所有脚本的路径入口 —— 换机器 / 换目录只看这一个文件。 - -默认布局(都相对项目根): - 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/s5e-charge/tools/render_ui.py b/s5e-charge/tools/render_ui.py deleted file mode 100644 index 4923707..0000000 --- a/s5e-charge/tools/render_ui.py +++ /dev/null @@ -1,202 +0,0 @@ -"""按页面规格渲染 480x480 预览图(真 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, DOCS -ROOT = PROJ -FONT_FILES = {'semibold': 'MiSans-Semibold.ttf', 'medium': 'MiSans-Medium.ttf', - 'regular': 'MiSans-Regular.ttf'} -W = H = 480 -_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, PAGES_ALT - os.makedirs(DOCS, exist_ok=True) - outs = [] - for name, items in list(PAGES.items()) + [('alt_' + k, v) for k, v in PAGES_ALT.items()]: - p = os.path.join(DOCS, 'ui_%s.png' % name) - render(items, p) - outs.append((name, p)) - print('wrote', p) - if OVERFLOW: - print('!! 标签超出框高(会被裁/折行,需要调框或调文案):') - for o in OVERFLOW: - print(' ', o) - else: - print('no label overflow(所有标签都在框内,不多行、不裁切)') - cols = 4 - rows = (len(outs) + cols - 1) // cols - sheet = Image.new('RGB', (cols * (W + 12) + 12, rows * (H + 26) + 12), (28, 28, 32)) - d = ImageDraw.Draw(sheet) - for i, (name, p) in enumerate(outs): - x = 12 + (i % cols) * (W + 12) - y = 12 + (i // cols) * (H + 26) - sheet.paste(Image.open(p), (x, y + 18)) - d.text((x + 4, y + 2), name, fill=(200, 200, 200)) - sp = os.path.join(DOCS, 'ui_all.png') - sheet.save(sp) - print('wrote', sp, sheet.size) diff --git a/s5e-charge/tools/romfs.py b/s5e-charge/tools/romfs.py deleted file mode 100644 index 753dda2..0000000 --- a/s5e-charge/tools/romfs.py +++ /dev/null @@ -1,112 +0,0 @@ -"""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-charge/tools/selfcheck.ps1 b/s5e-charge/tools/selfcheck.ps1 deleted file mode 100644 index 7b6a62f..0000000 --- a/s5e-charge/tools/selfcheck.ps1 +++ /dev/null @@ -1,47 +0,0 @@ -# S5eChg rebuild + self-check pipeline. Key output is appended to docs/selfcheck_run.txt -# Usage: & ".\\.work\\selfcheck.ps1" (run from the project root) -# NOTE: keep this file pure ASCII -- the Chinese text comes from the python tools. -$ErrorActionPreference = 'Continue' -[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 -$env:PYTHONIOENCODING = 'utf-8' -$proj = Split-Path -Parent $PSScriptRoot -Set-Location $proj -$py = 'C:\Users\hi\.dsh\dsh-runtimes\dsh-primary-runtime\dependencies\python\python.exe' -$log = 'docs\selfcheck_run.txt' -$bar = '=' * 78 - -$stamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss' -"S5eChg rebuild + self-check run: $stamp" | Out-File -Encoding utf8 $log -"Python : $py" | Out-File -Append -Encoding utf8 $log -"Project: $proj" | Out-File -Append -Encoding utf8 $log - -function Step([string[]]$argv) { - $line = '>>> python ' + ($argv -join ' ') - $bar | Out-File -Append -Encoding utf8 $log - $line | Out-File -Append -Encoding utf8 $log - $bar | Out-File -Append -Encoding utf8 $log - Write-Host '' - Write-Host $line - & $py @argv 2>&1 | Tee-Object -FilePath $log -Append - $rc = $LASTEXITCODE - "EXIT=$rc" | Out-File -Append -Encoding utf8 $log - Write-Host "EXIT=$rc" - if ($rc -ne 0) { - Write-Host '!! step failed, aborting' -ForegroundColor Red - exit $rc - } -} - -Step @('tools\make_charge_payload.py', 'all') -Step @('tools\make_dd_payload.py', 'replace') -Step @('tools\gen_ui_lua.py', 'replace') -Step @('tools\verify_anim.py', 'replace') -Step @('tools\make_dd_payload.py', 'restore') -Step @('tools\gen_ui_lua.py', 'restore') -Step @('tools\verify_anim.py', 'restore') -Step @('tools\verify_all.py') - -'' | Out-File -Append -Encoding utf8 $log -'ALL STEPS OK' | Out-File -Append -Encoding utf8 $log -Write-Host '' -Write-Host "ALL STEPS OK -- log: $log" diff --git a/s5e-charge/tools/verify_all.py b/s5e-charge/tools/verify_all.py deleted file mode 100644 index 2e23a5b..0000000 --- a/s5e-charge/tools/verify_all.py +++ /dev/null @@ -1,286 +0,0 @@ -# -*- coding: utf-8 -*- -"""独立总校验:只看**交付物 + 固件镜像**,不看任何中间产物(不读 .work/)。 - - python tools/verify_all.py # 生成 docs/selfcheck_report.txt + 预览图 - -逐项判据(每项都是一条可证伪的断言): - A 交付物形态:.face 魔数、watchface id = 462150103(两份共用)、b5=0、体积上限 - B Lua 表:从 .face 里的 Lua 反解 pay / rb 表(各 61 条) - C 等长:每条 pay 的长度 == 原厂槽位长度,且目标偏移就是 charging/chargeN.bin 的 - 数据偏移(61 条恰好一一覆盖,无重复无遗漏) - D 可解码:每条 pay 是 480x480 I8+RLE,且 RLE 流**恰好**解出 usize = 1024+480*480 - E 帧内容:把交付物里的每一帧解出来,与"用 q63 源帧独立做的 Lanczos 放大"比对, - 平均差 < 1.0/255(吸附误差量级),并报最像的源帧号(证明 n 对 n 没错帧) - F 版本指纹:replace 盘的 rb == **原厂**前 64 B(对固件镜像独立比对) - restore 盘的 pay == **原厂**整条字节(逐字节) - restore 盘的 rb == replace 盘 pay 的前 64 B(两份交付物互证) - G 整镜像:用 replace 盘的负载重建改后镜像 -> inode 表 / superblock / 长度与原厂 - 逐位一致,且**除 61 个槽位外没有任何字节变化** - H 预览:把交付物解出来的 61 帧拼成图(docs/decode_preview.png)便于肉眼复核 -""" -import os, re, sys, hashlib, struct -sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) -from paths import PROJ, INPUTS, DOCS -from romfs import Romfs -import lvgl_bin as L -from make_charge_payload import src_indexed -from PIL import Image, ImageDraw -import numpy as np - -W = H = 480 -WF_ID = '462150103' -DIRP = 'charging/' -STOCK = os.path.join(INPUTS, 'target', 'vela_system.bin') -SOURCE = os.path.join(INPUTS, 'source', 'vela_system.bin') -FACES = {'replace': os.path.join(PROJ, 'dist', 'S5Chg.face'), - 'restore': os.path.join(PROJ, 'dist', 'S5ChgRestore.face')} -REPORT = [] -ok = fail = 0 - - -def log(s=''): - print(s) - REPORT.append(s) - - -def ck(cond, msg, detail=''): - global ok, fail - if cond: - ok += 1 - else: - fail += 1 - log(' [%s] %s%s' % ('OK ' if cond else 'FAIL', msg, (' ' + detail) if detail else '')) - return cond - - -def num_of(p): - return int(p.rsplit('charge', 1)[-1].replace('.bin', '')) - - -def slots_of(path): - rom = Romfs(path) - d = {} - for p, i in rom.entries(): - if i.type == 1 or not p.startswith(DIRP) or not p.endswith('.bin'): - continue - d[i.data_off] = (num_of(p), i.size) - return rom, d - - -def decode(blob): - h = L.parse_header(blob) - m, csize, usize = struct.unpack(' ' + os.path.join(DOCS, 'selfcheck_report.txt')) -sys.exit(1 if fail else 0) diff --git a/s5e-charge/tools/verify_anim.py b/s5e-charge/tools/verify_anim.py deleted file mode 100644 index 748de1f..0000000 --- a/s5e-charge/tools/verify_anim.py +++ /dev/null @@ -1,106 +0,0 @@ -# -*- coding: utf-8 -*- -"""独立校验交付的 .face(只看交付物 + 固件镜像,不看中间产物)。 - - role: replace -> dist/S5Chg.face pay 应等于**改后镜像**的字节,fp 应等于**原厂**前 64 B - restore -> dist/S5ChgRestore.face pay 应等于**原厂**字节, fp 应等于**改后镜像**前 64 B - ① 从 .face 里抠出 Lua 表(pay / rb),按表里的偏移取字节 - ② 每条 pay 长度必须 == 原厂槽位长度(等长覆盖的底线),且能按 LVGL RLE 解出 480x480 - ③ 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 = 'S5Chg' if ROLE == 'replace' else 'S5ChgRestore' -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'] != 480 or h['h'] != 480: - 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 deleted file mode 100644 index 26abc5b..0000000 Binary files a/s5e-icons/assets/MdiBandage.png and /dev/null differ diff --git a/s5e-icons/assets/MdiClipboardTextSearchOutline.png b/s5e-icons/assets/MdiClipboardTextSearchOutline.png deleted file mode 100644 index 85ea2da..0000000 Binary files a/s5e-icons/assets/MdiClipboardTextSearchOutline.png and /dev/null differ diff --git a/s5e-icons/assets/MdiInformationOutline.png b/s5e-icons/assets/MdiInformationOutline.png deleted file mode 100644 index 3af2a29..0000000 Binary files a/s5e-icons/assets/MdiInformationOutline.png and /dev/null differ diff --git a/s5e-icons/assets/MdiLightbulbOutline.png b/s5e-icons/assets/MdiLightbulbOutline.png deleted file mode 100644 index 7d7819c..0000000 Binary files a/s5e-icons/assets/MdiLightbulbOutline.png and /dev/null differ diff --git a/s5e-icons/assets/MdiReplyAllOutline.png b/s5e-icons/assets/MdiReplyAllOutline.png deleted file mode 100644 index f419de0..0000000 Binary files a/s5e-icons/assets/MdiReplyAllOutline.png and /dev/null differ diff --git a/s5e-icons/assets/backarrow.png b/s5e-icons/assets/backarrow.png deleted file mode 100644 index 9e55859..0000000 Binary files a/s5e-icons/assets/backarrow.png and /dev/null differ diff --git a/s5e-icons/assets/cover.png b/s5e-icons/assets/cover.png deleted file mode 100644 index ccbba23..0000000 Binary files a/s5e-icons/assets/cover.png and /dev/null differ diff --git a/s5e-icons/assets/override/activities.png b/s5e-icons/assets/override/activities.png deleted file mode 100644 index ae5c552..0000000 Binary files a/s5e-icons/assets/override/activities.png and /dev/null differ diff --git a/s5e-icons/assets/override/voice_aivs.png b/s5e-icons/assets/override/voice_aivs.png deleted file mode 100644 index 228914f..0000000 Binary files a/s5e-icons/assets/override/voice_aivs.png and /dev/null differ diff --git a/s5e-icons/assets/qq.png b/s5e-icons/assets/qq.png deleted file mode 100644 index 3982762..0000000 Binary files a/s5e-icons/assets/qq.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/background.png b/s5e-icons/assets/walkie_talkie/background.png deleted file mode 100644 index 879c98f..0000000 Binary files a/s5e-icons/assets/walkie_talkie/background.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/cancel_btn.png b/s5e-icons/assets/walkie_talkie/cancel_btn.png deleted file mode 100644 index cc56188..0000000 Binary files a/s5e-icons/assets/walkie_talkie/cancel_btn.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/check.png b/s5e-icons/assets/walkie_talkie/check.png deleted file mode 100644 index bc89d28..0000000 Binary files a/s5e-icons/assets/walkie_talkie/check.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/checked.png b/s5e-icons/assets/walkie_talkie/checked.png deleted file mode 100644 index 64bf2b9..0000000 Binary files a/s5e-icons/assets/walkie_talkie/checked.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/close_btn.png b/s5e-icons/assets/walkie_talkie/close_btn.png deleted file mode 100644 index a872247..0000000 Binary files a/s5e-icons/assets/walkie_talkie/close_btn.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/confirm_btn.png b/s5e-icons/assets/walkie_talkie/confirm_btn.png deleted file mode 100644 index 932d711..0000000 Binary files a/s5e-icons/assets/walkie_talkie/confirm_btn.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/delete_icon.png b/s5e-icons/assets/walkie_talkie/delete_icon.png deleted file mode 100644 index b3d948d..0000000 Binary files a/s5e-icons/assets/walkie_talkie/delete_icon.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/guidance/finish_guidance.png b/s5e-icons/assets/walkie_talkie/guidance/finish_guidance.png deleted file mode 100644 index 932d711..0000000 Binary files a/s5e-icons/assets/walkie_talkie/guidance/finish_guidance.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/history_anim/anim0.png b/s5e-icons/assets/walkie_talkie/history_anim/anim0.png deleted file mode 100644 index cdbf172..0000000 Binary files a/s5e-icons/assets/walkie_talkie/history_anim/anim0.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/history_anim/anim1.png b/s5e-icons/assets/walkie_talkie/history_anim/anim1.png deleted file mode 100644 index 9eb6f8b..0000000 Binary files a/s5e-icons/assets/walkie_talkie/history_anim/anim1.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/history_anim/anim10.png b/s5e-icons/assets/walkie_talkie/history_anim/anim10.png deleted file mode 100644 index 927d24a..0000000 Binary files a/s5e-icons/assets/walkie_talkie/history_anim/anim10.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/history_anim/anim11.png b/s5e-icons/assets/walkie_talkie/history_anim/anim11.png deleted file mode 100644 index 38d3985..0000000 Binary files a/s5e-icons/assets/walkie_talkie/history_anim/anim11.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/history_anim/anim12.png b/s5e-icons/assets/walkie_talkie/history_anim/anim12.png deleted file mode 100644 index 32c452c..0000000 Binary files a/s5e-icons/assets/walkie_talkie/history_anim/anim12.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/history_anim/anim13.png b/s5e-icons/assets/walkie_talkie/history_anim/anim13.png deleted file mode 100644 index b4338f7..0000000 Binary files a/s5e-icons/assets/walkie_talkie/history_anim/anim13.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/history_anim/anim14.png b/s5e-icons/assets/walkie_talkie/history_anim/anim14.png deleted file mode 100644 index 6dd42f1..0000000 Binary files a/s5e-icons/assets/walkie_talkie/history_anim/anim14.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/history_anim/anim15.png b/s5e-icons/assets/walkie_talkie/history_anim/anim15.png deleted file mode 100644 index e4795f0..0000000 Binary files a/s5e-icons/assets/walkie_talkie/history_anim/anim15.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/history_anim/anim16.png b/s5e-icons/assets/walkie_talkie/history_anim/anim16.png deleted file mode 100644 index 5cdb8ad..0000000 Binary files a/s5e-icons/assets/walkie_talkie/history_anim/anim16.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/history_anim/anim17.png b/s5e-icons/assets/walkie_talkie/history_anim/anim17.png deleted file mode 100644 index 9eb6f8b..0000000 Binary files a/s5e-icons/assets/walkie_talkie/history_anim/anim17.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/history_anim/anim18.png b/s5e-icons/assets/walkie_talkie/history_anim/anim18.png deleted file mode 100644 index cdbf172..0000000 Binary files a/s5e-icons/assets/walkie_talkie/history_anim/anim18.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/history_anim/anim2.png b/s5e-icons/assets/walkie_talkie/history_anim/anim2.png deleted file mode 100644 index 5cdb8ad..0000000 Binary files a/s5e-icons/assets/walkie_talkie/history_anim/anim2.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/history_anim/anim3.png b/s5e-icons/assets/walkie_talkie/history_anim/anim3.png deleted file mode 100644 index e4795f0..0000000 Binary files a/s5e-icons/assets/walkie_talkie/history_anim/anim3.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/history_anim/anim4.png b/s5e-icons/assets/walkie_talkie/history_anim/anim4.png deleted file mode 100644 index 6dd42f1..0000000 Binary files a/s5e-icons/assets/walkie_talkie/history_anim/anim4.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/history_anim/anim5.png b/s5e-icons/assets/walkie_talkie/history_anim/anim5.png deleted file mode 100644 index b4338f7..0000000 Binary files a/s5e-icons/assets/walkie_talkie/history_anim/anim5.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/history_anim/anim6.png b/s5e-icons/assets/walkie_talkie/history_anim/anim6.png deleted file mode 100644 index 32c452c..0000000 Binary files a/s5e-icons/assets/walkie_talkie/history_anim/anim6.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/history_anim/anim7.png b/s5e-icons/assets/walkie_talkie/history_anim/anim7.png deleted file mode 100644 index 38d3985..0000000 Binary files a/s5e-icons/assets/walkie_talkie/history_anim/anim7.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/history_anim/anim8.png b/s5e-icons/assets/walkie_talkie/history_anim/anim8.png deleted file mode 100644 index 927d24a..0000000 Binary files a/s5e-icons/assets/walkie_talkie/history_anim/anim8.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/history_anim/anim9.png b/s5e-icons/assets/walkie_talkie/history_anim/anim9.png deleted file mode 100644 index 6d5503a..0000000 Binary files a/s5e-icons/assets/walkie_talkie/history_anim/anim9.png and /dev/null 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 deleted file mode 100644 index cdbf172..0000000 Binary files a/s5e-icons/assets/walkie_talkie/history_anim_right/anim0.png and /dev/null 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 deleted file mode 100644 index 5d99f04..0000000 Binary files a/s5e-icons/assets/walkie_talkie/history_anim_right/anim1.png and /dev/null 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 deleted file mode 100644 index 493b920..0000000 Binary files a/s5e-icons/assets/walkie_talkie/history_anim_right/anim10.png and /dev/null 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 deleted file mode 100644 index e596207..0000000 Binary files a/s5e-icons/assets/walkie_talkie/history_anim_right/anim11.png and /dev/null 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 deleted file mode 100644 index 44ccabf..0000000 Binary files a/s5e-icons/assets/walkie_talkie/history_anim_right/anim12.png and /dev/null 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 deleted file mode 100644 index cc6d96c..0000000 Binary files a/s5e-icons/assets/walkie_talkie/history_anim_right/anim13.png and /dev/null 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 deleted file mode 100644 index 709fe42..0000000 Binary files a/s5e-icons/assets/walkie_talkie/history_anim_right/anim14.png and /dev/null 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 deleted file mode 100644 index 08c909e..0000000 Binary files a/s5e-icons/assets/walkie_talkie/history_anim_right/anim15.png and /dev/null 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 deleted file mode 100644 index 5b5a34a..0000000 Binary files a/s5e-icons/assets/walkie_talkie/history_anim_right/anim16.png and /dev/null 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 deleted file mode 100644 index 5d99f04..0000000 Binary files a/s5e-icons/assets/walkie_talkie/history_anim_right/anim17.png and /dev/null 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 deleted file mode 100644 index cdbf172..0000000 Binary files a/s5e-icons/assets/walkie_talkie/history_anim_right/anim18.png and /dev/null 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 deleted file mode 100644 index 5b5a34a..0000000 Binary files a/s5e-icons/assets/walkie_talkie/history_anim_right/anim2.png and /dev/null 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 deleted file mode 100644 index 08c909e..0000000 Binary files a/s5e-icons/assets/walkie_talkie/history_anim_right/anim3.png and /dev/null 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 deleted file mode 100644 index 709fe42..0000000 Binary files a/s5e-icons/assets/walkie_talkie/history_anim_right/anim4.png and /dev/null 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 deleted file mode 100644 index cc6d96c..0000000 Binary files a/s5e-icons/assets/walkie_talkie/history_anim_right/anim5.png and /dev/null 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 deleted file mode 100644 index 44ccabf..0000000 Binary files a/s5e-icons/assets/walkie_talkie/history_anim_right/anim6.png and /dev/null 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 deleted file mode 100644 index e596207..0000000 Binary files a/s5e-icons/assets/walkie_talkie/history_anim_right/anim7.png and /dev/null 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 deleted file mode 100644 index 493b920..0000000 Binary files a/s5e-icons/assets/walkie_talkie/history_anim_right/anim8.png and /dev/null 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 deleted file mode 100644 index 2286ae4..0000000 Binary files a/s5e-icons/assets/walkie_talkie/history_anim_right/anim9.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/icon_plus.png b/s5e-icons/assets/walkie_talkie/icon_plus.png deleted file mode 100644 index a1117ab..0000000 Binary files a/s5e-icons/assets/walkie_talkie/icon_plus.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker0.png b/s5e-icons/assets/walkie_talkie/image/Speaker0.png deleted file mode 100644 index fc91da7..0000000 Binary files a/s5e-icons/assets/walkie_talkie/image/Speaker0.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker1.png b/s5e-icons/assets/walkie_talkie/image/Speaker1.png deleted file mode 100644 index 66fc559..0000000 Binary files a/s5e-icons/assets/walkie_talkie/image/Speaker1.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker10.png b/s5e-icons/assets/walkie_talkie/image/Speaker10.png deleted file mode 100644 index 2bb9b59..0000000 Binary files a/s5e-icons/assets/walkie_talkie/image/Speaker10.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker100.png b/s5e-icons/assets/walkie_talkie/image/Speaker100.png deleted file mode 100644 index 521ed3e..0000000 Binary files a/s5e-icons/assets/walkie_talkie/image/Speaker100.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker101.png b/s5e-icons/assets/walkie_talkie/image/Speaker101.png deleted file mode 100644 index 9b955dd..0000000 Binary files a/s5e-icons/assets/walkie_talkie/image/Speaker101.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker102.png b/s5e-icons/assets/walkie_talkie/image/Speaker102.png deleted file mode 100644 index 7825c03..0000000 Binary files a/s5e-icons/assets/walkie_talkie/image/Speaker102.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker11.png b/s5e-icons/assets/walkie_talkie/image/Speaker11.png deleted file mode 100644 index c4415dd..0000000 Binary files a/s5e-icons/assets/walkie_talkie/image/Speaker11.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker12.png b/s5e-icons/assets/walkie_talkie/image/Speaker12.png deleted file mode 100644 index 844818b..0000000 Binary files a/s5e-icons/assets/walkie_talkie/image/Speaker12.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker13.png b/s5e-icons/assets/walkie_talkie/image/Speaker13.png deleted file mode 100644 index 89029fd..0000000 Binary files a/s5e-icons/assets/walkie_talkie/image/Speaker13.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker14.png b/s5e-icons/assets/walkie_talkie/image/Speaker14.png deleted file mode 100644 index 221f19a..0000000 Binary files a/s5e-icons/assets/walkie_talkie/image/Speaker14.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker15.png b/s5e-icons/assets/walkie_talkie/image/Speaker15.png deleted file mode 100644 index 4423df7..0000000 Binary files a/s5e-icons/assets/walkie_talkie/image/Speaker15.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker16.png b/s5e-icons/assets/walkie_talkie/image/Speaker16.png deleted file mode 100644 index bd8aa8a..0000000 Binary files a/s5e-icons/assets/walkie_talkie/image/Speaker16.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker17.png b/s5e-icons/assets/walkie_talkie/image/Speaker17.png deleted file mode 100644 index 9ea2e8b..0000000 Binary files a/s5e-icons/assets/walkie_talkie/image/Speaker17.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker18.png b/s5e-icons/assets/walkie_talkie/image/Speaker18.png deleted file mode 100644 index fb5a10b..0000000 Binary files a/s5e-icons/assets/walkie_talkie/image/Speaker18.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker19.png b/s5e-icons/assets/walkie_talkie/image/Speaker19.png deleted file mode 100644 index 3ad3bf7..0000000 Binary files a/s5e-icons/assets/walkie_talkie/image/Speaker19.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker2.png b/s5e-icons/assets/walkie_talkie/image/Speaker2.png deleted file mode 100644 index 8601636..0000000 Binary files a/s5e-icons/assets/walkie_talkie/image/Speaker2.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker20.png b/s5e-icons/assets/walkie_talkie/image/Speaker20.png deleted file mode 100644 index b6ea25b..0000000 Binary files a/s5e-icons/assets/walkie_talkie/image/Speaker20.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker21.png b/s5e-icons/assets/walkie_talkie/image/Speaker21.png deleted file mode 100644 index 14200fd..0000000 Binary files a/s5e-icons/assets/walkie_talkie/image/Speaker21.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker22.png b/s5e-icons/assets/walkie_talkie/image/Speaker22.png deleted file mode 100644 index 3d9c48f..0000000 Binary files a/s5e-icons/assets/walkie_talkie/image/Speaker22.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker23.png b/s5e-icons/assets/walkie_talkie/image/Speaker23.png deleted file mode 100644 index e91fd3d..0000000 Binary files a/s5e-icons/assets/walkie_talkie/image/Speaker23.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker24.png b/s5e-icons/assets/walkie_talkie/image/Speaker24.png deleted file mode 100644 index 393fc07..0000000 Binary files a/s5e-icons/assets/walkie_talkie/image/Speaker24.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker25.png b/s5e-icons/assets/walkie_talkie/image/Speaker25.png deleted file mode 100644 index fa9e68e..0000000 Binary files a/s5e-icons/assets/walkie_talkie/image/Speaker25.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker26.png b/s5e-icons/assets/walkie_talkie/image/Speaker26.png deleted file mode 100644 index 6d43f7d..0000000 Binary files a/s5e-icons/assets/walkie_talkie/image/Speaker26.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker27.png b/s5e-icons/assets/walkie_talkie/image/Speaker27.png deleted file mode 100644 index 0a65418..0000000 Binary files a/s5e-icons/assets/walkie_talkie/image/Speaker27.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker28.png b/s5e-icons/assets/walkie_talkie/image/Speaker28.png deleted file mode 100644 index b0160e4..0000000 Binary files a/s5e-icons/assets/walkie_talkie/image/Speaker28.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker29.png b/s5e-icons/assets/walkie_talkie/image/Speaker29.png deleted file mode 100644 index 1a8c275..0000000 Binary files a/s5e-icons/assets/walkie_talkie/image/Speaker29.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker3.png b/s5e-icons/assets/walkie_talkie/image/Speaker3.png deleted file mode 100644 index 142da0d..0000000 Binary files a/s5e-icons/assets/walkie_talkie/image/Speaker3.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker30.png b/s5e-icons/assets/walkie_talkie/image/Speaker30.png deleted file mode 100644 index c07d68b..0000000 Binary files a/s5e-icons/assets/walkie_talkie/image/Speaker30.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker31.png b/s5e-icons/assets/walkie_talkie/image/Speaker31.png deleted file mode 100644 index b5bf877..0000000 Binary files a/s5e-icons/assets/walkie_talkie/image/Speaker31.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker32.png b/s5e-icons/assets/walkie_talkie/image/Speaker32.png deleted file mode 100644 index c7635aa..0000000 Binary files a/s5e-icons/assets/walkie_talkie/image/Speaker32.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker33.png b/s5e-icons/assets/walkie_talkie/image/Speaker33.png deleted file mode 100644 index 55c5c23..0000000 Binary files a/s5e-icons/assets/walkie_talkie/image/Speaker33.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker34.png b/s5e-icons/assets/walkie_talkie/image/Speaker34.png deleted file mode 100644 index 8291262..0000000 Binary files a/s5e-icons/assets/walkie_talkie/image/Speaker34.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker35.png b/s5e-icons/assets/walkie_talkie/image/Speaker35.png deleted file mode 100644 index 7299baa..0000000 Binary files a/s5e-icons/assets/walkie_talkie/image/Speaker35.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker36.png b/s5e-icons/assets/walkie_talkie/image/Speaker36.png deleted file mode 100644 index ab8b08e..0000000 Binary files a/s5e-icons/assets/walkie_talkie/image/Speaker36.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker37.png b/s5e-icons/assets/walkie_talkie/image/Speaker37.png deleted file mode 100644 index 5f76111..0000000 Binary files a/s5e-icons/assets/walkie_talkie/image/Speaker37.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker38.png b/s5e-icons/assets/walkie_talkie/image/Speaker38.png deleted file mode 100644 index 41cb88e..0000000 Binary files a/s5e-icons/assets/walkie_talkie/image/Speaker38.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker39.png b/s5e-icons/assets/walkie_talkie/image/Speaker39.png deleted file mode 100644 index 72a5863..0000000 Binary files a/s5e-icons/assets/walkie_talkie/image/Speaker39.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker4.png b/s5e-icons/assets/walkie_talkie/image/Speaker4.png deleted file mode 100644 index c7ad479..0000000 Binary files a/s5e-icons/assets/walkie_talkie/image/Speaker4.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker40.png b/s5e-icons/assets/walkie_talkie/image/Speaker40.png deleted file mode 100644 index 81c7391..0000000 Binary files a/s5e-icons/assets/walkie_talkie/image/Speaker40.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker41.png b/s5e-icons/assets/walkie_talkie/image/Speaker41.png deleted file mode 100644 index 9881ad6..0000000 Binary files a/s5e-icons/assets/walkie_talkie/image/Speaker41.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker42.png b/s5e-icons/assets/walkie_talkie/image/Speaker42.png deleted file mode 100644 index 7e9ec32..0000000 Binary files a/s5e-icons/assets/walkie_talkie/image/Speaker42.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker43.png b/s5e-icons/assets/walkie_talkie/image/Speaker43.png deleted file mode 100644 index 216afb2..0000000 Binary files a/s5e-icons/assets/walkie_talkie/image/Speaker43.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker44.png b/s5e-icons/assets/walkie_talkie/image/Speaker44.png deleted file mode 100644 index 873728e..0000000 Binary files a/s5e-icons/assets/walkie_talkie/image/Speaker44.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker45.png b/s5e-icons/assets/walkie_talkie/image/Speaker45.png deleted file mode 100644 index 70535de..0000000 Binary files a/s5e-icons/assets/walkie_talkie/image/Speaker45.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker46.png b/s5e-icons/assets/walkie_talkie/image/Speaker46.png deleted file mode 100644 index 43563a3..0000000 Binary files a/s5e-icons/assets/walkie_talkie/image/Speaker46.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker47.png b/s5e-icons/assets/walkie_talkie/image/Speaker47.png deleted file mode 100644 index f7bd1b7..0000000 Binary files a/s5e-icons/assets/walkie_talkie/image/Speaker47.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker48.png b/s5e-icons/assets/walkie_talkie/image/Speaker48.png deleted file mode 100644 index 6a942f9..0000000 Binary files a/s5e-icons/assets/walkie_talkie/image/Speaker48.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker49.png b/s5e-icons/assets/walkie_talkie/image/Speaker49.png deleted file mode 100644 index f65235c..0000000 Binary files a/s5e-icons/assets/walkie_talkie/image/Speaker49.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker5.png b/s5e-icons/assets/walkie_talkie/image/Speaker5.png deleted file mode 100644 index 8681f10..0000000 Binary files a/s5e-icons/assets/walkie_talkie/image/Speaker5.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker50.png b/s5e-icons/assets/walkie_talkie/image/Speaker50.png deleted file mode 100644 index be90a63..0000000 Binary files a/s5e-icons/assets/walkie_talkie/image/Speaker50.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker51.png b/s5e-icons/assets/walkie_talkie/image/Speaker51.png deleted file mode 100644 index d6de73f..0000000 Binary files a/s5e-icons/assets/walkie_talkie/image/Speaker51.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker52.png b/s5e-icons/assets/walkie_talkie/image/Speaker52.png deleted file mode 100644 index 8b58b2d..0000000 Binary files a/s5e-icons/assets/walkie_talkie/image/Speaker52.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker53.png b/s5e-icons/assets/walkie_talkie/image/Speaker53.png deleted file mode 100644 index 7b3b206..0000000 Binary files a/s5e-icons/assets/walkie_talkie/image/Speaker53.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker54.png b/s5e-icons/assets/walkie_talkie/image/Speaker54.png deleted file mode 100644 index 2367008..0000000 Binary files a/s5e-icons/assets/walkie_talkie/image/Speaker54.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker55.png b/s5e-icons/assets/walkie_talkie/image/Speaker55.png deleted file mode 100644 index dbdf016..0000000 Binary files a/s5e-icons/assets/walkie_talkie/image/Speaker55.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker56.png b/s5e-icons/assets/walkie_talkie/image/Speaker56.png deleted file mode 100644 index 1d3eacb..0000000 Binary files a/s5e-icons/assets/walkie_talkie/image/Speaker56.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker57.png b/s5e-icons/assets/walkie_talkie/image/Speaker57.png deleted file mode 100644 index 97c19e3..0000000 Binary files a/s5e-icons/assets/walkie_talkie/image/Speaker57.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker58.png b/s5e-icons/assets/walkie_talkie/image/Speaker58.png deleted file mode 100644 index a7195d0..0000000 Binary files a/s5e-icons/assets/walkie_talkie/image/Speaker58.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker59.png b/s5e-icons/assets/walkie_talkie/image/Speaker59.png deleted file mode 100644 index 3e36cba..0000000 Binary files a/s5e-icons/assets/walkie_talkie/image/Speaker59.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker6.png b/s5e-icons/assets/walkie_talkie/image/Speaker6.png deleted file mode 100644 index 2503467..0000000 Binary files a/s5e-icons/assets/walkie_talkie/image/Speaker6.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker60.png b/s5e-icons/assets/walkie_talkie/image/Speaker60.png deleted file mode 100644 index cb0e1b9..0000000 Binary files a/s5e-icons/assets/walkie_talkie/image/Speaker60.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker61.png b/s5e-icons/assets/walkie_talkie/image/Speaker61.png deleted file mode 100644 index 93b8905..0000000 Binary files a/s5e-icons/assets/walkie_talkie/image/Speaker61.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker62.png b/s5e-icons/assets/walkie_talkie/image/Speaker62.png deleted file mode 100644 index d171dab..0000000 Binary files a/s5e-icons/assets/walkie_talkie/image/Speaker62.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker63.png b/s5e-icons/assets/walkie_talkie/image/Speaker63.png deleted file mode 100644 index 3bdfd0f..0000000 Binary files a/s5e-icons/assets/walkie_talkie/image/Speaker63.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker64.png b/s5e-icons/assets/walkie_talkie/image/Speaker64.png deleted file mode 100644 index 392093f..0000000 Binary files a/s5e-icons/assets/walkie_talkie/image/Speaker64.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker65.png b/s5e-icons/assets/walkie_talkie/image/Speaker65.png deleted file mode 100644 index e26ca7c..0000000 Binary files a/s5e-icons/assets/walkie_talkie/image/Speaker65.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker66.png b/s5e-icons/assets/walkie_talkie/image/Speaker66.png deleted file mode 100644 index 57aff14..0000000 Binary files a/s5e-icons/assets/walkie_talkie/image/Speaker66.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker67.png b/s5e-icons/assets/walkie_talkie/image/Speaker67.png deleted file mode 100644 index 2b7281a..0000000 Binary files a/s5e-icons/assets/walkie_talkie/image/Speaker67.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker68.png b/s5e-icons/assets/walkie_talkie/image/Speaker68.png deleted file mode 100644 index cdb5592..0000000 Binary files a/s5e-icons/assets/walkie_talkie/image/Speaker68.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker69.png b/s5e-icons/assets/walkie_talkie/image/Speaker69.png deleted file mode 100644 index dc7eba5..0000000 Binary files a/s5e-icons/assets/walkie_talkie/image/Speaker69.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker7.png b/s5e-icons/assets/walkie_talkie/image/Speaker7.png deleted file mode 100644 index 054a489..0000000 Binary files a/s5e-icons/assets/walkie_talkie/image/Speaker7.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker70.png b/s5e-icons/assets/walkie_talkie/image/Speaker70.png deleted file mode 100644 index 6bbe93d..0000000 Binary files a/s5e-icons/assets/walkie_talkie/image/Speaker70.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker71.png b/s5e-icons/assets/walkie_talkie/image/Speaker71.png deleted file mode 100644 index bc451fb..0000000 Binary files a/s5e-icons/assets/walkie_talkie/image/Speaker71.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker72.png b/s5e-icons/assets/walkie_talkie/image/Speaker72.png deleted file mode 100644 index 69b5ee7..0000000 Binary files a/s5e-icons/assets/walkie_talkie/image/Speaker72.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker73.png b/s5e-icons/assets/walkie_talkie/image/Speaker73.png deleted file mode 100644 index 909de00..0000000 Binary files a/s5e-icons/assets/walkie_talkie/image/Speaker73.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker74.png b/s5e-icons/assets/walkie_talkie/image/Speaker74.png deleted file mode 100644 index a2d65b2..0000000 Binary files a/s5e-icons/assets/walkie_talkie/image/Speaker74.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker75.png b/s5e-icons/assets/walkie_talkie/image/Speaker75.png deleted file mode 100644 index 77d3cf6..0000000 Binary files a/s5e-icons/assets/walkie_talkie/image/Speaker75.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker76.png b/s5e-icons/assets/walkie_talkie/image/Speaker76.png deleted file mode 100644 index 4bacc59..0000000 Binary files a/s5e-icons/assets/walkie_talkie/image/Speaker76.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker77.png b/s5e-icons/assets/walkie_talkie/image/Speaker77.png deleted file mode 100644 index 3527380..0000000 Binary files a/s5e-icons/assets/walkie_talkie/image/Speaker77.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker78.png b/s5e-icons/assets/walkie_talkie/image/Speaker78.png deleted file mode 100644 index f9cfe31..0000000 Binary files a/s5e-icons/assets/walkie_talkie/image/Speaker78.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker79.png b/s5e-icons/assets/walkie_talkie/image/Speaker79.png deleted file mode 100644 index 4583948..0000000 Binary files a/s5e-icons/assets/walkie_talkie/image/Speaker79.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker8.png b/s5e-icons/assets/walkie_talkie/image/Speaker8.png deleted file mode 100644 index 6b885ec..0000000 Binary files a/s5e-icons/assets/walkie_talkie/image/Speaker8.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker80.png b/s5e-icons/assets/walkie_talkie/image/Speaker80.png deleted file mode 100644 index 6dbe5d3..0000000 Binary files a/s5e-icons/assets/walkie_talkie/image/Speaker80.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker81.png b/s5e-icons/assets/walkie_talkie/image/Speaker81.png deleted file mode 100644 index cdff971..0000000 Binary files a/s5e-icons/assets/walkie_talkie/image/Speaker81.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker82.png b/s5e-icons/assets/walkie_talkie/image/Speaker82.png deleted file mode 100644 index 304ccea..0000000 Binary files a/s5e-icons/assets/walkie_talkie/image/Speaker82.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker83.png b/s5e-icons/assets/walkie_talkie/image/Speaker83.png deleted file mode 100644 index fe62e25..0000000 Binary files a/s5e-icons/assets/walkie_talkie/image/Speaker83.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker84.png b/s5e-icons/assets/walkie_talkie/image/Speaker84.png deleted file mode 100644 index 263f40c..0000000 Binary files a/s5e-icons/assets/walkie_talkie/image/Speaker84.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker85.png b/s5e-icons/assets/walkie_talkie/image/Speaker85.png deleted file mode 100644 index 060ae88..0000000 Binary files a/s5e-icons/assets/walkie_talkie/image/Speaker85.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker86.png b/s5e-icons/assets/walkie_talkie/image/Speaker86.png deleted file mode 100644 index c8fec2a..0000000 Binary files a/s5e-icons/assets/walkie_talkie/image/Speaker86.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker87.png b/s5e-icons/assets/walkie_talkie/image/Speaker87.png deleted file mode 100644 index 053ee6d..0000000 Binary files a/s5e-icons/assets/walkie_talkie/image/Speaker87.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker88.png b/s5e-icons/assets/walkie_talkie/image/Speaker88.png deleted file mode 100644 index 15505f5..0000000 Binary files a/s5e-icons/assets/walkie_talkie/image/Speaker88.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker89.png b/s5e-icons/assets/walkie_talkie/image/Speaker89.png deleted file mode 100644 index 162e637..0000000 Binary files a/s5e-icons/assets/walkie_talkie/image/Speaker89.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker9.png b/s5e-icons/assets/walkie_talkie/image/Speaker9.png deleted file mode 100644 index 03b9b1e..0000000 Binary files a/s5e-icons/assets/walkie_talkie/image/Speaker9.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker90.png b/s5e-icons/assets/walkie_talkie/image/Speaker90.png deleted file mode 100644 index ceec461..0000000 Binary files a/s5e-icons/assets/walkie_talkie/image/Speaker90.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker91.png b/s5e-icons/assets/walkie_talkie/image/Speaker91.png deleted file mode 100644 index b3b35ae..0000000 Binary files a/s5e-icons/assets/walkie_talkie/image/Speaker91.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker92.png b/s5e-icons/assets/walkie_talkie/image/Speaker92.png deleted file mode 100644 index 3464b7d..0000000 Binary files a/s5e-icons/assets/walkie_talkie/image/Speaker92.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker93.png b/s5e-icons/assets/walkie_talkie/image/Speaker93.png deleted file mode 100644 index de800f6..0000000 Binary files a/s5e-icons/assets/walkie_talkie/image/Speaker93.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker94.png b/s5e-icons/assets/walkie_talkie/image/Speaker94.png deleted file mode 100644 index 51e6c2a..0000000 Binary files a/s5e-icons/assets/walkie_talkie/image/Speaker94.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker95.png b/s5e-icons/assets/walkie_talkie/image/Speaker95.png deleted file mode 100644 index a22dffe..0000000 Binary files a/s5e-icons/assets/walkie_talkie/image/Speaker95.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker96.png b/s5e-icons/assets/walkie_talkie/image/Speaker96.png deleted file mode 100644 index f7151aa..0000000 Binary files a/s5e-icons/assets/walkie_talkie/image/Speaker96.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker97.png b/s5e-icons/assets/walkie_talkie/image/Speaker97.png deleted file mode 100644 index eae1262..0000000 Binary files a/s5e-icons/assets/walkie_talkie/image/Speaker97.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker98.png b/s5e-icons/assets/walkie_talkie/image/Speaker98.png deleted file mode 100644 index 328931c..0000000 Binary files a/s5e-icons/assets/walkie_talkie/image/Speaker98.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/image/Speaker99.png b/s5e-icons/assets/walkie_talkie/image/Speaker99.png deleted file mode 100644 index 11a494e..0000000 Binary files a/s5e-icons/assets/walkie_talkie/image/Speaker99.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/image/go_next.png b/s5e-icons/assets/walkie_talkie/image/go_next.png deleted file mode 100644 index e9a1d81..0000000 Binary files a/s5e-icons/assets/walkie_talkie/image/go_next.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/image/stop.png b/s5e-icons/assets/walkie_talkie/image/stop.png deleted file mode 100644 index b611709..0000000 Binary files a/s5e-icons/assets/walkie_talkie/image/stop.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/launcher.png b/s5e-icons/assets/walkie_talkie/launcher.png deleted file mode 100644 index 9a611a9..0000000 Binary files a/s5e-icons/assets/walkie_talkie/launcher.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/launcher1.png b/s5e-icons/assets/walkie_talkie/launcher1.png deleted file mode 100644 index 60aa469..0000000 Binary files a/s5e-icons/assets/walkie_talkie/launcher1.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/listen.png b/s5e-icons/assets/walkie_talkie/listen.png deleted file mode 100644 index e06f591..0000000 Binary files a/s5e-icons/assets/walkie_talkie/listen.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/login_success.png b/s5e-icons/assets/walkie_talkie/login_success.png deleted file mode 100644 index 6e8c826..0000000 Binary files a/s5e-icons/assets/walkie_talkie/login_success.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/next_btn.png b/s5e-icons/assets/walkie_talkie/next_btn.png deleted file mode 100644 index 415abc0..0000000 Binary files a/s5e-icons/assets/walkie_talkie/next_btn.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/poc/default_icon.png b/s5e-icons/assets/walkie_talkie/poc/default_icon.png deleted file mode 100644 index d5c33f0..0000000 Binary files a/s5e-icons/assets/walkie_talkie/poc/default_icon.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/poc/group_icon1.png b/s5e-icons/assets/walkie_talkie/poc/group_icon1.png deleted file mode 100644 index 9f22404..0000000 Binary files a/s5e-icons/assets/walkie_talkie/poc/group_icon1.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/poc/group_icon2.png b/s5e-icons/assets/walkie_talkie/poc/group_icon2.png deleted file mode 100644 index 0adef53..0000000 Binary files a/s5e-icons/assets/walkie_talkie/poc/group_icon2.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/poc/group_icon3.png b/s5e-icons/assets/walkie_talkie/poc/group_icon3.png deleted file mode 100644 index 4f1560b..0000000 Binary files a/s5e-icons/assets/walkie_talkie/poc/group_icon3.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/poc/group_icon4.png b/s5e-icons/assets/walkie_talkie/poc/group_icon4.png deleted file mode 100644 index 5bb064a..0000000 Binary files a/s5e-icons/assets/walkie_talkie/poc/group_icon4.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/poc/group_icon5.png b/s5e-icons/assets/walkie_talkie/poc/group_icon5.png deleted file mode 100644 index abd170d..0000000 Binary files a/s5e-icons/assets/walkie_talkie/poc/group_icon5.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/poc/member_icon.png b/s5e-icons/assets/walkie_talkie/poc/member_icon.png deleted file mode 100644 index d5c33f0..0000000 Binary files a/s5e-icons/assets/walkie_talkie/poc/member_icon.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/poc/no_sound.png b/s5e-icons/assets/walkie_talkie/poc/no_sound.png deleted file mode 100644 index 2244475..0000000 Binary files a/s5e-icons/assets/walkie_talkie/poc/no_sound.png and /dev/null 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 deleted file mode 100644 index 64e5ea1..0000000 Binary files a/s5e-icons/assets/walkie_talkie/poc/say_button_mic_icon.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/poc/sound.png b/s5e-icons/assets/walkie_talkie/poc/sound.png deleted file mode 100644 index 9b7b453..0000000 Binary files a/s5e-icons/assets/walkie_talkie/poc/sound.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/poc/talk_listen.png b/s5e-icons/assets/walkie_talkie/poc/talk_listen.png deleted file mode 100644 index c9a9688..0000000 Binary files a/s5e-icons/assets/walkie_talkie/poc/talk_listen.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/poc/toast_listen.png b/s5e-icons/assets/walkie_talkie/poc/toast_listen.png deleted file mode 100644 index c9a9688..0000000 Binary files a/s5e-icons/assets/walkie_talkie/poc/toast_listen.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/poc/toast_say.png b/s5e-icons/assets/walkie_talkie/poc/toast_say.png deleted file mode 100644 index f5b5dec..0000000 Binary files a/s5e-icons/assets/walkie_talkie/poc/toast_say.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/poc/tools.png b/s5e-icons/assets/walkie_talkie/poc/tools.png deleted file mode 100644 index e7cf9be..0000000 Binary files a/s5e-icons/assets/walkie_talkie/poc/tools.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/poc/voice_icon.png b/s5e-icons/assets/walkie_talkie/poc/voice_icon.png deleted file mode 100644 index 85e366d..0000000 Binary files a/s5e-icons/assets/walkie_talkie/poc/voice_icon.png and /dev/null 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 deleted file mode 100644 index 501320a..0000000 Binary files a/s5e-icons/assets/walkie_talkie/poc/voice_icon_rt.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/speak.png b/s5e-icons/assets/walkie_talkie/speak.png deleted file mode 100644 index 1b715e3..0000000 Binary files a/s5e-icons/assets/walkie_talkie/speak.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/walkie_talkie.png b/s5e-icons/assets/walkie_talkie/walkie_talkie.png deleted file mode 100644 index 9a611a9..0000000 Binary files a/s5e-icons/assets/walkie_talkie/walkie_talkie.png and /dev/null differ diff --git a/s5e-icons/assets/walkie_talkie/walkie_toast_icon.png b/s5e-icons/assets/walkie_talkie/walkie_toast_icon.png deleted file mode 100644 index 81f4e3f..0000000 Binary files a/s5e-icons/assets/walkie_talkie/walkie_toast_icon.png and /dev/null differ diff --git a/s5e-icons/dist/S5e-Pt1.face b/s5e-icons/dist/S5e-Pt1.face deleted file mode 100644 index 5b0ce0e..0000000 Binary files a/s5e-icons/dist/S5e-Pt1.face and /dev/null differ diff --git a/s5e-icons/dist/S5eIcons_toolchain.face b/s5e-icons/dist/S5eIcons_toolchain.face deleted file mode 100644 index 06f6d27..0000000 Binary files a/s5e-icons/dist/S5eIcons_toolchain.face and /dev/null differ diff --git a/s5e-icons/docs/logo_preview_480.png b/s5e-icons/docs/logo_preview_480.png deleted file mode 100644 index 912706f..0000000 Binary files a/s5e-icons/docs/logo_preview_480.png and /dev/null differ diff --git a/s5e-icons/docs/s5e_before_after.png b/s5e-icons/docs/s5e_before_after.png deleted file mode 100644 index 61e05c9..0000000 Binary files a/s5e-icons/docs/s5e_before_after.png and /dev/null differ diff --git a/s5e-icons/docs/s5e_pressure.png b/s5e-icons/docs/s5e_pressure.png deleted file mode 100644 index b6d1043..0000000 Binary files a/s5e-icons/docs/s5e_pressure.png and /dev/null differ diff --git a/s5e-icons/docs/s5e_sysicons.png b/s5e-icons/docs/s5e_sysicons.png deleted file mode 100644 index 1c12b16..0000000 Binary files a/s5e-icons/docs/s5e_sysicons.png and /dev/null differ diff --git a/s5e-icons/docs/s5e_ui_about.png b/s5e-icons/docs/s5e_ui_about.png deleted file mode 100644 index 55b2212..0000000 Binary files a/s5e-icons/docs/s5e_ui_about.png and /dev/null differ diff --git a/s5e-icons/docs/s5e_ui_all.png b/s5e-icons/docs/s5e_ui_all.png deleted file mode 100644 index 299a5bb..0000000 Binary files a/s5e-icons/docs/s5e_ui_all.png and /dev/null differ diff --git a/s5e-icons/docs/s5e_ui_confirm.png b/s5e-icons/docs/s5e_ui_confirm.png deleted file mode 100644 index 664aee9..0000000 Binary files a/s5e-icons/docs/s5e_ui_confirm.png and /dev/null differ diff --git a/s5e-icons/docs/s5e_ui_help.png b/s5e-icons/docs/s5e_ui_help.png deleted file mode 100644 index 7aac976..0000000 Binary files a/s5e-icons/docs/s5e_ui_help.png and /dev/null differ diff --git a/s5e-icons/docs/s5e_ui_help_err.png b/s5e-icons/docs/s5e_ui_help_err.png deleted file mode 100644 index 0fb8452..0000000 Binary files a/s5e-icons/docs/s5e_ui_help_err.png and /dev/null differ diff --git a/s5e-icons/docs/s5e_ui_help_ok.png b/s5e-icons/docs/s5e_ui_help_ok.png deleted file mode 100644 index c7e7e40..0000000 Binary files a/s5e-icons/docs/s5e_ui_help_ok.png and /dev/null differ diff --git a/s5e-icons/docs/s5e_ui_home.png b/s5e-icons/docs/s5e_ui_home.png deleted file mode 100644 index 2500de6..0000000 Binary files a/s5e-icons/docs/s5e_ui_home.png and /dev/null differ diff --git a/s5e-icons/docs/s5e_ui_running.png b/s5e-icons/docs/s5e_ui_running.png deleted file mode 100644 index 81b5fc9..0000000 Binary files a/s5e-icons/docs/s5e_ui_running.png and /dev/null differ diff --git a/s5e-icons/docs/stock_vs_os4.png b/s5e-icons/docs/stock_vs_os4.png deleted file mode 100644 index f49a746..0000000 Binary files a/s5e-icons/docs/stock_vs_os4.png and /dev/null differ diff --git a/s5e-icons/docs/zoom_check.png b/s5e-icons/docs/zoom_check.png deleted file mode 100644 index d73f67c..0000000 Binary files a/s5e-icons/docs/zoom_check.png and /dev/null differ diff --git a/s5e-icons/docs/历史-S5eSIM-图标替换-说明.md b/s5e-icons/docs/历史-S5eSIM-图标替换-说明.md deleted file mode 100644 index 19e6b35..0000000 --- a/s5e-icons/docs/历史-S5eSIM-图标替换-说明.md +++ /dev/null @@ -1,246 +0,0 @@ -# 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 deleted file mode 100644 index 15edb84..0000000 --- a/s5e-icons/docs/说明.md +++ /dev/null @@ -1,253 +0,0 @@ -# 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 deleted file mode 100644 index 979bb85..0000000 --- a/s5e-icons/main.lua +++ /dev/null @@ -1,681 +0,0 @@ ---[[---------------------------------------------------------------------------- - 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) - -- 盘内标题 = 显示名:'S5e Pt.1' 31px 量得 121 -> x = (480-121)/2 = 179.5 -> 179 - pageTitle(p, 'S5e Pt.1', 179, 123) - 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 - -- 'S5e Pt.1' 29px 量得 114 -> 居中 x = 54 + (373-114)/2 = 183.5 -> 183 - lab(p, { x = 183, y = 198, w = 116, h = 40, text = 'S5e Pt.1', - 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 deleted file mode 100644 index 557f955..0000000 --- a/s5e-icons/payload/payload.lua +++ /dev/null @@ -1,188 +0,0 @@ --- 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 deleted file mode 100644 index 3eeeb98..0000000 --- a/s5e-icons/payload_rollback/rollback.lua +++ /dev/null @@ -1,188 +0,0 @@ --- 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 deleted file mode 100644 index 4012c6e..0000000 --- a/s5e-icons/tools/iconpack.py +++ /dev/null @@ -1,447 +0,0 @@ -"""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 deleted file mode 100644 index a33ebba..0000000 --- a/s5e-icons/tools/lua_balance.py +++ /dev/null @@ -1,72 +0,0 @@ -"""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 deleted file mode 100644 index 753dda2..0000000 --- a/s5e-icons/tools/romfs.py +++ /dev/null @@ -1,112 +0,0 @@ -"""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 deleted file mode 100644 index 8642761..0000000 --- a/s5e-icons/tools/s5e_build.py +++ /dev/null @@ -1,290 +0,0 @@ -# -*- 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' # 路径/标识(文件名、目录名、dist 名都用它,**不要改**) -DISPLAY_NAME = 'S5e Pt.1' # 手表上显示的表盘名 = fprj 的 = config 的 projectName -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": DISPLAY_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, DISPLAY_NAME, SCREEN, SCREEN) - fprjdir = os.path.join(BUILD, 'watchface', 'fprj') - open(os.path.join(fprjdir, NAME + '.fprj'), 'w', - encoding='utf-16', newline='').write(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') - # 显式给 -FaceName:vendor 脚本默认拿 config 的 projectName 当输出名,而 projectName - # 现在是**显示名**(S5e Pt.1),交付文件名必须还是 NAME.face。 - r = subprocess.run(['powershell', '-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', ps, - '-FaceName', NAME + '.face'], - capture_output=True, text=True, errors='replace') - workspace_fprj() # vendor 可能把 .fprj 改名成显示名,编完改回 NAME.fprj - 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 workspace_fprj(): - """把工作区里的 .fprj 归一成 NAME.fprj,并返回它的路径。 - - 我们写下去时就是 NAME.fprj,但 vendor 的 build_face.ps1 会先跑 - sync_watchface_config.ps1:它按 config 的 projectName(现在是**显示名**)找 - .fprj,找不到就把唯一那份改名过去(Title 也被改写成 projectName)。 - 编完这里再改回 NAME.fprj —— 这样工作区里始终只有一份 .fprj(放两份会被 - Compiler.exe 并成双倍资源,实测资源整体翻倍),文件名也保持项目标识不变。 - """ - d = os.path.join(BUILD, 'watchface', 'fprj') - fs = sorted(f for f in os.listdir(d) if f.endswith('.fprj')) - if len(fs) != 1: - raise SystemExit('watchface/fprj 下的 .fprj 应恰好 1 份,实际:%s' % fs) - want = os.path.join(d, NAME + '.fprj') - if fs[0] != NAME + '.fprj': - os.replace(os.path.join(d, fs[0]), want) - return want - - -def compile_raw(): - exe = os.path.join(BUILD, 'watchface', 'tools', 'Compiler.exe') - fprj = workspace_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 deleted file mode 100644 index 56a1b7c..0000000 --- a/s5e-icons/tools/s5e_pages.py +++ /dev/null @@ -1,133 +0,0 @@ -# -*- 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('S5e Pt.1', 179, 123), - *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=183, y=198, w=116, h=40, text='S5e Pt.1', - 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 deleted file mode 100644 index ce17706..0000000 --- a/s5e-icons/tools/s5e_payload.py +++ /dev/null @@ -1,808 +0,0 @@ -# -*- 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 deleted file mode 100644 index 21a090d..0000000 --- a/s5e-icons/tools/s5e_render_ui.py +++ /dev/null @@ -1,30 +0,0 @@ -# -*- 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 deleted file mode 100644 index 5f6b007..0000000 --- a/s5e-icons/tools/s5e_ui_images.py +++ /dev/null @@ -1,38 +0,0 @@ -# -*- 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 deleted file mode 100644 index 3f4b6e0..0000000 --- a/s5e-icons/tools/s5e_verify.py +++ /dev/null @@ -1,120 +0,0 @@ -# -*- 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 deleted file mode 100644 index 76eb40c..0000000 Binary files a/s5e-icons/ui_images/back.bin and /dev/null differ diff --git a/s5e-icons/ui_images/ic_about.bin b/s5e-icons/ui_images/ic_about.bin deleted file mode 100644 index 2476014..0000000 Binary files a/s5e-icons/ui_images/ic_about.bin and /dev/null differ diff --git a/s5e-icons/ui_images/ic_check.bin b/s5e-icons/ui_images/ic_check.bin deleted file mode 100644 index 177ead3..0000000 Binary files a/s5e-icons/ui_images/ic_check.bin and /dev/null differ diff --git a/s5e-icons/ui_images/ic_help.bin b/s5e-icons/ui_images/ic_help.bin deleted file mode 100644 index 9ed2ddb..0000000 Binary files a/s5e-icons/ui_images/ic_help.bin and /dev/null differ diff --git a/s5e-icons/ui_images/ic_qq.bin b/s5e-icons/ui_images/ic_qq.bin deleted file mode 100644 index 7db71e4..0000000 Binary files a/s5e-icons/ui_images/ic_qq.bin and /dev/null differ diff --git a/s5e-icons/ui_images/ic_replace.bin b/s5e-icons/ui_images/ic_replace.bin deleted file mode 100644 index a231ebd..0000000 Binary files a/s5e-icons/ui_images/ic_replace.bin and /dev/null differ diff --git a/s5e-icons/ui_images/ic_rollback.bin b/s5e-icons/ui_images/ic_rollback.bin deleted file mode 100644 index 19debcb..0000000 Binary files a/s5e-icons/ui_images/ic_rollback.bin and /dev/null differ diff --git a/s5e-icons/ui_images/logo.bin b/s5e-icons/ui_images/logo.bin deleted file mode 100644 index 6935df5..0000000 Binary files a/s5e-icons/ui_images/logo.bin and /dev/null differ diff --git a/s5e-pressure-anim/.gitignore b/s5e-pressure-anim/.gitignore deleted file mode 100644 index 13a0346..0000000 --- a/s5e-pressure-anim/.gitignore +++ /dev/null @@ -1,10 +0,0 @@ -# 大文件 / 中间产物:不进仓库 -.work/ -inputs/ -vendor/ -__pycache__/ -*.pyc -payload/ -payload_restore/ -blob_replace/ -blob_restore/ diff --git a/s5e-pressure-anim/README.md b/s5e-pressure-anim/README.md deleted file mode 100644 index b93d11d..0000000 --- a/s5e-pressure-anim/README.md +++ /dev/null @@ -1,319 +0,0 @@ -# S5ePAnim —— 把 S5 41mm(q63) 的压力检测动画搬到 S5 eSIM 46mm - -把 **Xiaomi Watch S5 41mm(q63, v4.101.020)** 压力检测的 **100 帧 480×480** 动画, -逐帧**等长覆盖**写进 **Xiaomi Watch S5 eSIM 46mm(p62lte, v3.112.035)** `/dev/health` 里 -`pressure/measure/Measuring1..119.bin` 的 **119 个槽位**(**464×464** I8 + LVGL RLE)。 - -> 关系:`s4e-pressure-anim` 是「S5 41mm → S4 eSIM(466×466)」的母项目(只读参考)。 -> 本项目沿用它的全部结构(两遍构建回填偏移、指纹表拆盘、逐字节等长覆盖、独立校验), -> 只把目标换成 S5 eSIM、界面重做成 480×480、写入节点换成 `/dev/health`。 - -## 交付物 - -| 文件 | 字节数 | id / b5 | sha256 | -|---|---|---|---| -| `dist/S5ePAnim.face` | **16,450,844** | `462150102` / b5=0 | `d0bb012a364fd46eb4bb27db26ead72ae3166060c72c08ec5a76670dc06a3b1c` | -| `dist/S5ePAnimRestore.face` | **16,450,844** | `462150102` / b5=0 | `13cafae4bf2b03db82f2c17f9d3af7c7e8c01d0c26275feda301753a76ebda69` | - -两份盘**共用同一个表盘 id**(`462150102`):装其中一份就会顶掉另一份,设备上始终只占一份空间 -(合在一张盘要 ~31 MB,超体积 —— 这是当初拆盘的原因)。 - -* `S5ePAnim.face`:`pay` = 替换后的 119 条(q63 的帧),`rb` = **原厂**前 64 B 指纹(只给版本校验用) -* `S5ePAnimRestore.face`:`pay` = **原厂** 119 条(逐字节),`rb` = 替换后前 64 B 指纹 - -两张盘除了负载/指纹与 Lua 里的两张表以外没有任何差别(头 50 B、8 张界面素材、资源基址逐字节相同)。 - -装法与普通 Lua 表盘相同(把生成的 `resource.bin` 推到表盘目录,例如 -`/data/app/watchface/market/462150102/`)。装好后在表盘上操作: - -``` -首页 →「快速帮助」→「环境预检查」 只读,一个字节都不写 -首页 →「开始替换」→ 确认页 →「确认替换」 写入 119 条 → 「重启手表」 -还原:装 dist/S5ePAnimRestore.face(同 id 顶掉替换盘)→「开始还原」→ 重启 -``` - -## 源 / 目标 - -| | 分区 | 槽位 | 尺寸 / 格式 | 单条大小 / 合计 | -|---|---|---|---|---| -| **目标** S5 eSIM 46mm(p62lte v3.112.035) | `inputs/target/vela_health.bin`(`/dev/health`,卷名 `health`) | `pressure/measure/Measuring1..119.bin` | 464×464 I8 **RLE**,`usize` **216,321 = 1024 + 464×464 + 1** | 4,485 ~ 196,538 B,共 **15.49 MB** | -| **源** S5 41mm(q63 v4.101.020) | `inputs/source/vela_app.bin` | `pressure/measure/Measuring1..100.bin` | 480×480 I8 **RLE**,`usize` 231,424 = 1024 + 480×480(**没有**尾巴字节) | ~182 KB / 帧 | - -* 目标镜像 sha256 `489752c8a3fb24fff3e167610964795f0faaf830f142223b63be6ac2f57e67aa` - (206,924,800 B,sb_size 206,924,736,superblock cksum `b804503a`)—— **偏移锁死这一版固件**。 -* 改后镜像 `.work/anim/vela_health_anim.bin` sha256 `b11b72fcb38ad75a643d93d7db4dd366477ac33bfed93ef91096e04a73d67479`。 - -**映射**:第 k 个槽位(k=1..119) ← q63 第 `round((k-1)/(119-1)*(100-1))+1` 帧(1..100)。 -119 与 100 不是整数倍关系,所以有 19 帧被连续两个槽位复用(`119↔100` 比例重采样,**不是** 1:1), -帧号顺序与速度与原动画一致。 - -### 目标固件的事实(从固件包里查出来的) - -| 项 | 结论 | 证据 | -|---|---|---| -| 写入分区 | `vela_health.bin`,**卷名 `health`**,设备节点 `/dev/health` | ROMFS superblock 卷名 = `health`;206,924,800 B | -| 压力动画槽位 | `pressure/measure/Measuring1.bin … Measuring119.bin`,**119 帧** | 逐条枚举 ROMFS;编号连续 1..119 | -| 槽位格式 | 464×464,`cf=0x0a`(I8),`flags=0x08`(RLE),`method=1`,`stride=464` | 每条 12 B 图像头 + 12 B 压缩头;119 条的 `usize` 全是 216,321 | -| 槽位长度 | 4,485 ~ 196,538 B(差 40 倍),合计 15.49 MB | 逐条统计;`槽位长度 = 24 B 头 + csize` | -| 源素材 | q63 的 `vela_app.bin` 里 `pressure/measure/Measuring1..100.bin` | q63 的 `vela_health.bin` 里**没有** `pressure/measure/` | -| 源格式 | 480×480 I8 + RLE,`usize` 231,424,约 182 KB/帧 | 底噪是接近黑的 `#1a1a1a` 抖动 | -| 表盘宿主 | Lua 5.4,480×480 圆屏;`Compiler.exe` 对 DeviceType 462 强制 326×326 预览 | 同 `s5e-icons` 的实测结论 | - -## 怎么做到「等长覆盖」 - -ROMFS 里每个文件的数据长度是硬编码的(后面所有文件的偏移都是绝对值),所以槽位必须**逐字节等长**替换: -`槽位长度 = 24 B 头(12 B 图像头 + 12 B 压缩头)+ csize 字节 RLE 流`。而两边尺寸不同、还都是 RLE, -压缩后长度各不相同。流水线(`tools/s5e_anim.py`): - -1. **暗部归零**(能不能塞进去的前提):q63 的"黑"其实是一条接近黑的抖动底噪,熵极高; - 阈值(亮度 < 40,部分帧 < 56)以下的像素一律钉成纯黑之后,背景塌成整行单游程, - RLE 体积掉到约 1/4。球体最暗的实体像素远高于阈值,所以只吃底噪、不动画面。 -2. **缩放 + 量化**:480×480 → 464×464,在一条 34 档的阶梯上挑一档 - (256 色@1.00 → … → 32 色@0.70 → … → 2 色@0.05)。 -3. **平滑**:以各帧"还能保住 ≥32 色"的最大缩放为上限,从峰值向两侧取**运行最小值**, - 得到单峰、单调的曲线,避免相邻帧忽大忽小(S4e 真机反馈过"忽大忽小")。 -4. **编码**:LVGL RLE(`tools/lvgl_bin.py: rle_compress`)。 -5. **补齐**:`pad_rle_safe` 用两类**产出 0 像素**的合法指令把流补到 csize - (`00 00` = 重复 0 次;`80` = 原样 0 个,用来修奇偶)。 -6. **usize 尾巴**:解码长度必须**恰好**等于 header 里的 `usize`。 - 目标槽位的 `usize = 216,321 = 1024 + 464×464 + **1**` —— 像素区之后还有 1 个字节; - 我们编码时也补出这 1 个字节(`usize_tail`),否则设备端 RLE 解码器认为流不完整, - **整帧直接不画**(S4e 真机上就是这个现象:黑屏、只偶尔闪一下原版帧)。 - `s5e_anim.py` 里对这条有显式断言(源世代是 0 字节尾巴、目标世代是 1 字节,两者不同)。 - -### 编码期的三条就地自证(每条负载都跑) - -1. `len(decode(stream)) == usize`(少一字节设备端就不画) -2. 解出来的像素 == 量化结果(逐点) -3. 量化结果的**形状**(非黑像素数 + bbox)与源图一致 - -> 第 3 条是被真 bug 逼出来的:早期版本量化时按 472 宽取像素、却读成 464 宽,整幅图被剪成 -> **斜条纹** —— RLE 流完全正确、解出来也和"量化结果"逐点一致,只有比形状才抓得住。 - -**替换质量**:**119/119 全部替换**,`kept_stock = []`(没有一帧保留原厂)。 -颜色分布:256 色 91 帧、192 色 1、128 色 2、96 色 1、64 色 7、48 色 3、32 色 6、 -24 色 1、16 色 2、12 色 1、4 色 4。 - -* 开头 8 帧(`Measuring1..8`,4~24 色、缩放 0.16~0.65)是**设计如此**:动画从一个小点长起来, - 平滑曲线本身的目标档就在低端,不是被迫降级。 -* **14 帧**(`Measuring106..119`)是**被迫退档**:平滑目标档(256 色@1.00)塞不进那 14 个偏小的槽位 - (槽位 19,025~77,823 B),退到 192/128/64/48/32 色 + 0.70~0.90 缩放;逐帧的 `(ci, target_ci, how)` - 都在 `.work/anim/manifest.json` 的 `records` / `degraded` 字段里。 -* 预算利用率约 **49%**(编码 7.88 MB / 槽位 16.24 MB):源帧本身是抖动过的 256 色图, - 去掉底噪后在 464×464 下的熵上限就是约 80 KB,而尾部槽位有 150~196 KB。 - 想把预算填满只能把源图的**抖动颗粒**一起搬过去(背景会泛灰,也不是有效细节), - 权衡后选择保持画面干净 —— 这是**有意的取舍**,不是做不到。 - -## 真机怎么做 - -1. 装 `dist/S5ePAnim.face` -2. 表盘「快速帮助 → 环境预检查」(**只读**):确认 `/dev/health` 可读、卷名是 `health`、 - 119 条负载不越界(用 superblock 的**大端** size 判),并对首/中/尾三条做抽样版本校验 - (`原厂✓` / `已替换✓`) -3. 首页「开始替换」→ 确认页 → **确认替换** → 等 `成功 119 失败 0`(15.49 MB) -4. 「重启手表」→ 打开压力 App 开始检测,看整段动画 -5. 还原:装 `dist/S5ePAnimRestore.face` →「开始还原」→ 重启 - -「正在替换」页每 10 条打一行 `> 写入 n/119 成功 x 失败 y`,结束打 `> 共 119 条,成功 x 失败 y`。 -**判断成功的唯一标准是 `失败 0`**;若出现 `> 版本不符跳过 n 条`,说明设备上的 `/dev/health` -不是算偏移时用的那一版固件 —— **不要重启**,装对应版本的盘或先还原。 - -写入前每条都做版本校验:读设备上该偏移的前 64 B,必须等于「本盘要写的字节」或「另一侧的指纹」, -**两边都不是就判版本不符,并跳过 `/dev/health` 剩余记录**(绝不把字节写进别人的文件里)。 -写入后用**内容比对**判成败(`os.execute` 的返回值在这台机器上不可靠),失败自动重试 3 次, -仍不过就退化成「整条一次 dd」。 - -### 为什么写入要分段(S4e 真机的教训) - -119 个槽位**全部 512 对齐**,但 **97/119 条的长度不是 4 的倍数**(RLE 流长度随机)。 -若按长度取块大小就只能退化成 `bs=1` —— 一条 19.6 万字节的记录要 ~40 万次单字节读写, -真机上慢到偶发失败。本项目按「`bs=512` 大块 + `bs=4` 次块 + 最后 1~3 字节 `bs=1`」切成 ≤3 段, -每条最多 3 个 dd 调用。 - -## 界面(480×480) - -设计稿只有一处:`tools/s5e_pages.py`(`main.lua` 用的就是同一套坐标), -用真 MiSans 渲染到 `docs/ui_*.png`,并用**真实渲染像素**判圆屏切边(`tools/s5e_check_clip.py`)。 - -圆屏的硬约束:**在高度 y 处,屏幕的弦半宽 = √(240² − (y−240)²)**,越靠下越窄。所以: - -* 列表底板 `radius=40` 不是审美:y=88 那行弦半宽只有 185.8,圆角不够时四个角会越过圆边 -* 型号那一行拆成两行(`Xiaomi Watch S5` / `eSIM 46mm(p62lte)`):整串 25px 真 MiSans 量得 - 367px,而 y=243 处弦半宽只有 ~190px,放一行必被圆边切掉 -* 说明文字每行都控制在 348px 以内(25px 下约 13 个中文字),否则折行后第 2 行会被圆边切 -* 多色文本**不能**用 LVGL recolor(会被原样显示成 `#ffffff…`),一律拆成多个标签 + 手工算 x - -实测:7 个页面越界像素 **0**(最远点 238.4 / 半径 240),标签溢框 0。 - -## 重建 - -```powershell -$env:PYTHONIOENCODING="utf-8" # Windows 上必须,否则中文输出会炸 -# 需要 Python 3 + Pillow (+ numpy) -python tools\s5e_anim.py all # 100 帧 -> 119 个等长槽位 + 改后整分区镜像(几分钟,几百 MB) -python tools\s5e_lua_static.py # main.lua 静态体检(块配平 / 先用后声明 / 模板约定) - -python tools\s5e_payload.py replace # 负载 + 指纹 -> payload/ + blob_replace/ -python tools\s5e_overlay.py replace # 480x480 overlay(首页真渲染 -> 326x326 缩略图) -python tools\s5e_build.py replace # 两遍构建 -> dist\S5ePAnim.face(0 漂移校验) -python tools\s5e_verify.py replace # 逐盘校验(38 项) - -python tools\s5e_payload.py restore -python tools\s5e_overlay.py restore -python tools\s5e_build.py restore # -> dist\S5ePAnimRestore.face -python tools\s5e_verify.py restore - -python tools\s5e_verify_indep.py # 独立总校验(70 项,不读构建中间产物) -``` - -界面相关(改了 `tools/s5e_pages.py` / `main.lua` 之后): - -```powershell -python tools\s5e_ui_images.py # assets/ -> ui_images/*.bin(8 张 LVGL v9 素材) -python tools\s5e_render_ui.py # 480x480 预览 -> docs\ui_*.png + docs\ui_all.png(会报溢框) -python tools\s5e_check_clip.py # 真实渲染像素判圆屏切边(0 越界才算过) -``` - -### 需要自己准备的输入(放进 `inputs/`,不进仓库) - -| 路径 | 是什么 | 大小 | -|---|---|---| -| `inputs/target/vela_health.bin` | 目标固件(p62lte)health 分区 | 206,924,800 | -| `inputs/target/vela_app.bin` | 目标固件 app 分区(可选) | 103,043,072 | -| `inputs/target/vela_font.bin` | 目标固件 font 分区(可选:渲染预览时从里面取 MiSans) | 43,379,712 | -| `inputs/source/vela_app.bin` | 素材来源(S5 41mm q63)app 分区 | 206,000,128 | -| `inputs/fonts/MiSans-*.ttf` | 可选;不提供就自动从 `inputs/target/vela_font.bin` 里取 | — | -| `vendor/LuaDevTemplate-main/` | 官方模板(里面有 `Compiler.exe`) | — | - -固件分区镜像就是官方 OTA 包(ZIP)里的 `vela_*.bin` 条目,解压出来即可。 -**不要设 `ICON_EDIT_VENDOR`** —— 每个项目自带 `vendor/LuaDevTemplate-main/`。 - -## 自证输出(全部真实执行过,原始输出在 `docs/selfcheck_run.txt` / `docs/selfcheck_report.txt`) - -**① 119 条负载长度逐条 == 原槽位长度** - -``` -OK 每条负载长度 == 原槽位长度(119 条) <- s5e_verify.py(两盘各一次) -OK 第 k 条负载的目标偏移 == Measuring{k}.bin 的偏移(0 处不符) <- s5e_verify_indep.py:名字也对得上 -OK 119 个目标偏移全部 512 对齐 - (97/119 条长度不是 4 的倍数 -> Lua 侧必须走 bs=512+4+1 分段写,否则退化成 bs=1) -``` - -**② 每条负载解出来都是 464×464,且 RLE 恰好 `usize`** - -``` -OK 每条负载都能按 LVGL RLE 解出正好 usize 字节、解成 464x464 -OK 每条都是 I8(0x0a) + 压缩 + 464x464 + stride 464(0 条不符) -OK 每条 RLE 解出**恰好** usize=216321 且 1024+464*464 之后正好 1 字节尾巴(0 条不符) -``` - -**③ 改后镜像的 inode 表 / superblock 逐位不变、槽位外 0 字节变化** - -``` -OK inode 表(路径/偏移/next/spec/size/cksum)5555 条逐位不变 -OK superblock 与卷名不变(size 206924736 / cksum b804503a / 'health') -OK 分区长度不变(206924800 B) -OK 除 119 个槽位外,其余 5275 个文件的字节**逐字节**不变(0 处不符) -OK 变化区间 0x2c18618..0x3b9d384 完全落在槽位区间 0x2c18600..0x3b9d385 内 -OK 119 个槽位全部被改写(未变的:无) -``` - -**④ `.face` 里 Lua 表反解出的偏移与镜像一致(0 漂移)+ 两张盘互证** - -``` -OK 两张表的偏移自洽:首条紧跟魔数,之后逐条紧邻(0 处不连续) -OK Lua 表里的 119 条偏移 == 文件基址 + 魔数长度 + 相对偏移 <- s5e_verify.py -偏移校验:全部吻合,0 漂移 <- s5e_build.py(两遍构建) -OK replace 的负载前 64 B == restore 的指纹;restore 的负载前 64 B == replace 的指纹 -OK 119/119 条负载在两张盘里都不一样 -``` - -**总账** - -``` -s5e_lua_static.py : 全部通过(块配平 202/202;无先用后声明;4 个占位符各 1 次) -s5e_verify.py replace : 38 项通过,0 项失败 -s5e_verify.py restore : 38 项通过,0 项失败 -s5e_verify_indep.py : 70 项通过,0 项失败 -s5e_check_clip.py : 7 个页面,越界 0(最远 238.4 / 半径 240) -``` - -## 构建期的两个坑(都在 `tools/s5e_build.py` 里修掉了) - -**① 交付物的 b5 曾经是 10(必须和固件自带表盘一样是 0)** -两遍构建走的是 vendor 的 `scripts/build_face.ps1`,它编完之后会调 -`scripts/internal/set_face_id.ps1`,里面有一句 `$bytes[5] = [byte]$faceIdSize`(= 10)—— -把 id 字符串长度写进了头里第 5 字节。现在最终产物取自**裸 `Compiler.exe -b`** 的输出 -(b5 由编译器给出,实测 0),只覆盖 40..49 = id;并断言「裸产物与 toolchain 产物只差 -b5 + id 这 8 个字节」,否则拒绝交付。 - -**② 第二遍曾经编的还是占位符版 Lua** -`main.lua` 是带 `__UIIMG__ / __PAY__ / __RB__ / __ROLE__` 的模板:第一遍编模板、量出资源偏移; -第二遍必须把**回填后的 Lua 写回编译工作区**再编。之前只把回填结果写到了 -`.work/overlay/...`(给人看的副本),编译工作区里还是模板 → 交付物里的 Lua 带着占位符, -表盘在设备上会直接"负载表为空"。现在第二遍前会 `write_lua(resolved)`。 - -## 已知边界 / 没做的部分 - -1. **真机未验证 S5 eSIM 的 `/dev/health` 可写**:同源平台的 S4 eSIM 上 dd 读写流程实测通过, - S5 eSIM 上只能先靠只读的「环境预检查」确认可读 + 偏移对版。整条 119 条的写入路径在本机 - **无法**端到端验证(没有设备),本机只验证了字节、长度、偏移、头、镜像完整性。 -2. **偏移锁固件版本**:换固件必须重跑 `s5e_anim.py all` → 4 步流水线。用错版本时表盘会判 - 「版本不符」并跳过 `/dev/health` **剩余**记录(`job.skipRest`),不会把字节写进别人的文件里。 -3. **开头 8 帧与结尾 14 帧的画质**:开头 8 帧只有 4~24 色(它们是"一个小点",槽位预算 - 4.5~17.5 KB);结尾 14 帧被预算逼到 32~192 色 + 0.70~0.90 缩放。这是等长覆盖的硬约束 - (464×464 的 I8 图光全黑背景就要 ~3.7 KB),不是编码 bug;要更大就得上「非等长写」, - 那会动 inode 表和后面所有文件的偏移(本项目刻意不做)。 -4. **`Compiler.exe` 的设备表里没有 480 档**:圆表且预览 326×326 的只有 362 / 462, - 所以用 `DeviceType=462` + 326×326 预览图。预览只是表盘列表里的缩略图, - 实际画面尺寸由 `main.lua` 与 `.fprj` 的 480 决定。 -5. **负载文件必须原样连续放在 resource.bin 里**,偏移才等于 `文件基址 + 魔数长度 + 相对偏移`; - `s5e_build.py` 会逐条用生成时的 sha256 校验,并做两遍 0 漂移检查。魔数长度是 **15 B** - (`S5P-PAYv1`),曾经写成 16 导致全部偏移差 1。 -6. **分区键名 ≠ 节点路径**:Lua 表里存 `'health'`,运行期由 `DEVS['health']` 取到 `/dev/health`。 - 把 `/dev/health` 直接存进表里,拼出来就是 `if=nil`。`s5e_verify.py` / `s5e_lua_static.py` - 都专门检查这一条。 -7. **没动的**:inode 表、目录项、superblock、任何非 `pressure/measure/Measuring*.bin` 的文件; - 仓库里其它项目(`s4e-*`、`s5e-icons`、`s5e-charge`)与 git(未 commit、未改 `.gitignore`)。 - -## 第三方与风险 - -* 本仓库不含固件、OTA 包、字体;`.face` 由第三方 `Compiler.exe` 编译 -* 往分区节点写数据是危险操作:写错偏移可能损坏资源分区。表盘自带版本校验与还原盘, - 但请自行确认设备所有权并承担后果 -* 官方 OTA 升级会整分区覆盖,动画会回到原厂(等于自动还原) - -## 目录 - -``` -main.lua 表盘源码(含 __UIIMG__ / __PAY__ / __RB__ / __ROLE__ 占位符,构建时回填) -tools/ 全部脚本(见下表) -ui_images/ 界面素材(LVGL v9 .bin,8 张,480 档尺寸) -assets/ 设计素材 PNG(Mdi 图标、返回箭头、QQ、封面) -dist/ 交付的 .face -docs/ 界面预览 + selfcheck_run.txt / selfcheck_report.txt -inputs/ 固件分区镜像 + 字体(不进仓库,见上) -vendor/ LuaDevTemplate-main(含 Compiler.exe,不进仓库) -.work/ 构建工作区(anim/ 改后镜像、overlay/、build/;可删) -payload/ payload_restore/ 逐条目负载 + payload.lua(人肉 dd 用) -blob_replace/ blob_restore/ 魔数 + 顺序拼接的 payload.bin / fp.bin / offsets.json -``` - -| 脚本 | 作用 | -|---|---| -| `tools/paths.py` | 所有路径的唯一入口(本项目事实:id / 480 / 464 / 119 都在这) | -| `tools/romfs.py` | ROMFS 分区解析(只读) | -| `tools/lvgl_bin.py` | LVGL v9 RLE 编解码 + `pad_rle_safe` | -| `tools/iconpack.py` | ARGB8888 / I8 图片编码(界面素材用) | -| `tools/lua_balance.py` | Lua 块配平(被 `s5e_lua_static.py` 调用) | -| `tools/s5e_anim.py` | **核心**:100 帧 → 119 个等长槽位 + 改后整分区镜像 + `manifest.json`(含三条就地自证) | -| `tools/s5e_payload.py` | 按角色切负载 + 指纹表 + `blob_*/`(含逐条 sha256) | -| `tools/s5e_overlay.py` | 生成可编译的 overlay(config / fprj / 预览图 / 资源) | -| `tools/s5e_build.py` | **两遍构建 + 裸编忠实产物(b5=0)+ 0 漂移校验** → `dist/*.face` | -| `tools/s5e_verify.py` | 逐盘校验(38 项):头 / Lua / 偏移 / 逐条负载 / 指纹 / 镜像 / UI / 体积 | -| `tools/s5e_verify_indep.py` | 独立总校验(70 项):不读构建中间产物的那一路 + 两张盘互证 | -| `tools/s5e_lua_static.py` | `main.lua` 静态体检(含 S4e 踩过的「先调用后声明」坑) | -| `tools/s5e_pages.py` | 480×480 页面规格(唯一的设计稿来源) | -| `tools/s5e_render_ui.py` | 用真 MiSans 渲染 7 页预览(会报标签溢框) | -| `tools/s5e_check_clip.py` | 用真实渲染像素判圆屏是否切边 | -| `tools/s5e_ui_images.py` | 设计素材 PNG → `ui_images/*.bin` + 圆形 logo | diff --git a/s5e-pressure-anim/assets/MdiBandage.png b/s5e-pressure-anim/assets/MdiBandage.png deleted file mode 100644 index 26abc5b..0000000 Binary files a/s5e-pressure-anim/assets/MdiBandage.png and /dev/null differ diff --git a/s5e-pressure-anim/assets/MdiClipboardTextSearchOutline.png b/s5e-pressure-anim/assets/MdiClipboardTextSearchOutline.png deleted file mode 100644 index 85ea2da..0000000 Binary files a/s5e-pressure-anim/assets/MdiClipboardTextSearchOutline.png and /dev/null differ diff --git a/s5e-pressure-anim/assets/MdiInformationOutline.png b/s5e-pressure-anim/assets/MdiInformationOutline.png deleted file mode 100644 index 3af2a29..0000000 Binary files a/s5e-pressure-anim/assets/MdiInformationOutline.png and /dev/null differ diff --git a/s5e-pressure-anim/assets/MdiLightbulbOutline.png b/s5e-pressure-anim/assets/MdiLightbulbOutline.png deleted file mode 100644 index 7d7819c..0000000 Binary files a/s5e-pressure-anim/assets/MdiLightbulbOutline.png and /dev/null differ diff --git a/s5e-pressure-anim/assets/MdiReplyAllOutline.png b/s5e-pressure-anim/assets/MdiReplyAllOutline.png deleted file mode 100644 index f419de0..0000000 Binary files a/s5e-pressure-anim/assets/MdiReplyAllOutline.png and /dev/null differ diff --git a/s5e-pressure-anim/assets/backarrow.png b/s5e-pressure-anim/assets/backarrow.png deleted file mode 100644 index 9e55859..0000000 Binary files a/s5e-pressure-anim/assets/backarrow.png and /dev/null differ diff --git a/s5e-pressure-anim/assets/cover.png b/s5e-pressure-anim/assets/cover.png deleted file mode 100644 index d3a9a70..0000000 Binary files a/s5e-pressure-anim/assets/cover.png and /dev/null differ diff --git a/s5e-pressure-anim/assets/qq.png b/s5e-pressure-anim/assets/qq.png deleted file mode 100644 index 3982762..0000000 Binary files a/s5e-pressure-anim/assets/qq.png and /dev/null differ diff --git a/s5e-pressure-anim/dist/S5e-Pt2.face b/s5e-pressure-anim/dist/S5e-Pt2.face deleted file mode 100644 index d07eff8..0000000 Binary files a/s5e-pressure-anim/dist/S5e-Pt2.face and /dev/null differ diff --git a/s5e-pressure-anim/dist/S5ePAnimRestore.face b/s5e-pressure-anim/dist/S5ePAnimRestore.face deleted file mode 100644 index d8fb22d..0000000 Binary files a/s5e-pressure-anim/dist/S5ePAnimRestore.face and /dev/null differ diff --git a/s5e-pressure-anim/docs/logo_preview_480.png b/s5e-pressure-anim/docs/logo_preview_480.png deleted file mode 100644 index 2143fc9..0000000 Binary files a/s5e-pressure-anim/docs/logo_preview_480.png and /dev/null differ diff --git a/s5e-pressure-anim/docs/selfcheck_report.txt b/s5e-pressure-anim/docs/selfcheck_report.txt deleted file mode 100644 index 1def5c8..0000000 --- a/s5e-pressure-anim/docs/selfcheck_report.txt +++ /dev/null @@ -1,298 +0,0 @@ -# S5ePAnim 交付前自证(全部只读;UTF-8) -# 生成时间 2026-10-02 22:34:54 -# python C:\Users\hi\.dsh\dsh-runtimes\dsh-primary-runtime\dependencies\python\python.exe - -============================================================================== -$ python tools/s5e_lua_static.py -============================================================================== - -ALL OK -=== tools/s5e_lua_static.py D:\UserData\Documents\GitHub\o62lte-customising-plan\s5e-pressure-anim\main.lua (31141 B) === - -[1] 块配平 - OK openers=202 end=202(function=90 if=99 for=11 while=1 do=1)repeat=0 until=0 - -[2] 先用后声明(真机上会变成 "call a nil value") - OK 没有"先调用后 local": - OK 没有未声明的被调用名: - -[3] 模板约定(s5e_verify.py 靠这些从 .face 里反解 Lua) - OK 占位符 __UIIMG__ 出现 1 次(要 1) - OK 占位符 __PAY__ 出现 1 次(要 1) - OK 占位符 __RB__ 出现 1 次(要 1) - OK 占位符 __ROLE__ 出现 1 次(要 1) - OK 文件以 --[[-- 开头 - OK --[[ 只出现 1 次(要 1,否则 verify 会从中间截 Lua) - OK 以 return ui\n 结尾 - OK return ui\n 只出现 1 次(要 1) - OK 有 local UIIMG = { - -[4] 与 s5e_verify.py 对齐的字面量 - OK local ROLE = '__ROLE__' - OK local W, H = 480, 480 - OK DEVS 里有 health -> /dev/health:health = "/dev/health" - OK DEV 来自 DEVS.health(表里的键名真的被用于取节点,不是摆设) - OK PAYTBL 里只有 __PAY__ / __RB__ 两处占位符(没有硬编码节点名) - OK 没有残留的 466(旧 S4e 尺寸) - -=== 全部通过 === -EXIT=0 - -============================================================================== -$ python tools/s5e_verify.py replace -============================================================================== -============================================================================== -S5ePAnim(role=replace) D:\UserData\Documents\GitHub\o62lte-customising-plan\s5e-pressure-anim\dist\S5ePAnim.face -============================================================================== - -[1] .face 头 - OK magic 5a a5 34 12 - OK b4 = 0 - OK b5 = 0(固件自带表盘 b5 也是 0) - OK id = b'462150102\x00'(期望 '462150102') - OK preview 326x326(Compiler.exe 对 462 设备强制 326x326) - OK preview 数据 77538 B @0xf9d638(文件尾 0xfb051c) - 文件 16450844 B sha256 d0bb012a364fd46eb4bb27db26ead72ae3166060c72c08ec5a76670dc06a3b1c - -[2] 从 .face 里反解加载的 Lua - OK 找到 Lua 结尾 "return ui\n" - OK Lua 提取成功(37900 B) - OK Lua 原样出现 1 次(偏移 0xf94220) - OK Lua 里的 ROLE == 'replace' - OK 占位符已全部回填 - OK 界面尺寸 W,H = 480,480(480x480 圆屏) - OK DEVS 里有 health -> /dev/health:health = "/dev/health" - -[3] 资源偏移(独立在 .face 里搜字节,不信 Lua 表) - OK payload 魔数(15 B)在 .face 里唯一 - OK fp 魔数(14 B)在 .face 里唯一 - OK payload.bin 原样连续存放在 0xb16a - OK fp.bin 原样连续存放在 0x205 - OK 负载记录 119 条(期望 119) - OK payload.bin 16238747 B 在 .face 里逐字节连续 - OK fp.bin 7630 B 在 .face 里逐字节连续 - OK Lua 表 pay 119 条 / rb 119 条 - OK Lua 表里的 119 条偏移 == 文件基址 + 魔数长度 + 相对偏移 - OK 原厂 pressure/measure/ 下 119 个槽位 - OK 原厂 usize 唯一且 == 1024 + 464*464 + 1 = 216321 - -[4] 逐条负载 - OK 每条负载长度 == 原槽位长度(119 条) - OK 每条负载字节 == 改后镜像对应位置 - OK 每条负载都能按 LVGL RLE 解出正好 usize 字节、解成 464x464 - -[5] 版本校验指纹 - OK 119 条指纹都是 64 B 且 == 原厂镜像前 64 B - OK 119/119 条的"原厂前 64 B"与"改后前 64 B"确实不同(指纹有区分度) - -[6] 改后镜像的完整性 - OK inode 表(路径/偏移/next/spec/size/cksum)5555 条逐位不变 - OK superblock 一致(size 206924736 / cksum b804503a) - OK 卷名 health 不变 - OK 分区长度不变(206924800 B) - OK 改动只落在负载槽位区间内(首差 0x2c18618,末差 0x3b9d384,槽位区间 0x2c18600..0x3b9d385) - OK 119 个槽位全部都真的写入了新字节(未变的槽位:无) - -[7] UI 图片 - OK UIIMG 条目 8 条(期望 8) - OK 8 张界面素材都与 ui_images/*.bin 逐字节一致 - -[8] 体积 - OK 交付物 15.69 MB < 20 MB - 两张盘同 id:装一份就顶掉另一份,设备上只占一份空间 - -============================================================================== -S5ePAnim:38 项通过,0 项失败 -全部通过 —— S5ePAnim.face(16450844 B,sha256 d0bb012a364fd46e) -EXIT=0 - -============================================================================== -$ python tools/s5e_verify.py restore -============================================================================== -============================================================================== -S5ePAnimRestore(role=restore) D:\UserData\Documents\GitHub\o62lte-customising-plan\s5e-pressure-anim\dist\S5ePAnimRestore.face -============================================================================== - -[1] .face 头 - OK magic 5a a5 34 12 - OK b4 = 0 - OK b5 = 0(固件自带表盘 b5 也是 0) - OK id = b'462150102\x00'(期望 '462150102') - OK preview 326x326(Compiler.exe 对 462 设备强制 326x326) - OK preview 数据 77538 B @0xf9d638(文件尾 0xfb051c) - 文件 16450844 B sha256 13cafae4bf2b03db82f2c17f9d3af7c7e8c01d0c26275feda301753a76ebda69 - -[2] 从 .face 里反解加载的 Lua - OK 找到 Lua 结尾 "return ui\n" - OK Lua 提取成功(37900 B) - OK Lua 原样出现 1 次(偏移 0xf94220) - OK Lua 里的 ROLE == 'restore' - OK 占位符已全部回填 - OK 界面尺寸 W,H = 480,480(480x480 圆屏) - OK DEVS 里有 health -> /dev/health:health = "/dev/health" - -[3] 资源偏移(独立在 .face 里搜字节,不信 Lua 表) - OK payload 魔数(15 B)在 .face 里唯一 - OK fp 魔数(14 B)在 .face 里唯一 - OK payload.bin 原样连续存放在 0xb16a - OK fp.bin 原样连续存放在 0x205 - OK 负载记录 119 条(期望 119) - OK payload.bin 16238747 B 在 .face 里逐字节连续 - OK fp.bin 7630 B 在 .face 里逐字节连续 - OK Lua 表 pay 119 条 / rb 119 条 - OK Lua 表里的 119 条偏移 == 文件基址 + 魔数长度 + 相对偏移 - OK 原厂 pressure/measure/ 下 119 个槽位 - OK 原厂 usize 唯一且 == 1024 + 464*464 + 1 = 216321 - -[4] 逐条负载 - OK 每条负载长度 == 原槽位长度(119 条) - OK 每条负载字节 == 原厂镜像对应位置 - OK 每条负载都能按 LVGL RLE 解出正好 usize 字节、解成 464x464 - -[5] 版本校验指纹 - OK 119 条指纹都是 64 B 且 == 改后镜像前 64 B - OK 119/119 条的"原厂前 64 B"与"改后前 64 B"确实不同(指纹有区分度) - -[6] 改后镜像的完整性 - OK inode 表(路径/偏移/next/spec/size/cksum)5555 条逐位不变 - OK superblock 一致(size 206924736 / cksum b804503a) - OK 卷名 health 不变 - OK 分区长度不变(206924800 B) - OK 改动只落在负载槽位区间内(首差 0x2c18618,末差 0x3b9d384,槽位区间 0x2c18600..0x3b9d385) - OK 119 个槽位全部都真的写入了新字节(未变的槽位:无) - -[7] UI 图片 - OK UIIMG 条目 8 条(期望 8) - OK 8 张界面素材都与 ui_images/*.bin 逐字节一致 - -[8] 体积 - OK 交付物 15.69 MB < 20 MB - 两张盘同 id:装一份就顶掉另一份,设备上只占一份空间 - -============================================================================== -S5ePAnimRestore:38 项通过,0 项失败 -全部通过 —— S5ePAnimRestore.face(16450844 B,sha256 13cafae4bf2b03db) -EXIT=0 - -============================================================================== -$ python tools/s5e_verify_indep.py -============================================================================== -============================================================================== -独立总校验(不读 blob_*/offsets.json、不读 .work/overlay、不读 .work/build) - 交付物 : dist/S5ePAnim.face + dist/S5ePAnimRestore.face - 固件 : inputs/target/vela_health.bin 206924800 B(卷名 health) - 改后 : .work/anim/vela_health_anim.bin 206924800 B -============================================================================== - OK 固件里 pressure/measure/ 下 119 个槽位(期望 119) - OK 槽位名字就是 Measuring1..119.bin - ------------------------------------------------------------------------------- -[replace] S5ePAnim.face 16450844 B sha256 d0bb012a364fd46eb4bb27db26ead72ae3166060c72c08ec5a76670dc06a3b1c ------------------------------------------------------------------------------- - OK magic - OK b5 = 0(固件自带表盘是 0) - OK b4 = 0 - OK id = b'462150102\x00' - OK preview 326x326 - OK Lua 在文件里只出现 1 次(偏移 0xf94220) - OK Lua 里的 ROLE == 'replace' - OK 占位符 __UIIMG__ 已回填 - OK 占位符 __PAY__ 已回填 - OK 占位符 __RB__ 已回填 - OK 占位符 __ROLE__ 已回填 - OK 界面尺寸 W,H = 480,480 - OK DEVS 有 health -> /dev/health - OK UIIMG 8 条(期望 8) - OK pay 119 条 / rb 119 条(期望 119/119) - OK 两个魔数各命中 1 次(payload 0xb16a / fp 0x205) - OK 两张表的偏移自洽:首条紧跟魔数,之后逐条紧邻(0 处不连续) - OK rb 每条都是 64 B - OK 第 k 条负载的目标偏移 == Measuring{k}.bin 的偏移(0 处不符) - OK 每条长度 == 该槽位原厂长度,且 rb 的 target 与 pay 一致(0 处不符) - OK 119 个目标偏移全部 512 对齐 - (97/119 条长度不是 4 的倍数 -> Lua 侧必须走 bs=512+4+1 分段写,否则退化成 bs=1) - OK 每条负载字节 == 改后镜像对应位置(0 处不符) - OK 每条负载都真的与"另一侧"不同(0 条与另一侧相同 —— 应当是 0) - OK 每条都是 I8(0x0a) + 压缩 + 464x464 + stride 464(0 条不符) - OK 每条 RLE 解出**恰好** usize=216321 且 1024+464*464 之后正好 1 字节尾巴(0 条不符) - OK 119/119 条的"原厂前 64 B"与"改后前 64 B"确实不同 - OK rb 的 target 也指向对应槽位 - OK 8 张界面素材与 ui_images/*.bin 逐字节一致(0 处不符) - ------------------------------------------------------------------------------- -[restore] S5ePAnimRestore.face 16450844 B sha256 13cafae4bf2b03db82f2c17f9d3af7c7e8c01d0c26275feda301753a76ebda69 ------------------------------------------------------------------------------- - OK magic - OK b5 = 0(固件自带表盘是 0) - OK b4 = 0 - OK id = b'462150102\x00' - OK preview 326x326 - OK Lua 在文件里只出现 1 次(偏移 0xf94220) - OK Lua 里的 ROLE == 'restore' - OK 占位符 __UIIMG__ 已回填 - OK 占位符 __PAY__ 已回填 - OK 占位符 __RB__ 已回填 - OK 占位符 __ROLE__ 已回填 - OK 界面尺寸 W,H = 480,480 - OK DEVS 有 health -> /dev/health - OK UIIMG 8 条(期望 8) - OK pay 119 条 / rb 119 条(期望 119/119) - OK 两个魔数各命中 1 次(payload 0xb16a / fp 0x205) - OK 两张表的偏移自洽:首条紧跟魔数,之后逐条紧邻(0 处不连续) - OK rb 每条都是 64 B - OK 第 k 条负载的目标偏移 == Measuring{k}.bin 的偏移(0 处不符) - OK 每条长度 == 该槽位原厂长度,且 rb 的 target 与 pay 一致(0 处不符) - OK 119 个目标偏移全部 512 对齐 - (97/119 条长度不是 4 的倍数 -> Lua 侧必须走 bs=512+4+1 分段写,否则退化成 bs=1) - OK 每条负载字节 == 原厂镜像对应位置(0 处不符) - OK 每条负载都真的与"另一侧"不同(0 条与另一侧相同 —— 应当是 0) - OK 每条都是 I8(0x0a) + 压缩 + 464x464 + stride 464(0 条不符) - OK 每条 RLE 解出**恰好** usize=216321 且 1024+464*464 之后正好 1 字节尾巴(0 条不符) - OK 119/119 条的"原厂前 64 B"与"改后前 64 B"确实不同 - OK rb 的 target 也指向对应槽位 - OK 8 张界面素材与 ui_images/*.bin 逐字节一致(0 处不符) - -============================================================================== -[互证] 两张交付物互相印证(完全不碰 .work / 固件镜像) -============================================================================== - OK 两张盘字节数相同(16450844) - OK 头 50 B(magic / b4 / b5 / id)逐字节相同 - OK replace 的负载前 64 B == restore 的指纹;restore 的负载前 64 B == replace 的指纹(0 处不符) - OK 两张盘的 8 张界面素材逐字节相同(同一套界面) - OK 两张盘的资源基址相同(0xb16a / 0x205) - OK 119/119 条负载在两张盘里都不一样(119) - -============================================================================== -[镜像] 改后镜像 vs 原厂镜像 -============================================================================== - OK inode 表(路径/偏移/next/spec/size/cksum)5555 条逐位不变 - OK superblock 与卷名不变(size 206924736 / cksum b804503a / 'health') - OK 分区长度不变(206924800 B) - OK 除 119 个槽位外,其余 5275 个文件的字节**逐字节**不变(0 处不符) - OK 变化区间 0x2c18618..0x3b9d384 完全落在槽位区间 0x2c18600..0x3b9d385 内 - OK 119 个槽位全部被改写(未变的:无) - -============================================================================== -独立总校验:70 项通过,0 项失败 -============================================================================== -EXIT=0 - -============================================================================== -$ python tools/s5e_check_clip.py -============================================================================== -page max r outside px worst point --------------------------------------------------------------- -home 230.7 0 (275, 12) -help 230.7 0 (275, 12) -help_ok 230.7 0 (275, 12) -help_err 230.7 0 (275, 12) -confirm 238.4 0 (133, 453) -running 238.4 0 (133, 453) -about 232.3 0 (61, 92) --------------------------------------------------------------- -圆心 (240,240) 半径 240 画布 480x480 越界页数:0 -EXIT=0 - -============================================================================== -ALL_STEPS_OK=True(5 步,0 步失败) -============================================================================== diff --git a/s5e-pressure-anim/docs/selfcheck_run.txt b/s5e-pressure-anim/docs/selfcheck_run.txt deleted file mode 100644 index 9910a48..0000000 --- a/s5e-pressure-anim/docs/selfcheck_run.txt +++ /dev/null @@ -1,347 +0,0 @@ -਍# S5ePAnim 重建 + 自证 原始输出(2026-10-02 22:24:49) -# python: C:\Users\hi\.dsh\dsh-runtimes\dsh-primary-runtime\dependencies\python\python.exe - -=== python tools\s5e_lua_static.py === - -ALL OK -=== tools/s5e_lua_static.py D:\UserData\Documents\GitHub\o62lte-customising-plan\s5e-pressure-anim\main.lua (31141 B) === - -[1] 块配平 - OK openers=202 end=202(function=90 if=99 for=11 while=1 do=1)repeat=0 until=0 - -[2] 先用后声明(真机上会变成 "call a nil value") - OK 没有"先调用后 local": - OK 没有未声明的被调用名: - -[3] 模板约定(s5e_verify.py 靠这些从 .face 里反解 Lua) - OK 占位符 __UIIMG__ 出现 1 次(要 1) - OK 占位符 __PAY__ 出现 1 次(要 1) - OK 占位符 __RB__ 出现 1 次(要 1) - OK 占位符 __ROLE__ 出现 1 次(要 1) - OK 文件以 --[[-- 开头 - OK --[[ 只出现 1 次(要 1,否则 verify 会从中间截 Lua) - OK 以 return ui\n 结尾 - OK return ui\n 只出现 1 次(要 1) - OK 有 local UIIMG = { - -[4] 与 s5e_verify.py 对齐的字面量 - OK local ROLE = '__ROLE__' - OK local W, H = 480, 480 - OK DEVS 里有 health -> /dev/health:health = "/dev/health" - OK DEV 来自 DEVS.health(表里的键名真的被用于取节点,不是摆设) - OK PAYTBL 里只有 __PAY__ / __RB__ 两处占位符(没有硬编码节点名) - OK 没有残留的 466(旧 S4e 尺寸) - -=== 全部通过 === -EXIT=0 - -=== python tools\s5e_payload.py replace === -blob_replace/ 单条目: payload.bin 16238747 B + fp.bin 7630 B(119 条记录,跳过 0 条未变) -role=replace: emitted 119 records, 15.49 MB; dropped 0 -EXIT=0 - -=== python tools\s5e_overlay.py replace === -overlay written to D:\UserData\Documents\GitHub\o62lte-customising-plan\s5e-pressure-anim\.work\overlay\S5ePAnim -EXIT=0 - -=== python tools\s5e_build.py replace === -=== S5ePAnim(role=replace,id=462150102)=== -第 1 遍:用占位符编一次,量出资源在 .face 里的位置 - | ------------------------------------------------------------------------------- - | Patched face ID: 462150102 - | Done: D:\UserData\Documents\GitHub\o62lte-customising-plan\s5e-pressure-anim\.work\build\S5ePAnim\bin\S5ePAnim.face - | Generated: D:\UserData\Documents\GitHub\o62lte-customising-plan\s5e-pressure-anim\.work\build\S5ePAnim\watchface\data\resource.bin - face 16444088 bytes - back off=0xf87a2e len=2100 - ic_help off=0xf8a105 len=3856 - ic_replace off=0xf8bf7c len=3856 - ic_about off=0xf8828e len=3856 - ic_check off=0xf891ca len=3856 - ic_rollback off=0xf8ceb9 len=3856 - ic_qq off=0xf8b03f len=3856 - logo off=0xf8ddf2 len=25612 - payload base=0x00b16a(16238747 B) / fp base=0x000205(7630 B),共 119 条记录 -第 2 遍:回填后的 Lua(32814 bytes)重新编译 - | ------------------------------------------------------------------------------- - | Patched face ID: 462150102 - | Done: D:\UserData\Documents\GitHub\o62lte-customising-plan\s5e-pressure-anim\.work\build\S5ePAnim\bin\S5ePAnim.face - | Generated: D:\UserData\Documents\GitHub\o62lte-customising-plan\s5e-pressure-anim\.work\build\S5ePAnim\watchface\data\resource.bin -偏移校验:全部吻合,0 漂移 -忠实产物 vs toolchain:8 字节差异(只允许 b5 + id 这 8 个位置:[5, 40, 42, 43, 44, 46, 47, 48]) -b4=0 b5=0 id=b'462150102\x00' -preview @0xf9d62c: 326x326 palette=1024 data=77538 -交付物:D:\UserData\Documents\GitHub\o62lte-customising-plan\s5e-pressure-anim\dist\S5ePAnim.face 16450844 B sha256 d0bb012a364fd46eb4bb27db26ead72ae3166060c72c08ec5a76670dc06a3b1c -EXIT=0 - -=== python tools\s5e_verify.py replace === -============================================================================== -S5ePAnim(role=replace) D:\UserData\Documents\GitHub\o62lte-customising-plan\s5e-pressure-anim\dist\S5ePAnim.face -============================================================================== - -[1] .face 头 - OK magic 5a a5 34 12 - OK b4 = 0 - OK b5 = 0(固件自带表盘 b5 也是 0) - OK id = b'462150102\x00'(期望 '462150102') - OK preview 326x326(Compiler.exe 对 462 设备强制 326x326) - OK preview 数据 77538 B @0xf9d638(文件尾 0xfb051c) - 文件 16450844 B sha256 d0bb012a364fd46eb4bb27db26ead72ae3166060c72c08ec5a76670dc06a3b1c - -[2] 从 .face 里反解加载的 Lua - OK 找到 Lua 结尾 "return ui\n" - OK Lua 提取成功(37900 B) - OK Lua 原样出现 1 次(偏移 0xf94220) - OK Lua 里的 ROLE == 'replace' - OK 占位符已全部回填 - OK 界面尺寸 W,H = 480,480(480x480 圆屏) - OK DEVS 里有 health -> /dev/health:health = "/dev/health" - -[3] 资源偏移(独立在 .face 里搜字节,不信 Lua 表) - OK payload 魔数(15 B)在 .face 里唯一 - OK fp 魔数(14 B)在 .face 里唯一 - OK payload.bin 原样连续存放在 0xb16a - OK fp.bin 原样连续存放在 0x205 - OK 负载记录 119 条(期望 119) - OK payload.bin 16238747 B 在 .face 里逐字节连续 - OK fp.bin 7630 B 在 .face 里逐字节连续 - OK Lua 表 pay 119 条 / rb 119 条 - OK Lua 表里的 119 条偏移 == 文件基址 + 魔数长度 + 相对偏移 - OK 原厂 pressure/measure/ 下 119 个槽位 - OK 原厂 usize 唯一且 == 1024 + 464*464 + 1 = 216321 - -[4] 逐条负载 - OK 每条负载长度 == 原槽位长度(119 条) - OK 每条负载字节 == 改后镜像对应位置 - OK 每条负载都能按 LVGL RLE 解出正好 usize 字节、解成 464x464 - -[5] 版本校验指纹 - OK 119 条指纹都是 64 B 且 == 原厂镜像前 64 B - OK 119/119 条的"原厂前 64 B"与"改后前 64 B"确实不同(指纹有区分度) - -[6] 改后镜像的完整性 - OK inode 表(路径/偏移/next/spec/size/cksum)5555 条逐位不变 - OK superblock 一致(size 206924736 / cksum b804503a) - OK 卷名 health 不变 - OK 分区长度不变(206924800 B) - OK 改动只落在负载槽位区间内(首差 0x2c18618,末差 0x3b9d384,槽位区间 0x2c18600..0x3b9d385) - OK 119 个槽位全部都真的写入了新字节(未变的槽位:无) - -[7] UI 图片 - OK UIIMG 条目 8 条(期望 8) - OK 8 张界面素材都与 ui_images/*.bin 逐字节一致 - -[8] 体积 - OK 交付物 15.69 MB < 20 MB - 两张盘同 id:装一份就顶掉另一份,设备上只占一份空间 - -============================================================================== -S5ePAnim:38 项通过,0 项失败 -全部通过 —— S5ePAnim.face(16450844 B,sha256 d0bb012a364fd46e) -EXIT=0 - -=== python tools\s5e_payload.py restore === -blob_restore/ 单条目: payload.bin 16238747 B + fp.bin 7630 B(119 条记录,跳过 0 条未变) -role=restore: emitted 119 records, 15.49 MB; dropped 0 -EXIT=0 - -=== python tools\s5e_overlay.py restore === -overlay written to D:\UserData\Documents\GitHub\o62lte-customising-plan\s5e-pressure-anim\.work\overlay\S5ePAnimRestore -EXIT=0 - -=== python tools\s5e_build.py restore === -=== S5ePAnimRestore(role=restore,id=462150102)=== -第 1 遍:用占位符编一次,量出资源在 .face 里的位置 - | ------------------------------------------------------------------------------- - | Patched face ID: 462150102 - | Done: D:\UserData\Documents\GitHub\o62lte-customising-plan\s5e-pressure-anim\.work\build\S5ePAnimRestore\bin\S5ePAnimRestore.face - | Generated: D:\UserData\Documents\GitHub\o62lte-customising-plan\s5e-pressure-anim\.work\build\S5ePAnimRestore\watchface\data\resource.bin - face 16444088 bytes - back off=0xf87a2e len=2100 - ic_help off=0xf8a105 len=3856 - ic_replace off=0xf8bf7c len=3856 - ic_about off=0xf8828e len=3856 - ic_check off=0xf891ca len=3856 - ic_rollback off=0xf8ceb9 len=3856 - ic_qq off=0xf8b03f len=3856 - logo off=0xf8ddf2 len=25612 - payload base=0x00b16a(16238747 B) / fp base=0x000205(7630 B),共 119 条记录 -第 2 遍:回填后的 Lua(32814 bytes)重新编译 - | ------------------------------------------------------------------------------- - | Patched face ID: 462150102 - | Done: D:\UserData\Documents\GitHub\o62lte-customising-plan\s5e-pressure-anim\.work\build\S5ePAnimRestore\bin\S5ePAnimRestore.face - | Generated: D:\UserData\Documents\GitHub\o62lte-customising-plan\s5e-pressure-anim\.work\build\S5ePAnimRestore\watchface\data\resource.bin -偏移校验:全部吻合,0 漂移 -忠实产物 vs toolchain:8 字节差异(只允许 b5 + id 这 8 个位置:[5, 40, 42, 43, 44, 46, 47, 48]) -b4=0 b5=0 id=b'462150102\x00' -preview @0xf9d62c: 326x326 palette=1024 data=77538 -交付物:D:\UserData\Documents\GitHub\o62lte-customising-plan\s5e-pressure-anim\dist\S5ePAnimRestore.face 16450844 B sha256 13cafae4bf2b03db82f2c17f9d3af7c7e8c01d0c26275feda301753a76ebda69 -EXIT=0 - -=== python tools\s5e_verify.py restore === -============================================================================== -S5ePAnimRestore(role=restore) D:\UserData\Documents\GitHub\o62lte-customising-plan\s5e-pressure-anim\dist\S5ePAnimRestore.face -============================================================================== - -[1] .face 头 - OK magic 5a a5 34 12 - OK b4 = 0 - OK b5 = 0(固件自带表盘 b5 也是 0) - OK id = b'462150102\x00'(期望 '462150102') - OK preview 326x326(Compiler.exe 对 462 设备强制 326x326) - OK preview 数据 77538 B @0xf9d638(文件尾 0xfb051c) - 文件 16450844 B sha256 13cafae4bf2b03db82f2c17f9d3af7c7e8c01d0c26275feda301753a76ebda69 - -[2] 从 .face 里反解加载的 Lua - OK 找到 Lua 结尾 "return ui\n" - OK Lua 提取成功(37900 B) - OK Lua 原样出现 1 次(偏移 0xf94220) - OK Lua 里的 ROLE == 'restore' - OK 占位符已全部回填 - OK 界面尺寸 W,H = 480,480(480x480 圆屏) - OK DEVS 里有 health -> /dev/health:health = "/dev/health" - -[3] 资源偏移(独立在 .face 里搜字节,不信 Lua 表) - OK payload 魔数(15 B)在 .face 里唯一 - OK fp 魔数(14 B)在 .face 里唯一 - OK payload.bin 原样连续存放在 0xb16a - OK fp.bin 原样连续存放在 0x205 - OK 负载记录 119 条(期望 119) - OK payload.bin 16238747 B 在 .face 里逐字节连续 - OK fp.bin 7630 B 在 .face 里逐字节连续 - OK Lua 表 pay 119 条 / rb 119 条 - OK Lua 表里的 119 条偏移 == 文件基址 + 魔数长度 + 相对偏移 - OK 原厂 pressure/measure/ 下 119 个槽位 - OK 原厂 usize 唯一且 == 1024 + 464*464 + 1 = 216321 - -[4] 逐条负载 - OK 每条负载长度 == 原槽位长度(119 条) - OK 每条负载字节 == 原厂镜像对应位置 - OK 每条负载都能按 LVGL RLE 解出正好 usize 字节、解成 464x464 - -[5] 版本校验指纹 - OK 119 条指纹都是 64 B 且 == 改后镜像前 64 B - OK 119/119 条的"原厂前 64 B"与"改后前 64 B"确实不同(指纹有区分度) - -[6] 改后镜像的完整性 - OK inode 表(路径/偏移/next/spec/size/cksum)5555 条逐位不变 - OK superblock 一致(size 206924736 / cksum b804503a) - OK 卷名 health 不变 - OK 分区长度不变(206924800 B) - OK 改动只落在负载槽位区间内(首差 0x2c18618,末差 0x3b9d384,槽位区间 0x2c18600..0x3b9d385) - OK 119 个槽位全部都真的写入了新字节(未变的槽位:无) - -[7] UI 图片 - OK UIIMG 条目 8 条(期望 8) - OK 8 张界面素材都与 ui_images/*.bin 逐字节一致 - -[8] 体积 - OK 交付物 15.69 MB < 20 MB - 两张盘同 id:装一份就顶掉另一份,设备上只占一份空间 - -============================================================================== -S5ePAnimRestore:38 项通过,0 项失败 -全部通过 —— S5ePAnimRestore.face(16450844 B,sha256 13cafae4bf2b03db) -EXIT=0 - -=== python tools\s5e_verify_indep.py === -============================================================================== -独立总校验(不读 blob_*/offsets.json、不读 .work/overlay、不读 .work/build) - 交付物 : dist/S5ePAnim.face + dist/S5ePAnimRestore.face - 固件 : inputs/target/vela_health.bin 206924800 B(卷名 health) - 改后 : .work/anim/vela_health_anim.bin 206924800 B -============================================================================== - OK 固件里 pressure/measure/ 下 119 个槽位(期望 119) - OK 槽位名字就是 Measuring1..119.bin - ------------------------------------------------------------------------------- -[replace] S5ePAnim.face 16450844 B sha256 d0bb012a364fd46eb4bb27db26ead72ae3166060c72c08ec5a76670dc06a3b1c ------------------------------------------------------------------------------- - OK magic - OK b5 = 0(固件自带表盘是 0) - OK b4 = 0 - OK id = b'462150102\x00' - OK preview 326x326 - OK Lua 在文件里只出现 1 次(偏移 0xf94220) - OK Lua 里的 ROLE == 'replace' - OK 占位符 __UIIMG__ 已回填 - OK 占位符 __PAY__ 已回填 - OK 占位符 __RB__ 已回填 - OK 占位符 __ROLE__ 已回填 - OK 界面尺寸 W,H = 480,480 - OK DEVS 有 health -> /dev/health - OK UIIMG 8 条(期望 8) - OK pay 119 条 / rb 119 条(期望 119/119) - OK 两个魔数各命中 1 次(payload 0xb16a / fp 0x205) - OK 两张表的偏移自洽:首条紧跟魔数,之后逐条紧邻(0 处不连续) - OK rb 每条都是 64 B - OK 第 k 条负载的目标偏移 == Measuring{k}.bin 的偏移(0 处不符) - OK 每条长度 == 该槽位原厂长度,且 rb 的 target 与 pay 一致(0 处不符) - OK 119 个目标偏移全部 512 对齐 - (97/119 条长度不是 4 的倍数 -> Lua 侧必须走 bs=512+4+1 分段写,否则退化成 bs=1) - OK 每条负载字节 == 改后镜像对应位置(0 处不符) - OK 每条负载都真的与"另一侧"不同(0 条与另一侧相同 —— 应当是 0) - OK 每条都是 I8(0x0a) + 压缩 + 464x464 + stride 464(0 条不符) - OK 每条 RLE 解出**恰好** usize=216321 且 1024+464*464 之后正好 1 字节尾巴(0 条不符) - OK 119/119 条的"原厂前 64 B"与"改后前 64 B"确实不同 - OK rb 的 target 也指向对应槽位 - OK 8 张界面素材与 ui_images/*.bin 逐字节一致(0 处不符) - ------------------------------------------------------------------------------- -[restore] S5ePAnimRestore.face 16450844 B sha256 13cafae4bf2b03db82f2c17f9d3af7c7e8c01d0c26275feda301753a76ebda69 ------------------------------------------------------------------------------- - OK magic - OK b5 = 0(固件自带表盘是 0) - OK b4 = 0 - OK id = b'462150102\x00' - OK preview 326x326 - OK Lua 在文件里只出现 1 次(偏移 0xf94220) - OK Lua 里的 ROLE == 'restore' - OK 占位符 __UIIMG__ 已回填 - OK 占位符 __PAY__ 已回填 - OK 占位符 __RB__ 已回填 - OK 占位符 __ROLE__ 已回填 - OK 界面尺寸 W,H = 480,480 - OK DEVS 有 health -> /dev/health - OK UIIMG 8 条(期望 8) - OK pay 119 条 / rb 119 条(期望 119/119) - OK 两个魔数各命中 1 次(payload 0xb16a / fp 0x205) - OK 两张表的偏移自洽:首条紧跟魔数,之后逐条紧邻(0 处不连续) - OK rb 每条都是 64 B - OK 第 k 条负载的目标偏移 == Measuring{k}.bin 的偏移(0 处不符) - OK 每条长度 == 该槽位原厂长度,且 rb 的 target 与 pay 一致(0 处不符) - OK 119 个目标偏移全部 512 对齐 - (97/119 条长度不是 4 的倍数 -> Lua 侧必须走 bs=512+4+1 分段写,否则退化成 bs=1) - OK 每条负载字节 == 原厂镜像对应位置(0 处不符) - OK 每条负载都真的与"另一侧"不同(0 条与另一侧相同 —— 应当是 0) - OK 每条都是 I8(0x0a) + 压缩 + 464x464 + stride 464(0 条不符) - OK 每条 RLE 解出**恰好** usize=216321 且 1024+464*464 之后正好 1 字节尾巴(0 条不符) - OK 119/119 条的"原厂前 64 B"与"改后前 64 B"确实不同 - OK rb 的 target 也指向对应槽位 - OK 8 张界面素材与 ui_images/*.bin 逐字节一致(0 处不符) - -============================================================================== -[互证] 两张交付物互相印证(完全不碰 .work / 固件镜像) -============================================================================== - OK 两张盘字节数相同(16450844) - OK 头 50 B(magic / b4 / b5 / id)逐字节相同 - OK replace 的负载前 64 B == restore 的指纹;restore 的负载前 64 B == replace 的指纹(0 处不符) - OK 两张盘的 8 张界面素材逐字节相同(同一套界面) - OK 两张盘的资源基址相同(0xb16a / 0x205) - OK 119/119 条负载在两张盘里都不一样(119) - -============================================================================== -[镜像] 改后镜像 vs 原厂镜像 -============================================================================== - OK inode 表(路径/偏移/next/spec/size/cksum)5555 条逐位不变 - OK superblock 与卷名不变(size 206924736 / cksum b804503a / 'health') - OK 分区长度不变(206924800 B) - OK 除 119 个槽位外,其余 5275 个文件的字节**逐字节**不变(0 处不符) - OK 变化区间 0x2c18618..0x3b9d384 完全落在槽位区间 0x2c18600..0x3b9d385 内 - OK 119 个槽位全部被改写(未变的:无) - -============================================================================== -独立总校验:70 项通过,0 项失败 -============================================================================== -EXIT=0 - -ALL_STEPS_OK=True diff --git a/s5e-pressure-anim/docs/ui_about.png b/s5e-pressure-anim/docs/ui_about.png deleted file mode 100644 index 702c5ae..0000000 Binary files a/s5e-pressure-anim/docs/ui_about.png and /dev/null differ diff --git a/s5e-pressure-anim/docs/ui_all.png b/s5e-pressure-anim/docs/ui_all.png deleted file mode 100644 index e3990bd..0000000 Binary files a/s5e-pressure-anim/docs/ui_all.png and /dev/null differ diff --git a/s5e-pressure-anim/docs/ui_confirm.png b/s5e-pressure-anim/docs/ui_confirm.png deleted file mode 100644 index a06b1c9..0000000 Binary files a/s5e-pressure-anim/docs/ui_confirm.png and /dev/null differ diff --git a/s5e-pressure-anim/docs/ui_help.png b/s5e-pressure-anim/docs/ui_help.png deleted file mode 100644 index f79741f..0000000 Binary files a/s5e-pressure-anim/docs/ui_help.png and /dev/null differ diff --git a/s5e-pressure-anim/docs/ui_help_err.png b/s5e-pressure-anim/docs/ui_help_err.png deleted file mode 100644 index 3639816..0000000 Binary files a/s5e-pressure-anim/docs/ui_help_err.png and /dev/null differ diff --git a/s5e-pressure-anim/docs/ui_help_ok.png b/s5e-pressure-anim/docs/ui_help_ok.png deleted file mode 100644 index 22fe935..0000000 Binary files a/s5e-pressure-anim/docs/ui_help_ok.png and /dev/null differ diff --git a/s5e-pressure-anim/docs/ui_home.png b/s5e-pressure-anim/docs/ui_home.png deleted file mode 100644 index 2a1e92e..0000000 Binary files a/s5e-pressure-anim/docs/ui_home.png and /dev/null differ diff --git a/s5e-pressure-anim/docs/ui_running.png b/s5e-pressure-anim/docs/ui_running.png deleted file mode 100644 index 5a694e6..0000000 Binary files a/s5e-pressure-anim/docs/ui_running.png and /dev/null differ diff --git a/s5e-pressure-anim/main.lua b/s5e-pressure-anim/main.lua deleted file mode 100644 index a018288..0000000 --- a/s5e-pressure-anim/main.lua +++ /dev/null @@ -1,817 +0,0 @@ ---[[---------------------------------------------------------------------------- - S5ePAnim —— S5 41mm(q63) 压力检测动画 -> Xiaomi Watch S5 eSIM 46mm(p62lte) - - 目标固件:miwear.watch.p62lte_v3.112.035_full(480x480,Lua 5.4 宿主) - 写入目标:ROMFS 分区 /dev/health(卷名 health,sb_size 206,924,736 B) - pressure/measure/Measuring1..119.bin - 119 个槽位全是 464x464 I8 + RLE(usize = 1024 + 464*464 + 1 = 216,321)。 - 负载**逐条等长**:每条写进去的字节数恰好等于原槽位长度(4485 ~ 196538 B), - 所以 ROMFS 的 inode 表 / 目录项 / 后面所有文件的绝对偏移一个字节都不动。 - - 源:Xiaomi Watch S5 41mm(q63, v4.101.020)pressure/measure 的 100 帧 480x480。 - 映射:第 k 个槽位(k=1..119) <- q63 第 round((k-1)/(119-1)*(100-1))+1 帧。 - - 界面按 480x480 设计(tools/s5e_pages.py,本机真 MiSans 渲染 + 真实像素判圆屏切边: - 0 越界、0 折行)。坐标与设计稿一一对应,预览图不是另画一套。 - - 两份盘共用同一个 watchface id 462150102(462 = S5 家族): - S5ePAnim.face 只装"替换后"的 119 条 + 原厂前 64 B 指纹 - S5ePAnimRestore.face 只装"原厂"的 119 条 + 替换后前 64 B 指纹 - 装其中一份就顶掉另一份,设备上始终只占一份空间。 - - ★ 分区键名 vs 设备节点(S4e 那版踩过的坑,别改回去) - DEVS 的**键**是 'health',值是设备节点 '/dev/health';运行期才取绝对路径。 - 负载表里存的是**分区内偏移**(整数),没有节点名字段。 - - 真机已验证的底层能力(沿用 S5->S4 那个表盘): - * 宿主不调用 ui.init -> 加载期建界面 - * SCRIPT_PATH 为空 -> 用 debug.getinfo 反推目录 - * .face 不解包,资源仍在 resource.bin 里 -> 按偏移读出来用 - * os.execute / dd 可用;os.execute 的返回值不可靠 -> 一律用内容比对判成败 - 真机**未**验证的:S5 eSIM(p62lte) 的 /dev/health 可写(同源平台 S4 eSIM 上的 dd 流程 - 实测通过)。所以写入前一律先只读预检查、再逐条版本校验,版本对不上就跳过分区剩余记录。 -------------------------------------------------------------------------------]] - -local lvgl = require("lvgl") - -local W, H = 480, 480 -local BLK = 4 - ---------------------------------------------------------------------- 设备与临时文件 -local DEVS = { health = "/dev/health" } -- 键名 -> 设备节点(Lua 表里只存键名) -local DEV = DEVS.health -local VOLUME = "health" -- ROMFS superblock 里的卷名,预检查要逐字节对上 -local TMPR = "/tmp/s5p_back.bin" -- 回读出来的那一份 -local TMPB = "/tmp/s5p_blob.bin" -- 待写入的负载(resource.bin 里抠出来的那一份) - --- 等长覆盖只保证「偏移」512 对齐;RLE 素材的长度常常不是 4 的倍数(如 4485 / 196538), --- 那种记录必须退化成 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 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__ -} - --- 角色:'replace' = 只负责替换(写 q63 的帧);'restore' = 只负责还原(写回原厂字节) --- 两份盘共用同一个 watchface id,所以设备上只会占一份空间(见 README) -local ROLE = '__ROLE__' - --- 负载表 { resource.bin 内偏移, /dev/health 内偏移, 长度 } —— 由脚本回填 --- pay = 本盘要写进去的字节 --- rb = **另一侧字节前 64 B 的指纹**(只给写入前的版本校验用) --- replace 盘的 rb 是原厂前 64 B;restore 盘的 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/s5p_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_SMALL = getFont('MiSans-Medium', 24) -- 说明文字 / 日志(一行 348px 以内) -local F_VER = getFont('MiSans-Medium', 21) - ---------------------------------------------------------------------- 按下高亮 --- 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, ok = 0, errs = 0, vers = 0, - skipRest = false } --- 从哪个入口进确认页:首页 -> 'replace'(本盘的 pay 表就是该角色的字节); --- 帮助页「回滚修改」只提示装另一份盘(本盘不含另一侧的完整负载) -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 - --- 时钟:居中不用 text_align(它要的是 lv_text_align_t,传 ALIGN.CENTER 越界会被忽略 --- -> 实测变成左对齐)。改为按真字体量出的宽度手工算 x:'10:24' 29px 量得 78 -local function clockLabel(parent) - 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 - --- 标题:框 y=36 h=46,字号 31 行高 42 -> 从框顶画即可(LVGL 标签从框顶起排) -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 是关键:LVGL 主题会给 Object 加内边距,不清零子元素会整体右下偏移。 --- radius=40 也不是审美选择:y=88 那行离圆心 152px,圆屏在那里的弦半宽只有 185.8, --- 圆角不够大时四个角会越过圆边(tools/s5e_check_clip.py 会量出来)。 -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 = 40 } - 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 - -- 标题 29px 行高 40,框高 42 -> 从框顶画就是垂直居中 - lab(plate, { x = 82, y = 26, w = 214, h = 42, text = title, - text_color = C_WHITE, font_size = 29, text_font = F_ITEM }) - end - return plate -end - --- 大按钮 258x82 圆角 41(y=376):圆角把四角收进圆内(本机像素判据实测 0 越界) -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 - -- 按钮文字:框 (71,22,126,40) 相对底板;'确认替换'/'确认还原'/'重启手表' 29px 量得 116 - -- -> 居中 x = (258-116)/2 = 71 - 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 - --- 偏移都是 512 对齐,RLE 素材的长度却常常不是 4/512 的倍数(4485 / 196538 …)。 --- 老写法对这种长度只能 bs=1 -> 一条 19 万字节要 38 万次单字节读写,慢到偶发失败 --- (S4e 真机上就是这么翻车的)。这里把它切成 ≤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 - --- 读设备上某偏移处的 len 字节(只读) -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 - --- 每段单独一个临时文件:这样 dd 只需要 seek(输出偏移),不依赖输入侧的 skip -local function ddWrite(off, len, data) - for _, s in ipairs(segs(off, len)) do - local b, rel, n = s[1], s[2], s[3] - 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 desc1 = nil - -local function setDesc1(txt, color) - if not desc1 then return end - pcall(function() desc1:set { text = txt, text_color = color } end) -end - --- 同一条记录,两边各自叫什么:replace 盘的 pay 是"改后"、rb 是"原厂" -local function sideName(which) - if which == 'mine' then - return (ROLE == 'restore') and '原厂' or '已替换' - end - return (ROLE == 'restore') and '已替换' or '原厂' -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 h = ddRead(0, 48) - if not h or #h < 48 then - setDesc1('预检查失败:设备节点读不到。', C_ERR) - return false - end - if h:sub(1, 8) ~= "-rom1fs-" then - setDesc1('预检查失败:设备节点读不到,\n或分区头不是 ROMFS。', C_ERR) - return false - end - local z = h:find("\0", 17, true) - local vol = h:sub(17, (z or 33) - 1) - if vol ~= VOLUME then - setDesc1('预检查失败:卷名是 ' .. vol .. '。', C_ERR) - return false - end - -- superblock 的 size 是**大端**(romfs 规范:__be32),所以这里必须用 ">I4"。 - -- (S4e 那版写成 " 越界判断形同虚设;这里修正。) - local size = string.unpack(">I4", h, 9) - local bad, total = 0, 0 - for i = 1, #list do - local r = list[i] - local b = ddBlk(r[2], r[3]) - if (r[2] % b) ~= 0 or (r[3] % b) ~= 0 then bad = bad + 1 end - if (r[2] + r[3]) > 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 probe = slice(list[1][1], list[1][3]) - if not probe or #probe ~= list[1][3] then - setDesc1('预检查失败:resource.bin 读取异常。', C_ERR) - return false - end - -- 抽样版本校验(只读):首 / 中 / 尾三条,比对设备上真实的 64 字节与本盘的 - -- 「要写进去的字节」或「另一侧指纹」—— 能确认负载偏移就是给这一版固件算的。 - local marks = { 1, math.max(1, #list // 2), #list } - local samp, okAll = {}, true - for i = 1, #marks do - local k = marks[i] - local rec, oth = list[k], (PAYTBL.rb or {})[k] - local got = ddRead(rec[2], 64) - local mine = slice(rec[1], 64) - local other = oth and slice(oth[1], 64) or nil - if not got or #got < 64 then - samp[#samp + 1] = '×' - okAll = false - elseif mine and got == mine then - samp[#samp + 1] = sideName('mine') .. '✓' - elseif other and got == other then - samp[#samp + 1] = sideName('other') .. '✓' - else - samp[#samp + 1] = '抽样×' - okAll = false - end - end - local head = string.format('预检查通过 | %d 条 | %.2fMB', #list, total / 1048576) - setDesc1(head .. '\n' .. VOLUME .. ' 分区可读。' .. table.concat(samp, ' '), - 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 - -- 日志框 h=280、24px 行高 34 -> 最多 8 行;留 8 行保证最后一行完整可见 - 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 - --- 一条记录三步:① 版本校验(设备上那段必须等于"本盘要写的"或"另一侧"的前 64 B) --- ② 写入 ③ 整段回读比对 --- 版本两边都不是 -> 'ver',并且**跳过 /dev/health 剩余记录**(换固件后最安全的行为: --- 绝不把字节写进别人的文件里)。os.execute 的返回值在这台机器上不可靠,所以判据 --- 一律是内容比对。 -local function writeOne(rec, want) - local data = slice(rec[1], rec[3]) - if not data or #data ~= rec[3] then return 'err' end - local n = 64 - if want then - if want[3] < n then n = want[3] end - local other = slice(want[1], n) - local cur = ddRead(rec[2], n) - if not cur or #cur ~= n or not other or #other ~= n then return 'err' end - if cur ~= data:sub(1, n) and cur ~= other then return 'ver' 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 'ok' -end - -local function startJob(variant) - if job.run then return end - local list = PAYTBL.pay or {} - if #list == 0 then - setDesc1('负载表为空,已中止。', C_ERR) - return - end - job.run, job.list, job.i, job.ok, job.errs, job.vers = true, list, 0, 0, 0, 0 - job.skipRest = false - logLines = {} - logOut(ROLE == 'restore' and '> 开始还原' or '> 开始替换') - logOut(string.format('> %s 119 帧 / 共 %d 条', VOLUME, #list)) - logOut('> 读取 resource.bin') - if rebootBtn then pcall(function() rebootBtn:add_flag(lvgl.FLAG.HIDDEN) end) end - if runningTitle then - pcall(function() - runningTitle:set { text = (ROLE == 'restore') and '正在还原' or '正在替换' } - 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 - logOut(ROLE == 'restore' and '> 还原完毕,请重启手表。' - or '> 替换完毕,请重启手表。') - logOut(string.format('> 共 %d 条,成功 %d 失败 %d', n, job.ok, job.errs)) - if job.vers > 0 then - logOut(string.format('> 版本不符跳过 %d 条', job.vers)) - logOut('> 请勿重启,先装回匹配版本。') - else - logOut('> 若发现问题,请回滚修改。') - end - showReboot() - return - end - if job.skipRest then - job.vers = job.vers + 1 - else - local rb = PAYTBL.rb and PAYTBL.rb[job.i] - local r = writeOne(job.list[job.i], rb) - if r == 'ver' then - job.vers = job.vers + 1 - job.skipRest = true -- 版本不符:跳过 /dev/health 剩余记录 - logOut('> /dev/health 版本不符,已跳过剩余记录') - elseif r == 'ok' then - job.ok = job.ok + 1 - 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 restore = (ROLE == 'restore') - -local function buildHome() - local p = newPage() - if not p then return nil end - clockLabel(p) - -- 盘内标题 = 显示名(不再用中文描述名);x 按真 MiSans 31px 量出的宽度居中: - -- 'S5e Pt.2' = 125 -> x = (480-125)/2 = 177.5 -> 177 - -- 'S5e Pt.2 · 还原' = 225 -> x = (480-225)/2 = 127.5 -> 127 - if restore then - pageTitle(p, 'S5e Pt.2 · 还原', 127, 227) - else - pageTitle(p, 'S5e Pt.2', 177, 127) - end - listItem(p, 88, 'ic_help', '快速帮助', function() show(pages.help) end) - listItem(p, 186, 'ic_replace', restore and '开始还原' or '开始替换', 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 = '检查 health 分区与负载完整性。', - text_color = C_GRAY, font_size = 24, text_font = F_SMALL }) - - -- 拆盘之后:本盘只装一份负载,「回滚/恢复」由另一份盘负责 - listItem(p, 264, 'ic_rollback', '回滚修改', function() - if restore then - setDesc1('本盘负责还原,换回动画\n请装 S5e Pt.2。', C_GRAY) - else - setDesc1('本盘只负责替换,请装\nS5e Pt.2 · 还原。', C_GRAY) - end - end) - lab(p, { x = 67, y = 360, w = 348, h = 42, - text = restore and '把 119 帧动画换回原厂样式。' - or '恢复 119 帧动画的原厂样式。', - 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, restore and '开始还原' or '开始替换', 188, 124) - setClickable(t) - bind(t, goBack) - - -- 多色文本**不能**用 recolor:AP 里那几处 recolor 是图片重着色(bg_img_recolor) - -- 和表盘编辑器的主题重着色,不是文本属性;实测标记会被原样显示出来。 - -- 改为拆成多个标签 + 手工算 x(宽度用真 MiSans 25px 量出),框宽 ≥ 文本宽 - -- -> 保证不折行(折行会让手工算 x 的片段错位),并且整框都在圆内。 - -- 型号拆两行:圆屏在 y=243 处的弦半宽只有 ~190px,而 'Xiaomi Watch S5 eSIM 46mm' - -- 量得 367px,放一行会被圆边切掉(tools/s5e_check_clip.py 抓过这个)。 - local WARN = { - { 60, 95, 360, 34, '替换压力检测动画之前,', C_GRAY }, - { 60, 129, 360, 34, '请先确认自己的系统版本号为 ', C_GRAY }, - { 304, 129, 120, 34, '3.112.035', C_WHITE }, - { 422, 129, 40, 34, '!', C_GRAY }, - { 64, 195, 360, 34, '请确认自己的手表型号为', C_GRAY }, - { 64, 229, 360, 34, 'Xiaomi Watch S5', C_WHITE }, - { 64, 263, 360, 34, 'eSIM 46mm(p62lte)', C_WHITE }, - { 60, 341, 360, 34, '搞机有风险,变砖后果自负。', C_GRAY }, - } - for i = 1, #WARN do - local w = WARN[i] - lab(p, { x = w[1], y = w[2], w = w[3], h = w[4], text = w[5], - text_color = w[6], font_size = 25, text_font = F_BODY }) - end - - local b = bigButton(p, restore and '确认还原' or '确认替换') - 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) - - -- 信息卡:底板 + 圆形 logo + 大标题 + 版本号 - 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 - -- 卡内 29px 量得:'S5e Pt.2' = 118 -> x = 54 + (373-118)/2 = 181.5 -> 181 - -- 'S5e Pt.2 · 还原' = 211 -> x = 54 + (373-211)/2 = 135(整) - if restore then - lab(p, { x = 135, y = 200, w = 213, h = 42, text = 'S5e Pt.2 · 还原', - text_color = C_WHITE, font_size = 29, text_font = F_ITEM }) - else - lab(p, { x = 181, y = 200, w = 120, h = 42, text = 'S5e Pt.2', - text_color = C_WHITE, font_size = 29, text_font = F_ITEM }) - end - -- '1.0.0' 21px 量得 49 -> 居中 x = 54 + (373-49)/2 = 216 - lab(p, { x = 222, y = 237, w = 49, h = 28, text = '1.0.0', - text_color = C_GRAY, font_size = 21, text_font = F_VER }) - - -- QQ 联系方式:结构与列表项一致(无动作,所以不给它绑点击) - 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) - -- 本页没有返回图标(规格:顶栏同首页),所以标题要屏幕居中: - -- '正在替换'/'正在还原' 31px 量得 124 -> x = 240 - 124/2 = 178 - runningTitle = pageTitle(p, restore and '正在还原' or '正在替换', 178, 124) - logLabel = lab(p, { x = 67, y = 95, w = 348, h = 280, text = '', - text_color = C_WHITE, font_size = 24, text_font = F_SMALL }) - 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-pressure-anim/tools/iconpack.py b/s5e-pressure-anim/tools/iconpack.py deleted file mode 100644 index 4012c6e..0000000 --- a/s5e-pressure-anim/tools/iconpack.py +++ /dev/null @@ -1,447 +0,0 @@ -"""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-pressure-anim/tools/lua_balance.py b/s5e-pressure-anim/tools/lua_balance.py deleted file mode 100644 index a33ebba..0000000 --- a/s5e-pressure-anim/tools/lua_balance.py +++ /dev/null @@ -1,72 +0,0 @@ -"""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('') - - -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-pressure-anim/tools/s5e_anim.py b/s5e-pressure-anim/tools/s5e_anim.py deleted file mode 100644 index e85bab3..0000000 --- a/s5e-pressure-anim/tools/s5e_anim.py +++ /dev/null @@ -1,562 +0,0 @@ -# -*- coding: utf-8 -*- -"""把 S5 41mm(q63) 的 `pressure/measure/Measuring1..100.bin`(480x480)逐帧等长写进 -S5 eSIM(p62lte) `vela_health.bin` 的 `pressure/measure/Measuring1..119.bin`(464x464 I8+RLE)。 - -硬约束(为什么必须"逐字节等长") --------------------------------- -ROMFS 里 `next` / `spec` 全是**绝对偏移**,文件数据又紧随 inode 之后;任何一条负载 -长度变化 1 字节,后面所有文件的偏移就整体平移 → 分区报废。所以: - - 12 B 头 + 12 B 压缩头 + csize 必须 == 原槽位长度 - csize 原样沿用(解码器按 csize 读),usize 原样沿用(= 1024 + 464*464 = 216321) - -四步流水线 ----------- - 1) 解码 q63 帧 → 缩放到 464x464(**缩小**,LANCZOS) - 2) 暗部归零:q63 帧的"黑"其实是一条均匀的 #1a1a1a(26,26,26) 底噪 + 抖动噪声, - 实测占 68% 的像素、熵极高(256 色几乎全糊在暗部,不处理时 RLE 压不动:172 KB)。 - 阈值以下的像素一律置 0,背景塌成整行单游程,体积掉到 1/4(~45 KB)。 - 3) 量化:按一条**单调质量阶梯**(缩放比例 + 颜色数一起动)挑能塞进 csize 的最高档。 - 4) LVGL RLE 压缩 → 先补「重复 N 次」把解码长度做到**正好 usize**, - 再用 `pad_rle_safe` 的合法空指令(00 00 / 80)补齐到 csize。 - -为什么用"质量单参数"而不是逐槽各自最优 --------------------------------------- -槽位预算从 4.5 KB 到 196 KB 差 40 倍。逐帧各自最优会让相邻帧出现 -「2 色 x0.32」跳到「4 色 x0.80」这种忽大忽小(S4e 那版踩过)。这里把 -(颜色数, 缩放) 绑成一个连续量,先算每槽能撑住的最大档,再从峰值向两侧取 -「运行最小值」—— 曲线单峰且平滑,画面尺寸/色阶随预算连续变化。 - -产出 ----- - .work/anim/vela_health_anim.bin 改后镜像(长度 == 原镜像) - .work/anim/manifest.json 逐条清单(偏移/长度/映射/降级/保留) -""" -import os, sys, json, struct, shutil, hashlib, time -sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) -from paths import (PROJ, INPUTS, WORK, TARGET_IMAGE, SOURCE_IMAGE, TARGET_VOLUME, - DIRP, FACE_W, FACE_H) -from romfs import Romfs -import lvgl_bin as L -from PIL import Image -import numpy as np - -OUTDIR = os.path.join(WORK, 'anim') -PATCHED = os.path.join(OUTDIR, 'vela_health_anim.bin') -# 设备端解码器要求流解出的字节数**正好** == 头里的 usize。原厂 119 条全是 216321, -# 比"调色板 + 像素"多 1 字节(解码器只用到前 1024 + 464*464 字节,多出来的 1 字节 -# 落在行外、被 `y < h` 挡掉,所以既安全又是原厂行为)。实测值由 build() 从镜像里读出来, -# 不在这儿写常量 —— 写错过一次(写成 216320)就被下面的断言当场抓住。 -USIZE = 1024 + FACE_W * FACE_H # 216320:调色板 + 像素;真实值以镜像为准 -TAIL_EXTRA = 1 # 期望的原厂尾部额外字节数 -USIZE_EXTRA = TAIL_EXTRA # 实际值由 build() 从镜像读出来后覆盖 - -# ------------------------------------------------------------------ 暗部归零 -# q63 帧的底噪是 #1a1a1a(26),球体最暗的实体像素也 >100。取 40 吃掉整片底噪与抖动, -# 又不动球体本体;56 作为第二档兜底(更狠,留给塞不下的槽位)。 -DARK_LEVELS = (40, 56) - -# ------------------------------------------------------------------ 固定调色板(低预算兜底) -# 到 8 色以下时"重新聚类"会把仅剩的颜色浪费在底噪残渣上、球体反而变花; -# 改用这几套手工调色板(索引 0 一定是黑),画面干净、也更好压。 -FIXED_PALETTES = { - 2: [(0, 0, 0), (92, 132, 255)], - 3: [(0, 0, 0), (40, 60, 190), (120, 190, 255)], - 4: [(0, 0, 0), (28, 40, 150), (70, 120, 245), (140, 220, 210)], - 6: [(0, 0, 0), (18, 26, 110), (44, 66, 200), (80, 130, 250), - (140, 200, 245), (150, 220, 160)], - 8: [(0, 0, 0), (12, 18, 80), (26, 38, 140), (46, 70, 195), (72, 110, 240), - (110, 165, 252), (165, 215, 250), (150, 222, 168)], -} - -# ------------------------------------------------------------------ 质量阶梯 -# 从「最好」到「最省」单调排列:(缩放, 颜色数, 固定调色板色数|None) -LADDER = [ - (1.00, 256, None), (1.00, 192, None), (1.00, 128, None), (1.00, 96, None), - (0.95, 96, None), (0.95, 64, None), (0.90, 64, None), - (0.85, 64, None), (0.85, 48, None), (0.80, 48, None), - (0.75, 48, None), (0.75, 32, None), (0.70, 32, None), - (0.65, 32, None), (0.65, 24, None), (0.60, 24, None), - (0.55, 24, None), (0.55, 16, None), (0.50, 16, None), - (0.45, 16, None), (0.45, 12, None), (0.40, 12, None), - (0.35, 12, None), (0.35, 8, 8), (0.30, 8, 8), - (0.26, 6, 6), (0.22, 6, 6), (0.19, 4, 4), (0.16, 4, 4), - (0.13, 3, 3), (0.11, 3, 3), (0.09, 2, 2), (0.07, 2, 2), (0.05, 2, 2), -] -CI_KEEP = len(LADDER) # 哨兵:连最省档都塞不下 -# 平滑之后允许比目标档**大**多少缩放:0 表示只许更小。留一点余量是为了让球体尽量顶到 -# 预算上限,同时不给相邻帧制造肉眼可见的尺寸跳变(每档缩放差约 0.05)。 -SCALE_SLACK = 0.02 - - -def num_of(path): - return int(path.rsplit('Measuring', 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') or i.size == 0: - continue - d[num_of(p)] = (p, i, rom.read(i)) - return d - - -# ------------------------------------------------------------------ 编码原语 -def palette_bytes(cols): - """1024 B BGRA;索引 0 一定是黑,没用的条目留 0 —— 调色板本身也好压。""" - out = bytearray(1024) - for k, (r, g, b) in enumerate(cols[:256]): - out[k * 4:k * 4 + 4] = bytes((b, g, r, 255)) - return bytes(out) - - -def kill_dark(img, dark): - a = np.asarray(img.convert('RGB')).copy() - a[a.max(axis=2) < dark] = 0 - return Image.fromarray(a, 'RGB') - - -def scale_image(img, sc): - if sc >= 0.999: - return img - s = max(4, int(round(FACE_W * sc))) - small = img.convert('RGBA').resize((s, s), Image.LANCZOS) - cv = Image.new('RGBA', (FACE_W, FACE_H), (0, 0, 0, 255)) - cv.paste(small, ((FACE_W - s) // 2, (FACE_H - s) // 2)) - return cv - - -def quantize_raw(rgb, ncol, fixed): - """-> 1024 B 调色板 + w*h 个索引(LVGL I8 的裸像素流)。 - - 自己实现量化,不再借 PIL 的调色板 API —— 这里踩过两个坑,留作警钟: - ① 为了"保证索引 0 是黑",先把图贴到 (W+8)x(H+8) 黑画布上再 quantize, - 然后 `q.tobytes()[:W*H]` 取像素。但 q 是 472 宽的,按 464 宽读就"每行错 8 列", - 整幅图被剪成斜条纹 —— RLE 流完全正确、解出来也和"量化结果"逐点一致, - 只有拿"量化结果的形状"去比**源图**才抓得住(见 silhouette())。 - ② 改成"循环移位调色板 + convert()/paste()"更糟:`quantize(palette=...)` 会把图重新 - 合并成它自己算出来的调色板,`paste()` 又只搬调色板,于是 64 色时整幅图变纯黑。 - 与其猜 PIL 的语义,不如把量化写成几行明确的 numpy。 - - 现在:黑单独占索引 0(背景因此塌成单游程),其余 ncol-1 个颜色在**非黑像素**上做 - 加权 k-means(按出现次数加权,梯度才不会被子像素级的近似黑拖偏)。固定迭代次数 => 确定。 - """ - src = np.asarray(rgb if rgb.mode == 'RGB' else rgb.convert('RGB')) - return quantize_arr(src, ncol, fixed) - - -def quantize_arr(src, ncol, fixed, cache=None, ckey=None): - """量化主体。src 是 (H,W,3) uint8;ckey 用来复用同一帧的颜色表。 - - 性能关键(第一版在这里把机器跑爆过内存 1.7 GB / CPU 卡死): - * 源图有 1.5~2 万个不同颜色、21.5 万个像素。**质心只在"不同颜色"上算**, - 绝不在 21.5 万像素上做 (N, K, 3) 的距离广播 - (那一步就是 N=215296 x K=255 的模式,一个临时数组 1.6 GB)。 - * 像素 -> 索引 走一张**粗查色立方** LUT:先把 RGB 量化到 step 网格 - (step=4 时 64³ = 262144 个格子),只对这 26 万个格子算一次最近邻, - 再让 21.5 万像素直接查表。代价是"近似最近邻"(最大偏 step/2 = 2 个色阶), - 对 8 位调色板完全够用。 - """ - H, W = src.shape[:2] - if cache is None: - cache = {} - ent = cache.get(ckey) - if ent is None: - flat = src.reshape(-1, 3) - bg = flat.max(axis=1) == 0 - upix, inv = np.unique(flat, axis=0, return_inverse=True) - upix = np.ascontiguousarray(upix, dtype=np.int32) - inv = inv.reshape(-1).astype(np.int32) - fg = np.nonzero(upix.max(axis=1) > 0)[0] - ent = (upix, inv, fg, bg, flat.shape[0]) - cache[ckey] = ent - upix, inv, fg, bg, npix = ent - if len(fg) == 0: - return palette_bytes([(0, 0, 0)]) + bytes(npix) - if fixed is not None: - cols = FIXED_PALETTES[fixed] - out = _lut_index(upix, cols)[inv] - out[bg] = 0 - # 背景必须落在索引 0(黑),这样 RLE 才是一条单游程 - if cols[0] != (0, 0, 0): - raise ValueError('固定调色板的索引 0 必须是黑') - return palette_bytes(cols) + out.tobytes() - k = max(1, min(255, int(ncol) - 1)) - ufg = upix[fg] - uq, cnt = np.unique(ufg, axis=0, return_counts=True) - if len(uq) <= k: - centers = uq.astype(np.float64) - else: - centers = uq[cnt.argsort()[::-1][:k]].astype(np.float64) # 出现最多的 k 色做种子 - w = cnt.astype(np.float64) - K = len(centers) - for _ in range(8): - lab = _nearest_argmin(uq, centers) - new = centers.copy() - for j in range(K): - sel = lab == j - if sel.any(): - ww = w[sel][:, None] - new[j] = (uq[sel] * ww).sum(axis=0) / ww.sum() - done = np.allclose(new, centers, atol=0.25) - centers = new - if done: - break - cols = [(0, 0, 0)] + [tuple(int(round(v)) for v in c) for c in centers] - out = _lut_index(upix, cols)[inv] - out[bg] = 0 - return palette_bytes(cols) + out.tobytes() - - -def _nearest_argmin(px, cent): - """返回每个 px 最近的 cent 下标;只用 float32(int32 的立方广播会反复申请上百 MB)。""" - a = np.asarray(px, dtype=np.float32) - c = np.asarray(cent, dtype=np.float32) - out = np.empty(len(a), dtype=np.int32) - chunk = 8192 - for s in range(0, len(a), chunk): - d = ((a[s:s + chunk, None, :] - c[None, :, :]) ** 2).sum(axis=2) - out[s:s + chunk] = d.argmin(axis=1) - return out - - -_GRID_CACHE = {} - - -def _cube_grid(step): - """粗查色立方的网格代表色(只算一次,全局缓存)。 - - 展平顺序是 idx = r*(bins*bins) + g*bins + b —— 与 `meshgrid(indexing='ij')` - + `reshape(-1,3)` 完全一致,所以立方体可以直接按这个公式索引。 - ★ 轴序写错会静默换色:查表必须用 (R,G,B) 顺序,写成 (B,G,R) 颜色就全串了。 - """ - g = _GRID_CACHE.get(step) - if g is None: - bins = 256 // step + 1 - ax = np.arange(bins, dtype=np.int32) * step - grid = np.stack(np.meshgrid(ax, ax, ax, indexing='ij'), axis=-1).reshape(-1, 3) - _GRID_CACHE[step] = (grid, bins) - return _GRID_CACHE[step] - return g - - -def _lut_index(px, cols): - """像素 -> 最近调色板项,走粗查色立方 LUT。 - - step=8 → 33³ = 35937 个格子,单通道最大偏差 step/2 = 4 个色阶; - 对这些低对比度、本来就带抖动的画面完全够用(step=4 的 26 万格子慢 8 倍)。 - 用 float32 算距离:int32 的 (35937, 256, 3) 中间数组会反复申请 ~100 MB, - 是之前把内存跑到 2.6 GB、整体跑十几分钟的元凶。 - """ - grid, bins = _cube_grid(8) - pal = np.asarray(cols, dtype=np.float32) - best = np.empty(len(grid), dtype=np.uint8) - gn = grid.astype(np.float32) - chunk = 8192 - for s in range(0, len(grid), chunk): - a = gn[s:s + chunk] - d = ((a[:, None, :] - pal[None, :, :]) ** 2).sum(axis=2) - best[s:s + chunk] = d.argmin(axis=1).astype(np.uint8) - q = np.clip((px.astype(np.int32) + 4) >> 3, 0, bins - 1) - flat = (q[:, 0] * bins + q[:, 1]) * bins + q[:, 2] - return best[flat] - - -def pad_to_usize(comp, raw_len): - """解码长度必须**正好 == usize**,否则设备端解码器直接判失败、整帧不画。 - - ★ 本会话踩过的坑:库里 rle_compress 的输出曾比 usize 少 1 个字节(末尾少一条指令)。 - 这里显式补「重复 N 次」token 把长度做满。补出来的余数可能是奇数(原厂 usize 就是 - 1024+464*464+1 = 216321,比"调色板+像素"多 1),所以最后允许一条「重复 1 次」的 - 2 字节指令;`00 00`(重复 0 次)也是合法空指令。 - """ - short = USIZE - raw_len - if short <= 0: - return comp - ex = bytearray() - while short >= 2: - k = min(short, 127) - ex += bytes((k, 0)) - short -= k - if short == 1: - ex += bytes((1, 0)) - return comp + bytes(ex) - - -def encode_cell(img, cell, qcache=None, qkey=None): - """(缩放, 颜色数, 固定) -> (RLE 流 bytes, 裸像素流 bytes)。不含 24 B 头。 - - qkey 要带上**缩放比例**:同一帧的不同缩放是不同的数组, - ★ 少了这个维度会让"不同颜色的表"永远命中不了缓存,每次都在 21.5 万像素上重算 - (第一版就是这样把内存跑到 1.7 GB、CPU 卡死的)。 - """ - sc, ncol, fixed = cell - rgb = scale_image(img, sc).convert('RGB') - raw = quantize_arr(np.asarray(rgb), ncol, fixed, qcache, - (qkey, sc) if qkey is not None else None) - return pad_to_usize(L.rle_compress(raw), len(raw)), raw - - -def raw_to_rgb(raw): - """把裸像素流还原成 RGB 图(用于"解出来的像素 == 量化结果"这条自证)。""" - pal = bytearray(768) - for i in range(256): - pal[i * 3 + 0] = raw[i * 4 + 2] - pal[i * 3 + 1] = raw[i * 4 + 1] - pal[i * 3 + 2] = raw[i * 4 + 0] - im = Image.frombytes('P', (FACE_W, FACE_H), raw[1024:1024 + FACE_W * FACE_H]) - im.putpalette(bytes(pal)) - return im.convert('RGB') - - -def silhouette(raw): - """(非黑像素数, bbox) —— 用来挡住"字节完全自洽、内容却整片错位"这类 bug。 - - ★ 这里就是因为只比字节、不比形状,差点放过去一版"斜条纹"图: - 当时 quantize 是按 472 宽取的像素,读成 464 宽 → 每行错 8 列 → 整幅图被剪成斜条纹。 - RLE 流本身完全正确、解出来的像素也和"量化结果"逐点一致,所以只有把 - "量化结果"再和**源图**比形状才抓得住。下面这个函数就是干这个的。 - """ - a = np.asarray(raw_to_rgb(raw)) - m = a.max(axis=2) > 24 - n = int(m.sum()) - if not n: - return 0, None - ys, xs = np.nonzero(m) - return n, (int(ys.min()), int(xs.min()), int(ys.max()), int(xs.max())) - - -# ------------------------------------------------------------------ 主流程 -def build(): - t0 = time.time() - global USIZE, USIZE_EXTRA - os.makedirs(OUTDIR, exist_ok=True) - stock, src = Romfs(TARGET_IMAGE), Romfs(SOURCE_IMAGE) - if stock.volume != TARGET_VOLUME: - raise SystemExit('目标镜像卷名是 %r,期望 %r —— 拿错分区了?' - % (stock.volume, TARGET_VOLUME)) - slots, srcs = collect(stock), collect(src) - tl, sl = sorted(slots), sorted(srcs) - h0 = L.parse_header(slots[tl[0]][2]) - print('目标槽位 %d 个(Measuring%d..Measuring%d,合计 %.2f MB)' % ( - len(tl), tl[0], tl[-1], sum(len(slots[n][2]) for n in tl) / 1048576)) - print('源帧 %d 个(Measuring%d..Measuring%d,合计 %.2f MB)' % ( - len(sl), sl[0], sl[-1], sum(len(srcs[n][2]) for n in sl) / 1048576)) - print('槽位格式 %dx%d cf=0x%02x flags=0x%02x stride=%d' % ( - h0['w'], h0['h'], h0['cf'], h0['flags'], h0['stride'])) - assert (h0['w'], h0['h']) == (FACE_W, FACE_H), '槽位不是 %dx%d' % (FACE_W, FACE_H) - assert (h0['cf'], h0['flags']) == (0x0a, 0x08), '槽位不是 I8 + RLE' - # ★ usize 从镜像里读出来,别写常量:原厂是 1024 + 464*464 + 1 = 216321 - n_usize = {struct.unpack(' (comp, raw) - src_sils = {} # (src_n, dark, ci) -> (非黑像素数, bbox) - qcache = {} - - def get_cell(src_n, dark, ci): - key = (src_n, dark, ci) - v = cells.get(key) - if v is None: - v = encode_cell(frames[src_n][dark], LADDER[ci], qcache, (src_n, dark)) - cells[key] = v - src_sils[key] = silhouette(v[1]) - return v - - def fits(src_n, dark, ci, csize): - return len(get_cell(src_n, dark, ci)[0]) <= csize - - # ---- 第一遍:逐槽量出「能塞进 csize 的最大阶梯档」---- - caps = [] - for k, n in enumerate(tl): - csize = struct.unpack(' best[1]: - best = ((ci, dark), key) - rec_name = '%03d_%s_%d' % (len(records), path.rsplit('/', 1)[-1][:-4], n) - if best is None: - kept.append((n, csize)) - records.append(dict(name=rec_name, src='measure%d' % src_n, offset=ino.data_off, - length=len(blob), data=blob, csize=csize, how='保留原厂', - ci=None, dark=None, scale=1.0, colors=0, pad=0, enc=0)) - print(' !! 槽 %d(csize %d)塞不下 -> 保留原厂' % (n, csize)) - continue - (ci, dark), _key = best - comp, raw = get_cell(src_n, dark, ci) - new = blob[:12] + struct.pack('= 0.995 else ' 缩放%.2f' % sc - how += ' 暗部<%d' % dark - if ci > start: - degrade.append((n, csize, start, ci, how)) - how += ' [平滑目标第%d档放不下,退到第%d档]' % (start, ci) - records.append(dict(name=rec_name, src='measure%d' % src_n, offset=ino.data_off, - length=len(blob), data=new, csize=csize, how=how, ci=ci, - dark=dark, scale=round(sc, 4), colors=ncol, fixed=fixed, - pad=csize - len(comp), enc=len(comp), target_ci=start)) - - # ---- 写出镜像 + 清单 ---- - shutil.copyfile(TARGET_IMAGE, 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 S5 eSIM 46mm (p62lte)', - partition='vela_health.bin volume=health device node=/dev/health', - source='Xiaomi Watch S5 41mm (q63, v4.101.020) pressure/measure frames', - target_dir=DIRP, - mapping='第 k 个槽位(k=1..%d) <- q63 第 round((k-1)/(%d-1)*(%d-1))+1 帧(1..%d)' - % (len(tl), len(tl), len(sl), len(sl)), - face_wh=[FACE_W, FACE_H], slot_count=len(tl), source_count=len(sl), usize=USIZE, - image_sha256=hashlib.sha256(stock.data).hexdigest(), - ladder=[dict(ci=i, scale=c[0], colors=c[1], fixed=c[2]) for i, c in enumerate(LADDER)], - records=[dict(name=r['name'], offset=r['offset'], length=r['length'], - src=r['src'], how=r['how'], ci=r['ci'], dark=r['dark'], - scale=r['scale'], colors=r['colors'], fixed=r['fixed'], - pad=r['pad'], enc=r['enc'], target_ci=r['target_ci'], - csize=r['csize']) for r in records], - kept_stock=[dict(n=n, csize=c, name=slots[n][0]) for n, c in kept], - degraded=[dict(n=n, csize=c, from_ci=a, to_ci=b, how=h) for n, c, a, b, h in degrade]) - json.dump(manifest, open(os.path.join(OUTDIR, 'manifest.json'), 'w', encoding='utf-8'), - indent=1, ensure_ascii=False) - - # ---- 汇总 ---- - print('\n阶梯使用情况(颜色数, 缩放, 暗部 -> 帧数):') - used = {} - for r in records: - if r['ci'] is None: - continue - k2 = (r['colors'], r['scale'], r['dark']) - used[k2] = used.get(k2, 0) + 1 - for kk, v in sorted(used.items(), key=lambda kv: (-kv[0][1], -kv[0][0])): - print(' %3d 色 缩放%.2f 暗部<%d -> %3d 帧' % (kk[0], kk[1], kk[2], v)) - print(' 保留原厂:%d 帧 %s' % (len(kept), [n for n, _ in kept])) - print(' 平滑目标档放不下、被迫退档:%d 帧 %s' - % (len(degrade), [(n, h) for n, _c, _a, _b, h in degrade][:8])) - pads = [p[3] for p in stat] - print(' 槽位填充:实际用掉 %.1f%% 的 csize(填充 min %d / max %d / 平均 %.0f B,' - '全是合法空指令 00 00 / 80)' - % (100.0 * sum(p[2] for p in stat) / sum(p[1] for p in stat), - min(pads), max(pads), sum(pads) / len(pads))) - print(' 回解自证:正好 usize 且像素一致 %d / %d' % (decode_ok, len(records) - len(kept))) - - # ---- 独立自证 ---- - 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(size %d / cksum %08x)' % ( - stock.sb_size == r2.sb_size and stock.sb_cksum == r2.sb_cksum, - r2.sb_size, r2.sb_cksum)) - print('镜像长度一致:%s(%d B)' % (len(stock.data) == len(r2.data), len(r2.data))) - sh = open(PATCHED, 'rb').read() - replay = all(sh[r['offset']:r['offset'] + r['length']] == r['data'] for r in records) - print('回放(改后镜像 == 计划字节):%s' % replay) - lens_ok = all(r['length'] == len(slots[int(r['name'].rsplit('_', 1)[1])][2]) for r in records) - print('每条负载长度 == 原槽位长度:%s' % lens_ok) - print('\n负载合计 %.2f MB;耗时 %.0fs;写出 %s' - % (sum(r['length'] for r in records) / 1048576, time.time() - t0, - os.path.join(OUTDIR, 'manifest.json'))) - return manifest - - -if __name__ == '__main__': - only = sys.argv[1] if len(sys.argv) > 1 else 'all' - if only not in ('all', 'replace', 'restore'): - raise SystemExit('用法: s5e_anim.py [all|replace|restore]') - build() diff --git a/s5e-pressure-anim/tools/s5e_build.py b/s5e-pressure-anim/tools/s5e_build.py deleted file mode 100644 index 19b9cfb..0000000 --- a/s5e-pressure-anim/tools/s5e_build.py +++ /dev/null @@ -1,312 +0,0 @@ -# -*- coding: utf-8 -*- -"""两遍构建:表盘源码 + 资源 -> dist/S5ePAnim.face / dist/S5ePAnimRestore.face - - main.lua <- **源码**,含 __UIIMG__ / __PAY__ / __RB__ / __ROLE__ 占位符 - .work/overlay/ <- 中间产物(回填后的 Lua 落在这里) - .work/build/ <- 编译工作区(Compiler.exe 的输入) - dist/.face <- 交付物 - -为什么两遍 ----------- -.face 里各资源的偏移取决于打包顺序;我们靠在 .face 里**搜字节**来定位它们。 -Lua 源码排在所有资源之后,所以回填(Lua 变长)不会移动前面的资源 —— 脚本最后逐条校验 0 漂移。 - -为什么最终产物要"再裸编一次" ----------------------------- -两遍构建走的是 vendor 的 scripts/build_face.ps1,它在编译后调 set_face_id.ps1, -把**头里第 5 字节写成 id 长度 10**,而固件自带表盘的 b5 是 0。 -所以交付物改从裸 `Compiler.exe -b` 的输出取(b5=0),只覆盖 40..49 = id。 -断言:裸产物与 toolchain 产物必须"只差 b5 + id 这 8 个字节",否则直接拒绝交付。 - -两份盘共用同一个 watchface id(462150102):装一份就顶掉另一份,设备上只占一份空间。 -""" -import os, re, sys, json, shutil, hashlib, struct, subprocess -sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) -from paths import PROJ, WORK, DIST, TEMPLATE, WF_ID, SCREEN, DEVICE_TYPE, PREVIEW -import s5e_overlay as OV - -ROLE = (sys.argv[1] if len(sys.argv) > 1 else 'replace').lower() -assert ROLE in ('replace', 'restore'), ROLE -NAME = 'S5ePAnim' if ROLE == 'replace' else 'S5ePAnimRestore' # 路径/标识(不要改) -TITLE = 'S5e Pt.2' # 手表上显示的表盘名(replace / restore 两张盘同名,须与 s5e_overlay.TITLE 一致) -RECDIR = 'payload' if ROLE == 'replace' else 'payload_restore' -TOOLS = os.path.dirname(os.path.abspath(__file__)) -SRC = os.path.join(PROJ, 'main.lua') -OVER = os.path.join(WORK, 'overlay', NAME) -BUILD = os.path.join(WORK, 'build', NAME) -RESOLVED = os.path.join(OVER, 'watchface', 'fprj', 'app', 'lua', 'main.lua') -LOGLINES = [] - - -def log(s): - print(s) - LOGLINES.append(s) - - -def write_lua(lua_text): - """把 Lua 写进编译工作区(两遍构建的唯一区别就在这里)。""" - p = os.path.join(BUILD, 'watchface', 'fprj', 'app', 'lua', 'main.lua') - os.makedirs(os.path.dirname(p), exist_ok=True) - open(p, 'w', encoding='utf-8', newline='\n').write(lua_text) - - -def prepare(lua_text, role): - """把官方模板拷成编译工作区,写 config / fprj / preview / lua / 资源。""" - if not os.path.exists(TEMPLATE): - raise SystemExit('找不到官方模板:%s\n请把 LuaDevTemplate-main 放到 vendor/ 下' % TEMPLATE) - if os.path.exists(BUILD): - shutil.rmtree(BUILD) - shutil.copytree(TEMPLATE, BUILD) - old = os.path.join(BUILD, 'watchface', 'fprj', 'LuaDevTemplate.fprj') - if os.path.exists(old): - os.remove(old) - - cfg = {"projectName": TITLE, "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, TITLE, SCREEN, SCREEN) - open(os.path.join(BUILD, 'watchface', 'fprj', NAME + '.fprj'), 'w', - encoding='utf-16', newline='').write(fprj) - - # 预览图 = 真实首页渲染 -> 圆形 -> 326x326(Compiler.exe 对 462 只收这个尺寸) - prev = os.path.join(OVER, 'watchface', 'fprj', 'images', 'preview.png') - OV.make_preview(prev) - shutil.copyfile(prev, os.path.join(BUILD, 'watchface', 'fprj', 'images', 'preview.png')) - - write_lua(lua_text) - - imgdir = os.path.join(BUILD, 'watchface', 'fprj', 'app', 'images') - for sub, src in (('pay', os.path.join(PROJ, 'blob_' + role)), - ('ui', os.path.join(PROJ, 'ui_images'))): - if not os.path.isdir(src): - raise SystemExit('缺少 %s/ —— 先跑 tools/s5e_payload.py %s' % (src, role)) - 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)) - - -def run_build(): - ps = os.path.join(BUILD, 'scripts', 'build_face.ps1') - # 显式给 -FaceName:vendor 脚本默认用 config 的 projectName 当输出名,而 projectName 现在 - # 是**显示名**(S5e Pt.2),交付文件名必须还是 NAME.face。 - r = subprocess.run(['powershell', '-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', ps, - '-FaceName', NAME + '.face'], - capture_output=True, text=True, errors='replace') - workspace_fprj() # vendor 可能把 .fprj 改名成显示名,编完改回 NAME.fprj - out = ((r.stdout or '') + (r.stderr or '')).splitlines() - for l in [x for x in out if x.strip()][-4:]: - log(' | ' + l) - face = os.path.join(BUILD, 'bin', NAME + '.face') - if not os.path.exists(face): - raise SystemExit('编译失败:没有产出 %s' % face) - return open(face, 'rb').read() - - -def workspace_fprj(): - """把工作区里的 .fprj 归一成 NAME.fprj,并返回它的路径。 - - 我们写下去时就是 NAME.fprj,但 vendor 的 build_face.ps1 会先跑 - sync_watchface_config.ps1:它按 config 的 projectName(现在是**显示名** S5e Pt.2)找 - .fprj,找不到就把唯一那份改名过去(Title 也被改写成 projectName)。 - 编完这里再改回 NAME.fprj —— 工作区里始终只有一份 .fprj(放两份会被 Compiler.exe 并成 - 双倍资源,实测资源整体翻倍),文件名也保持项目标识不变。 - """ - d = os.path.join(BUILD, 'watchface', 'fprj') - fs = sorted(f for f in os.listdir(d) if f.endswith('.fprj')) - if len(fs) != 1: - raise SystemExit('watchface/fprj 下的 .fprj 应恰好 1 份,实际:%s' % fs) - want = os.path.join(d, NAME + '.fprj') - if fs[0] != NAME + '.fprj': - os.replace(os.path.join(d, fs[0]), want) - return want - - -def compile_raw(): - """裸 Compiler.exe —— 拿**忠实产物**(不经 vendor 的 set_face_id.ps1 改头)。 - - ★ 为什么必须多这一步(b5=10 的根因) - run_build() 走的是 vendor 的 build_face.ps1,它编完之后会调 - scripts/internal/set_face_id.ps1,那个脚本里有一句 - $bytes[5] = [byte]$faceIdSize # = 10 - 也就是把「id 字符串长度」写进了头里第 5 字节。固件自带表盘的 b5 是 0, - 交付物照着 0 走;所以最终产物必须取自**裸编译输出**(b5 由编译器给,实测 0), - 然后只把 40..49 覆盖成我们的 id,b5 一个字节都不碰。 - (s5e-icons 的 s5e_build.py 就是 compile_raw() + 补 id 这一套,真机验证过。) - """ - exe = os.path.join(BUILD, 'watchface', 'tools', 'Compiler.exe') - fprj = workspace_fprj() - out = os.path.join(BUILD, 'raw') - if os.path.exists(out): - shutil.rmtree(out) - os.makedirs(out) - r = 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('裸编译失败:%s' % ((r.stdout or '') + (r.stderr or ''))[-400:]) - 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 - - -UI_NAMES = ['back', 'ic_help', 'ic_replace', 'ic_about', 'ic_check', 'ic_rollback', - 'ic_qq', 'logo'] - - -def locate(face, blobs, pay_blob, fp_blob): - """量出 UI 图片 / 负载 / 指纹在 .face 里的偏移,并逐条字节校验。""" - ui, bad = [], 0 - for name in UI_NAMES: - blob = open(os.path.join(PROJ, 'ui_images', name + '.bin'), 'rb').read() - h = hits(face, blob) - if len(h) != 1: - log(' !! UI %s 命中 %d 次(应为 1)' % (name, len(h))) - bad += 1 - if h: - ui.append((h[0], len(blob), name)) - if bad: - raise SystemExit('有 UI 资源无法唯一定位') - ml, mlf = blobs['magic_len'], blobs['magic_fp_len'] - if face.count(pay_blob[:ml]) != 1: - raise SystemExit('payload 魔数命中 %d 次(应为 1)' % face.count(pay_blob[:ml])) - if face.count(fp_blob[:mlf]) != 1: - raise SystemExit('fp 魔数命中 %d 次(应为 1)' % face.count(fp_blob[:mlf])) - pay_base = face.find(pay_blob) - fp_base = face.find(fp_blob) - if pay_base < 0: - raise SystemExit('payload 不是原样连续存放') - if fp_base < 0: - raise SystemExit('fp 不是原样连续存放') - # 用生成负载时的 sha256 逐条独立校验(避免"自己比自己"这种恒真检查) - for tag, base, path, key, mag in ( - ('pay', pay_base, os.path.join(PROJ, 'blob_' + ROLE, 'payload.bin'), - 'pay_sha', ml), - ('fp', fp_base, os.path.join(PROJ, 'blob_' + ROLE, 'fp.bin'), 'fp_sha', mlf)): - data = open(path, 'rb').read() - if face.find(data) != base: - raise SystemExit('%s 文件不是原样连续存放' % tag) - for i, r in enumerate(blobs['records']): - res = base + mag + (r['rel'] if tag == 'pay' else r['fprel']) - ln = r['length'] if tag == 'pay' else r['fplen'] - seg = face[res:res + ln] - if len(seg) != ln or hashlib.sha256(seg).hexdigest() != r[key]: - raise SystemExit('%s[%d] 在 0x%x 处字节不吻合' % (tag, i, res)) - 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']] - return ui, pay, rb, pay_base, fp_base - - -def main(): - tpl = open(SRC, encoding='utf-8').read() - for ph in ('__UIIMG__', '__PAY__', '__RB__', '__ROLE__'): - if ph not in tpl: - raise SystemExit('main.lua 里没有占位符 %s —— 它应该是源码模板' % ph) - log('=== %s(role=%s,id=%s)===' % (NAME, ROLE, WF_ID)) - blobs = json.load(open(os.path.join(PROJ, 'blob_' + ROLE, 'offsets.json'), - encoding='utf-8')) - for r in blobs['records']: - if 'pay_sha' not in r or 'fp_sha' not in r: - raise SystemExit('offsets.json 里没有 sha256 —— 请重跑 tools/s5e_payload.py %s' % ROLE) - 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'] - - log('第 1 遍:用占位符编一次,量出资源在 .face 里的位置') - prepare(tpl, ROLE) - face1 = run_build() - log(' face %d bytes' % len(face1)) - ui, pay, rb, pay_base, fp_base = locate(face1, blobs, pay_blob, fp_blob) - for o, l, n in ui: - log(' %-12s off=0x%06x len=%d' % (n, o, l)) - log(' payload base=0x%06x(%d B) / fp base=0x%06x(%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) - 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 - assert '__PAY__' not in resolved and '__RB__' not in resolved - os.makedirs(os.path.dirname(RESOLVED), exist_ok=True) - open(RESOLVED, 'w', encoding='utf-8', newline='\n').write(resolved) - log('第 2 遍:回填后的 Lua(%d bytes)重新编译' % len(resolved)) - # ★ 第 1 遍编的是**占位符版**;第 2 遍必须把回填后的 Lua 写回编译工作区再编, - # 否则交付物里还是 __PAY__/__RB__/__ROLE__(表盘在设备上直接"负载表为空")。 - write_lua(resolved) - face2 = run_build() - - # ---- 0 漂移校验 ---- - drift = 0 - ui2, pay2, rb2, pb2, fb2 = locate(face2, blobs, pay_blob, fp_blob) - if ui2 != ui: - log(' !! UI 偏移漂移'); drift += 1 - if (pb2, fb2) != (pay_base, fp_base): - log(' !! 负载偏移漂移'); drift += 1 - if (pay2, rb2) != (pay, rb): - log(' !! 负载表内容漂移'); 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 _n, x in files)) - if ok != len(pay): - log(' !! pay 只有 %d/%d 条与 %s/ 吻合' % (ok, len(pay), RECDIR)); drift += 1 - log('偏移校验:%s' % ('全部吻合,0 漂移' if drift == 0 else '!! %d 处异常' % drift)) - - # ---- 最终产物:id 必须逐字节正确(b5=0,40..49 = id + NUL)---- - # 交付物取**裸编译**的忠实产物:b5 保持编译器给的值(实测 0),只覆盖 id 那 10 字节。 - # (vendor 的 build_face.ps1 会顺手把 b5 写成 id 长度 10,不能拿它当交付物。) - raw = compile_raw() - if raw[0:4] != b'\x5a\xa5\x34\x12': - raise SystemExit('bad face magic') - nd = [i for i in range(min(len(raw), len(face2))) if raw[i] != face2[i]] - stray = [i for i in nd if not (i == 5 or 40 <= i < 50)] - if len(raw) != len(face2) or stray: - raise SystemExit('忠实产物与 toolchain 产物不是"只差 b5 + id":' - 'len %d vs %d,差异 %d 字节,越界位置 %s' - % (len(raw), len(face2), len(nd), stray[:8])) - if raw[5] != 0: - raise SystemExit('裸编译产物的 b5 = %d(期望 0)—— 设备表盘的约定变了,得重查' % raw[5]) - d = bytearray(raw) - d[40:50] = WF_ID.encode() + b'\x00' * (10 - len(WF_ID)) - os.makedirs(DIST, exist_ok=True) - out = os.path.join(DIST, NAME + '.face') - open(out, 'wb').write(bytes(d)) - a32, = struct.unpack('8 且离圆心 >240 的像素都算越界。 -""" -import os, sys, math -sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) -import s5e_render_ui as R -from s5e_pages import PAGES - -W = H = 480 -CX = CY = 240.0 -RAD = 240.0 - -print('%-10s %8s %11s worst point' % ('page', 'max r', 'outside px')) -print('-' * 62) -bad = 0 -for name, items in PAGES.items(): - im = R.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: - 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('-' * 62) -print('圆心 (240,240) 半径 240 画布 480x480 越界页数:%d' % bad) -if R.OVERFLOW: - print('!! 文本溢出框 %d 处:' % len(R.OVERFLOW)) - for o in R.OVERFLOW: - print(' ', o) -sys.exit(1 if (bad or R.OVERFLOW) else 0) diff --git a/s5e-pressure-anim/tools/s5e_lua_static.py b/s5e-pressure-anim/tools/s5e_lua_static.py deleted file mode 100644 index adab1d0..0000000 --- a/s5e-pressure-anim/tools/s5e_lua_static.py +++ /dev/null @@ -1,145 +0,0 @@ -# -*- coding: utf-8 -*- -"""main.lua 的静态体检 —— 编译前把「真机上才会炸」的几类错抓出来。 - -S4e 那版的真机事故是:`segs/ddWrite/ddRead` 被放在 `slurp/sh` **之前**, -于是函数体里引用的 `sh` 不是那个 local,而是**全局 nil**,一调用就 -`attempt to call a nil value (global 'sh')` —— 而且当时整条流水线全绿 -(Compiler.exe 只打包 Lua、不解析它)。所以本文件专门查: - - ① 块配平(function/if/for/while/do/end/repeat/until) - ② **先用后声明**:某个名字在文件里是 `local`,但第一次调用发生在它声明之前 - ③ 调用了既不是 local、也不在 Lua 标准库白名单里的名字(拼写错 / 忘了 local) - ④ 模板约定:4 个占位符各 1 次、`--[[` 只在文件头、以 `return ui\\n` 结尾 - (tools/s5e_verify.py 会从 .face 里反解 Lua,这几条是它定位 Lua 的前提) - -用法:python tools/s5e_lua_static.py [main.lua] -""" -import os, re, sys -sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) -from paths import PROJ -from lua_balance import strip - -LUAC = os.path.join(PROJ, 'main.lua') - -# Lua 5.4 标准库 + 本项目用到的宿主全局(都不是拼写错) -WHITELIST = set(""" -require pcall xpcall type pairs ipairs next select tostring tonumber rawget rawset -rawequal rawlen setmetatable getmetatable error assert print unpack load -string table math io os coroutine utf8 debug collectgarbage -""".split()) -KEYWORDS = set(""" -and break do else elseif end false for function goto if in local nil not or -repeat return then true until while -""".split()) -REQUIRED_PH = ('__UIIMG__', '__PAY__', '__RB__', '__ROLE__') - -fail = 0 - - -def bad(msg): - global fail - fail += 1 - print(' FAIL ' + msg) - - -def ck(cond, msg): - print((' OK ' if cond else ' FAIL ') + msg) - if not cond: - global fail - fail += 1 - - -def lineno(src, idx): - return src.count('\n', 0, idx) + 1 - - -def decls(src, code): - """名字 -> 它在 code 里第一次被 local 声明(或作为函数参数出现)的位置。""" - out = {} - for m in re.finditer(r'\blocal\s+function\s+([A-Za-z_]\w*)', code): - out.setdefault(m.group(1), m.start(1)) - for m in re.finditer(r'\blocal\s+([A-Za-z_][\w,\s]*?)\s*(?:=|$)', code, re.M): - base = m.start(1) - for piece in m.group(1).split(','): - nm = piece.strip() - if re.fullmatch(r'[A-Za-z_]\w*', nm): - out.setdefault(nm, base) - # 函数参数也是 local(`function(a, b)` / `function f(a)`),否则会误报 - for m in re.finditer(r'\bfunction\b\s*[A-Za-z_.:]*\s*\(([^)]*)\)', code): - for piece in m.group(1).split(','): - nm = piece.strip() - if re.fullmatch(r'[A-Za-z_]\w*', nm): - out.setdefault(nm, m.start()) - return out - - -def calls(code): - """所有函数调用点(排除 `function NAME(` 这种定义)。""" - out = [] - for m in re.finditer(r'(? /dev/health:%s' % (m.group(1).strip() if m else None)) - ck("local DEV = DEVS.health" in src, - 'DEV 来自 DEVS.health(表里的键名真的被用于取节点,不是摆设)') - paytbl = re.search(r'local PAYTBL = \{\n__PAY__\n__RB__\n\}', src) - ck(paytbl is not None, 'PAYTBL 里只有 __PAY__ / __RB__ 两处占位符(没有硬编码节点名)') - ck('466' not in src, '没有残留的 466(旧 S4e 尺寸)') - - print('\n=== %s ===' % ('全部通过' if not fail else '%d 项失败' % fail)) - return 1 if fail else 0 - - -if __name__ == '__main__': - sys.exit(main()) diff --git a/s5e-pressure-anim/tools/s5e_overlay.py b/s5e-pressure-anim/tools/s5e_overlay.py deleted file mode 100644 index 62c2e67..0000000 --- a/s5e-pressure-anim/tools/s5e_overlay.py +++ /dev/null @@ -1,84 +0,0 @@ -# -*- coding: utf-8 -*- -"""为官方 LuaDevTemplate 生成可编译的 overlay。 - -两份盘(role): - replace -> S5ePAnim 负载 = q63 的帧 - restore -> S5ePAnimRestore 负载 = 原厂字节 -两份盘**共用同一个 watchface id**(462150102):装一份就顶掉另一份,设备上只占一份空间。 - -要点(都是踩过的坑): - * preview.png 必须是 326x326 —— Compiler.exe 对 DeviceType 462 只收这个尺寸 - * fprj 必须是 UTF-16(带 BOM)且用 CRLF,否则 Compiler.exe 直接拒绝 - * 表盘界面是 480x480(Screen Width/Height),预览图只是表盘列表里的缩略图 - * 负载只放 blob_/ 那一份「合并后的单条目」 -""" -import os, sys, json, shutil -from PIL import Image -from paths import PROJ, WORK, SCREEN, PREVIEW, WF_ID, DEVICE_TYPE -from s5e_pages import PAGES -import s5e_render_ui as R - -ROLE = (sys.argv[1] if len(sys.argv) > 1 else 'replace').lower() -assert ROLE in ('replace', 'restore'), ROLE -NAME = 'S5ePAnim' if ROLE == 'replace' else 'S5ePAnimRestore' # 路径/标识(不要改) -TITLE = 'S5e Pt.2' # 手表上显示的表盘名(两份盘同一个名字)= fprj 的 -OUT = os.path.join(WORK, 'overlay') -ENTRY_WIDGET = 'app_lua%2Fmain.lua' - - -def make_preview(path): - """480 首页 -> 圆形 alpha -> 326x326(Compiler.exe 只收这个尺寸)。""" - R.W = R.H = SCREEN - img = R.render_raw(PAGES['home']) - base = Image.new('RGBA', (SCREEN, SCREEN), (0, 0, 0, 255)) - base.alpha_composite(img) - mask = Image.new('L', (SCREEN, SCREEN), 0) - from PIL import ImageDraw - ImageDraw.Draw(mask).ellipse([0, 0, SCREEN - 1, SCREEN - 1], fill=255) - base.putalpha(mask) - os.makedirs(os.path.dirname(path), exist_ok=True) - base.resize((PREVIEW, PREVIEW), Image.LANCZOS).save(path) - - -def write_fprj(path, title): - xml = ('\r\n' - '\r\n' - ' \r\n' - ' \r\n' - ' \r\n\r\n' - % (DEVICE_TYPE, title, ENTRY_WIDGET, SCREEN, SCREEN)) - 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": TITLE, "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, name + '.fprj'), TITLE) - 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 = os.path.join(PROJ, 'blob_' + role) - if not os.path.isdir(blob): - raise SystemExit('缺少 blob_%s/ —— 先跑 tools/s5e_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/s5e-pressure-anim/tools/s5e_pages.py b/s5e-pressure-anim/tools/s5e_pages.py deleted file mode 100644 index 1ab8225..0000000 --- a/s5e-pressure-anim/tools/s5e_pages.py +++ /dev/null @@ -1,150 +0,0 @@ -# -*- coding: utf-8 -*- -"""Xiaomi Watch S5 eSIM (p62lte) 480x480 页面规格。 - -坐标系就是 `main.lua` 用的那一套;`s5e_render_ui.py` / `s5e_check_clip.py` 画的是同一份规格, -所以预览图是"诚实"的(不是另画一套)。与 S5 图标包那份 480 设计稿同一套语言, -只是页面文案改成压力动画替换器。 -""" -import os - -from paths import PROJ, DOCS, ASSETS -W = H = 480 -BG = (0, 0, 0) -GRAY = '999999' -WHITE = 'ffffff' -PLATE = '002927' -CIRCLE = '4f4f4f' -OK = '43cf7c' -ERR = 'd43030' -S600, S500, S400 = 'semibold', 'medium', 'regular' - -# 列表项几何 -# radius=40 不是审美选择:y=88 那一行离圆心 152px,圆屏在那里的弦半宽只有 √(240²-152²)=185.8, -# 也就是该行 x 只能在 54.2..425.8 之内。底板是 x=50 w=379,圆角不够大时四个角会越过圆边 -# (r=35 时角点半径 240.7 → check_clip 记为越界)。r=40 正好把角收进去。 -ITEM_X, ITEM_W, ITEM_H, ITEM_R = 50, 379, 91, 40 -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, 42 -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=40, 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=42, 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), - ] - - -# ------------------------------------------------------------------ 页面 -# 所有文字的像素宽度都用真 MiSans 量过(tools/s5e_render_ui.py 同一套字体): -# 31px: 'S5e Pt.2'=125 '快速帮助'/'开始替换'/'正在替换'=124 '关于'=62 -# 29px: '确认替换'/'重启手表'=116 '环境预检查'=145 '732681995'=155 -# 25px: '检查 health 分区与负载完整性。'=369 '恢复 119 帧动画的原厂样式。'=331 -# '预检查通过 | 119 条 | 15.49MB'=349 'Xiaomi Watch S5 eSIM 46mm'=367 -# 圆屏在高度 y 处的弦半宽 = √(240² - (y-240)²),所以**越靠下的行越窄** —— -# 型号那行原来放在 y=229(弦半宽只有 171),'Xiaomi Watch S5 eSIM 46mm' 宽 367 直接被切掉。 -# 现在把该行拆成两行、整体上移到 y=195 / 228,两行都短于那里的弦。 -PAGE_HOME = [clock(), title('S5e Pt.2', 177, 127), - *item(88, 'ic_help', '快速帮助'), - *item(186, 'ic_replace', '开始替换'), - *item(285, 'ic_about', '关于')] - -_HELP_HEAD = [clock(), back_arrow(167), title('快速帮助', 188, 124)] -# 说明文字必须**单行**放下(折行后第 2 行会越出圆屏,check_clip 抓过): -# y=182 处圆屏弦半宽 = √(240²-58²) = 232.9,x 从 64 起时最多能到 64+232.9 = 296.9... -# 实际用 380 的框宽是安全的(文字宽 354px < 380,且落在 64..444 内)。 -_HELP_D1 = dict(kind='label', x=64, y=182, w=380, h=35, - text='检查 health 分区与负载完整性。', size=24, - weight=S500, color=GRAY, align='left', vtop=True) -_HELP_I2 = item(264, 'ic_rollback', '回滚修改') -_HELP_D2 = dict(kind='label', x=67, y=360, w=348, h=35, - text='恢复 119 帧动画的原厂样式。', size=25, - weight=S500, color=GRAY, align='left', vtop=True) -PAGE_HELP = _HELP_HEAD + item(88, 'ic_check', '环境预检查') + [_HELP_D1] + _HELP_I2 + [_HELP_D2] - -# 预检查结果:最多 2 行(24px 行高 33,框高 76 >= 2*33) -HELP_DESC_OK = dict(kind='label', x=67, y=182, w=348, h=76, - text='预检查通过 | 119 条 | 15.49MB\nhealth 分区可读。', - size=24, 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=24, 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] - -# 警告页:每行单独一个框(h=34 ≈ 25px 真行高 35),行距 34。 -# 最下面那行放在 y=341:圆屏在 y=375 处正好收成一点,所以底边必须留在 375 以上。 -WARN_LABELS = [ - dict(kind='label', x=60, y=95, w=360, h=35, - text='替换压力检测动画之前,', size=25, weight=S500, color=GRAY, - align='left', vtop=True), - dict(kind='label', x=60, y=129, w=360, h=35, - text='请先确认自己的系统版本号为 ', size=25, weight=S500, color=GRAY, align='left', vtop=True), - dict(kind='label', x=304, y=129, w=120, h=35, text='3.112.035', - size=25, weight=S500, color=WHITE, align='left', vtop=True), - dict(kind='label', x=422, y=129, w=40, h=35, text='!', - size=25, weight=S500, color=GRAY, align='left', vtop=True), - # 型号拆成两行:y=195 处弦半宽 195,y=228 处 173 —— 367px 的长行只能拆 - dict(kind='label', x=64, y=195, w=360, h=35, text='请确认自己的手表型号为', - size=25, weight=S500, color=GRAY, align='left', vtop=True), - dict(kind='label', x=64, y=229, w=360, h=35, text='Xiaomi Watch S5', - size=25, weight=S500, color=WHITE, align='left', vtop=True), - dict(kind='label', x=64, y=263, w=360, h=35, text='eSIM 46mm(p62lte)', - size=25, weight=S500, color=WHITE, align='left', vtop=True), - dict(kind='label', x=60, y=341, w=360, h=35, 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' - '> health 119 条\n' - '> 写入 60/119 成 60 失 0\n' - '> 替换完毕,请重启手表。\n' - '> 若发现问题,请回滚修改。') -PAGE_RUNNING = [clock(), title('正在替换', 178, 124), - dict(kind='label', x=67, y=95, w=348, h=280, text=LOG_MID, size=24, - 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=181, y=200, w=120, h=40, text='S5e Pt.2', - size=29, weight=S500, color=WHITE, align='left', vtop=True), - dict(kind='label', x=222, y=237, 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-pressure-anim/tools/s5e_payload.py b/s5e-pressure-anim/tools/s5e_payload.py deleted file mode 100644 index 3e65f6b..0000000 --- a/s5e-pressure-anim/tools/s5e_payload.py +++ /dev/null @@ -1,128 +0,0 @@ -# -*- coding: utf-8 -*- -"""按「角色」切负载:replace(只负责替换)/ restore(只负责还原)。 - -为什么拆成两张盘 ----------------- -「负载 + 回滚」合在一张盘 = 两份 15.5 MB ≈ 31 MB,超过 .face 的体积上限。 -拆开之后每张盘只装一份负载(≈15.5 MB)。 - -拆盘后版本校验怎么办 --------------------- -每张盘再带一张**指纹表**:每条记录存「另一侧字节的前 64 B」。 - * replace 盘:pay = q63 的帧(新字节);fp = **原厂字节**前 64 B - * restore 盘:pay = 原厂字节; fp = **新字节**前 64 B -于是「写入前版本校验」和「写入后回读比对」两道保险都还在,代价只有 119*64 ≈ 7.4 KB。 - -关键约定(S4e 那版踩过的坑) ----------------------------- -Lua 表里存的是**分区键名**('health'),运行期才由 `DEVS[键]` 拼成 `/dev/health`。 -若把 '/dev/health' 直接存进表里,拼出来就是 `if=nil` 或 `if=/dev//dev/health`。 - -产出 ----- - payload/ NNN_.bin + payload.lua (replace 盘的负载) - payload_restore/ NNN_.bin + payload.lua (restore 盘的负载) - blob_replace/ payload.bin(魔数 + 顺序拼接)+ fp.bin + offsets.json - blob_restore/ 同上 - payload.json 机器可读清单 -""" -import os, sys, json, hashlib, struct -sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) -from paths import PROJ, WORK, TARGET_IMAGE - -ROLE = (sys.argv[1] if len(sys.argv) > 1 else 'replace').lower() -assert ROLE in ('replace', 'restore'), ROLE -PATCHED = os.path.join(WORK, 'anim', 'vela_health_anim.bin') -OUT = PROJ -DEV = 'health' # ← Lua 表里的键名(不是 '/dev/health') -FP_LEN = 64 - -recs_in = json.load(open(os.path.join(WORK, 'anim', 'manifest.json'), encoding='utf-8')) -stock = open(TARGET_IMAGE, 'rb').read() -patched = open(PATCHED, 'rb').read() -assert len(stock) == len(patched), '改后镜像长度变了 —— 等长覆盖的前提被破坏' -assert stock[:8] == b'-rom1fs-' - -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) - -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]), - fp=OTHER[off:off + FP_LEN])) -# replace 盘跳过「本来就没变」的记录(省体积);restore 盘一条都不能少 -emitted = [r for r in records if (r['changed'] or ROLE == 'restore')] -dropped = [r for r in records if r not in emitted] -for r in emitted: - assert len(r['data']) == r['length'], r['name'] - assert len(r['fp']) == FP_LEN, r['name'] - - -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/s5e_payload.py (%s) -- do not hand-edit\n' % ROLE) - f.write('-- dd: -if / -of /dev/%s bs=4 seek=offset/4 count=length/4\n' % DEV) - 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) - -# 魔数必须全局唯一,且**长度不要手写常量**(S4e/S5e 图标包都因为手写成 16 导致偏移差 1) -MAGIC_PAY = b'\x00S5P-PAYv1\x00\xa5\x5a\xc3\x3c' -MAGIC_FP = b'\x00S5P-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: - # 每条都存独立 sha256:.face 构建时用它校验"确实原样连续存放", - # 而不是拿 .face 跟自己比(那种检查恒真、抓不到 bug) - rel.append(dict(name=r['name'], rel=off, fprel=fpoff, target=r['offset'], - length=r['length'], fplen=len(r['fp']), - pay_sha=hashlib.sha256(pay_blob[len(MAGIC_PAY) + off: - len(MAGIC_PAY) + off + r['length']]).hexdigest(), - fp_sha=hashlib.sha256(fp_blob[len(MAGIC_FP) + fpoff: - len(MAGIC_FP) + fpoff + len(r['fp'])]).hexdigest())) - off += r['length'] - fpoff += len(r['fp']) -assert fpoff == len(fp_blob) - len(MAGIC_FP), (fpoff, len(fp_blob)) -assert off == len(pay_blob) - len(MAGIC_PAY), (off, len(pay_blob)) -json.dump(dict(role=ROLE, device=DEV, 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), - dropped_identical=[r['name'] for r in dropped]), - 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 条记录,跳过 %d 条未变)' - % (ROLE, len(pay_blob), len(fp_blob), len(rel), len(dropped))) - -json.dump(dict(device='Xiaomi Watch S5 eSIM 46mm (p62lte)', target='/dev/' + DEV, dev_key=DEV, - partition='vela_health.bin', role=ROLE, kind='pressure-measure-anim', - dd='dd if=/ of=/dev/%s bs=4 seek= count=' % DEV, - 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/s5e-pressure-anim/tools/s5e_render_ui.py b/s5e-pressure-anim/tools/s5e_render_ui.py deleted file mode 100644 index 94b479d..0000000 --- a/s5e-pressure-anim/tools/s5e_render_ui.py +++ /dev/null @@ -1,196 +0,0 @@ -"""按 480x480 页面规格渲染预览图(真 MiSans 字体 + 真素材 + 圆形遮罩)。 - -支持 kind: rect / circle / image / label -label 支持 align / vtop / vcenter / lh,以及 LVGL 的 recolor 写法 "#RRGGBB 文字#" -""" -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 = 480 -_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): - 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): - 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 = [] # (文本前 14 字, 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) - 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_raw(items): - """画成 RGBA(不遮罩、不铺黑底)—— 切边检查就用这个。""" - 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) - 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': - 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) - return img - - -def render(items, out, mask=True, ring=False): - img = render_raw(items) - 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)) - os.makedirs(os.path.dirname(out), exist_ok=True) - base.save(out) - return out - - -if __name__ == '__main__': - from s5e_pages import PAGES - outs = [] - for name, items in PAGES.items(): - p = os.path.join(ROOT, 'docs', 'ui_%s.png' % name) - 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): - sheet.paste(Image.open(p), (12 + (i % cols) * (W + 12), 12 + (i // cols) * (H + 12))) - sp = os.path.join(ROOT, 'docs', 'ui_all.png') - sheet.save(sp) - print('wrote', sp, sheet.size) - if OVERFLOW: - print('!! 文本溢出 %d 处:%s' % (len(OVERFLOW), OVERFLOW)) diff --git a/s5e-pressure-anim/tools/s5e_ui_images.py b/s5e-pressure-anim/tools/s5e_ui_images.py deleted file mode 100644 index c8087ec..0000000 --- a/s5e-pressure-anim/tools/s5e_ui_images.py +++ /dev/null @@ -1,44 +0,0 @@ -# -*- coding: utf-8 -*- -"""UI 素材 PNG -> LVGL v9 .bin(480x480 表盘用)+ 80px 圆形 logo。 - -格式(真机验证过):12 B 头 magic=0x19|cf|flags|w|h|stride|rsv + BGRA 像素。 -编码器复用 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 - -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)) -os.makedirs(DOCS, exist_ok=True) -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-pressure-anim/tools/s5e_verify.py b/s5e-pressure-anim/tools/s5e_verify.py deleted file mode 100644 index d4c4dd9..0000000 --- a/s5e-pressure-anim/tools/s5e_verify.py +++ /dev/null @@ -1,221 +0,0 @@ -# -*- coding: utf-8 -*- -"""独立校验交付的两张 .face —— 只读交付物 + 固件镜像,不信任任何中间产物。 - - role=replace -> dist/S5ePAnim.face pay 应等于**改后镜像**的字节,rb 应等于**原厂**前 64 B - role=restore -> dist/S5ePAnimRestore.face pay 应等于**原厂**字节, rb 应等于**改后**前 64 B - -查的东西(缺一不可): - ① .face 头:magic / b4 / b5 / id / 预览块尺寸 326x326 - ② 从 .face 里**反解**加载的 Lua 源码:ROLE 正确、占位符已回填、 - 分区键是 'health'(不是 '/dev/health',那样运行时会拼成 if=nil) - ③ pay / rb 表逐条:目标偏移 == 原厂槽位 data_off、长度 == 原槽位长度(等长覆盖的底线) - ④ 每条 pay 长度 == 原槽位长度,且能按 LVGL RLE 解出**正好 usize 字节**、解成 464x464 - ⑤ 每条 pay 字节 == 另一侧镜像对应位置;每条 rb 指纹 == 另一侧前 64 B - ⑥ 改后镜像的 inode 表 / superblock / 分区长度与原厂**逐位一致** - ⑦ 负载在 .face 里原样连续存放(偏移 == 文件基址 + 魔数长度 + 相对偏移 的前提) -""" -import os, re, sys, json, struct, hashlib -sys.stdout.reconfigure(encoding='utf-8', errors='replace') -sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) -from paths import (PROJ, INPUTS, WORK, DIST, TARGET_IMAGE, DIRP, FACE_W, FACE_H, - WF_ID, PREVIEW, SCREEN) -from romfs import Romfs -import lvgl_bin as L - -ROLE = (sys.argv[1] if len(sys.argv) > 1 else 'replace').lower() -assert ROLE in ('replace', 'restore'), ROLE -NAME = 'S5ePAnim' if ROLE == 'replace' else 'S5ePAnimRestore' -FACE = os.path.join(DIST, NAME + '.face') -PATCHED = os.path.join(WORK, 'anim', 'vela_health_anim.bin') -FP_LEN = 64 -ok = fail = 0 - - -def ck(cond, msg): - global ok, fail - if cond: - ok += 1 - print(' OK ' + msg) - else: - fail += 1 - print(' FAIL ' + msg) - - -print('=' * 78) -print('%s(role=%s) %s' % (NAME, ROLE, FACE)) -print('=' * 78) -if not os.path.exists(FACE): - raise SystemExit('交付物不存在:%s' % FACE) -face = open(FACE, 'rb').read() -stock_b = open(TARGET_IMAGE, 'rb').read() -patch_b = open(PATCHED, 'rb').read() -stock = Romfs(TARGET_IMAGE) -patched = Romfs(PATCHED) - -# ---------------------------------------------------------------- ① 头 -print('\n[1] .face 头') -ck(face[0:4] == b'\x5a\xa5\x34\x12', 'magic 5a a5 34 12') -ck(face[4] in (0, 1, 2), 'b4 = %d' % face[4]) -ck(face[5] == 0, 'b5 = %d(固件自带表盘 b5 也是 0)' % face[5]) -ck(face[40:50] == WF_ID.encode() + b'\x00', 'id = %r(期望 %r)' % (face[40:50], WF_ID)) -a32, = struct.unpack(' 0, '找到 Lua 结尾 "return ui\\n"') -_start = face.rfind(b'--[[', 0, _end) -lua = face[_start:_end + len(b'return ui\n')] -ck(lua.startswith(b'--[[--') and b'local UIIMG = {' in lua, 'Lua 提取成功(%d B)' % len(lua)) -txt = lua.decode('utf-8') -ck(face.count(lua) == 1, 'Lua 原样出现 1 次(偏移 0x%x)' % face.find(lua)) -c = re.search(r"local ROLE = '(\w+)'", txt) -ck(c is not None and c.group(1) == ROLE, "Lua 里的 ROLE == '%s'" % ROLE) -ck('__UIIMG__' not in txt and '__PAY__' not in txt and '__RB__' not in txt - and '__ROLE__' not in txt, '占位符已全部回填') -ck(re.search(r'local W, H = %d, %d' % (SCREEN, SCREEN), txt) is not None, - '界面尺寸 W,H = %d,%d(480x480 圆屏)' % (SCREEN, SCREEN)) -# ★ S4e 那版踩过的坑:Lua 表里存 '/dev/app',运行时又拼一次 -> if=nil -mdev = re.search(r'local DEVS = \{([^}]*)\}', txt) -ck(mdev is not None and '/dev/health' in mdev.group(1), - 'DEVS 里有 health -> /dev/health:%s' % (mdev.group(1).strip() if mdev else None)) - -# ---------------------------------------------------------------- 偏移(自己找,不信 Lua) -print('\n[3] 资源偏移(独立在 .face 里搜字节,不信 Lua 表)') -ML_PAY = b'\x00S5P-PAYv1\x00\xa5\x5a\xc3\x3c' -ML_FP = b'\x00S5P-FPv1\x00\x5a\xa5\x3c\xc3' -offs = 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() -ck(face.count(ML_PAY) == 1, 'payload 魔数(%d B)在 .face 里唯一' % len(ML_PAY)) -ck(face.count(ML_FP) == 1, 'fp 魔数(%d B)在 .face 里唯一' % len(ML_FP)) -pay_base = face.find(ML_PAY) -fp_base = face.find(ML_FP) -ck(face.find(pay_blob) == pay_base, 'payload.bin 原样连续存放在 0x%x' % pay_base) -ck(face.find(fp_blob) == fp_base, 'fp.bin 原样连续存放在 0x%x' % fp_base) -recs = offs['records'] -ck(len(recs) == 119, '负载记录 %d 条(期望 119)' % len(recs)) -# 整个负载文件必须**原样连续**放在 base 处 —— 这是"偏移 == base + 魔数 + rel"成立的前提 -ck(face[pay_base:pay_base + len(pay_blob)] == pay_blob, - 'payload.bin %d B 在 .face 里逐字节连续' % len(pay_blob)) -ck(face[fp_base:fp_base + len(fp_blob)] == fp_blob, - 'fp.bin %d B 在 .face 里逐字节连续' % len(fp_blob)) - -# Lua 表里的偏移必须和独立量出来的一致 -pay = [tuple(int(x) for x in r) for r in - re.findall(r'\{(\d+),(\d+),(\d+)\}', re.search(r'pay = \{(.*?)\n \},', txt, re.S).group(1))] -rb = [tuple(int(x) for x in r) for r in - re.findall(r'\{(\d+),(\d+),(\d+)\}', re.search(r'rb = \{(.*?)\n \},', txt, re.S).group(1))] -ck(len(pay) == len(recs) and len(rb) == len(recs), - 'Lua 表 pay %d 条 / rb %d 条' % (len(pay), len(rb))) -bad_off = 0 -for i, r in enumerate(recs): - if pay[i] != (pay_base + len(ML_PAY) + r['rel'], r['target'], r['length']): - bad_off += 1 - if rb[i] != (fp_base + len(ML_FP) + r['fprel'], r['target'], r['fplen']): - bad_off += 1 -ck(bad_off == 0, 'Lua 表里的 %d 条偏移 == 文件基址 + 魔数长度 + 相对偏移' % len(recs)) - -# ---------------------------------------------------------------- 原厂槽位 -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, i) -ck(len(slots) == 119, '原厂 %s 下 %d 个槽位' % (DIRP, len(slots))) -usizes = {struct.unpack(' 100, '%d/%d 条的"原厂前 64 B"与"改后前 64 B"确实不同(指纹有区分度)' - % (diff_fp, len(recs))) - -# ---------------------------------------------------------------- ⑥ 镜像完整性 -print('\n[6] 改后镜像的完整性') -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 patched.entries()] -ck(a == b, 'inode 表(路径/偏移/next/spec/size/cksum)%d 条逐位不变' % len(a)) -ck(stock.sb_size == patched.sb_size and stock.sb_cksum == patched.sb_cksum, - 'superblock 一致(size %d / cksum %08x)' % (patched.sb_size, patched.sb_cksum)) -ck(stock.volume == patched.volume == 'health', '卷名 health 不变') -ck(len(stock_b) == len(patch_b), '分区长度不变(%d B)' % len(patch_b)) -chg = [i for i in range(len(stock_b)) if stock_b[i] != patch_b[i]] -last_slot_end = max(o + slots[o][1] for o in slots) -ck(chg and chg[0] >= min(slots) and chg[-1] < last_slot_end, - '改动只落在负载槽位区间内(首差 0x%x,末差 0x%x,槽位区间 0x%x..0x%x)' - % (chg[0], chg[-1], min(slots), last_slot_end)) -# 每个槽位都必须真的被改过(不是"看起来写了、其实字节没变") -unchanged = [slots[o][0] for o in slots - if stock_b[o:o + slots[o][1]] == patch_b[o:o + slots[o][1]]] -ck(not unchanged, '119 个槽位全部都真的写入了新字节(未变的槽位:%s)' % (unchanged or '无')) - -# ---------------------------------------------------------------- ⑦ UI 图 -print('\n[7] UI 图片') -uipairs = re.findall(r'\{(\d+),(\d+),"([a-z_]+)"\}', txt) -ck(len(uipairs) == 8, 'UIIMG 条目 %d 条(期望 8)' % len(uipairs)) -badui = 0 -for o, l, n in uipairs: - blob = open(os.path.join(PROJ, 'ui_images', n + '.bin'), 'rb').read() - if int(l) != len(blob) or face[int(o):int(o) + int(l)] != blob: - badui += 1 -ck(badui == 0, '8 张界面素材都与 ui_images/*.bin 逐字节一致') - -# ---------------------------------------------------------------- 体积 -print('\n[8] 体积') -ck(len(face) < 20 * 1048576, '交付物 %.2f MB < 20 MB' % (len(face) / 1048576)) -print(' 两张盘同 id:装一份就顶掉另一份,设备上只占一份空间') - -print('\n' + '=' * 78) -print('%s:%d 项通过,%d 项失败' % (NAME, ok, fail)) -if fail: - sys.exit(1) -print('全部通过 —— %s(%d B,sha256 %s)' - % (os.path.basename(FACE), len(face), hashlib.sha256(face).hexdigest()[:16])) diff --git a/s5e-pressure-anim/tools/s5e_verify_indep.py b/s5e-pressure-anim/tools/s5e_verify_indep.py deleted file mode 100644 index bbb69f2..0000000 --- a/s5e-pressure-anim/tools/s5e_verify_indep.py +++ /dev/null @@ -1,289 +0,0 @@ -# -*- coding: utf-8 -*- -"""独立总校验 —— 只读两张交付物 + 固件镜像,**不读任何构建中间产物**。 - -与 tools/s5e_verify.py 的分工 ------------------------------ -s5e_verify.py 会读 `blob_/offsets.json` 来核对 Lua 表里的偏移。本脚本刻意 -**不读** offsets.json / .work/overlay / .work/build,改为: - - * 自己从 `.face` 里反解 Lua(占位符、ROLE、pay/rb/UIIMG 三张表) - * 自己从**固件镜像**里量出 119 个槽位(名字 / 偏移 / 长度) - * 自己证明两张表「自洽 + 连续」:pay[0] 必须紧跟在魔数之后、pay[i+1] == pay[i]+len - * 自己证明**第 k 条负载落在 Measuring{k}.bin 上**(名字对得上,不只是偏移对得上) - * 逐条把负载解成像素:RLE 解出**恰好 usize**,且多出来的那 1 字节正好在像素区之后 - (这是"少 1 像素整帧不画"的那个坑) - * 两张交付物**互相印证**(replace 的 pay 前 64 B == restore 的 rb,反之亦然)—— - 这一条完全不碰 .work,也不碰固件镜像 - * 镜像级:inode 表 / superblock / 卷名 / 长度逐位不变;除 119 个槽位外, - **其余每一个文件的每一个字节都逐字节不变**(不是只做区间统计) - -用法:python tools/s5e_verify_indep.py -""" -import os, re, sys, hashlib, struct -sys.stdout.reconfigure(encoding='utf-8', errors='replace') -sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) -from paths import PROJ, DIST, TARGET_IMAGE, DIRP, FACE_W, FACE_H, WF_ID, PREVIEW, SCREEN, WORK -from romfs import Romfs -import lvgl_bin as L - -MAGIC_PAY = b'\x00S5P-PAYv1\x00\xa5\x5a\xc3\x3c' -MAGIC_FP = b'\x00S5P-FPv1\x00\x5a\xa5\x3c\xc3' -FACES = {'replace': 'S5ePAnim.face', 'restore': 'S5ePAnimRestore.face'} -PATCHED = os.path.join(WORK, 'anim', 'vela_health_anim.bin') -UI_NAMES = ['back', 'ic_help', 'ic_replace', 'ic_about', 'ic_check', 'ic_rollback', - 'ic_qq', 'logo'] -ok = fail = 0 - - -def ck(cond, msg): - global ok, fail - if cond: - ok += 1 - print(' OK ' + msg) - else: - fail += 1 - print(' FAIL ' + msg) - - -def parse_face(path): - """把一张 .face 反解成:头 + Lua 文本 + 三张表 + 魔数位置。""" - d = open(path, 'rb').read() - end = d.find(b'return ui\n') - start = d.rfind(b'--[[', 0, end) - lua = d[start:end + len(b'return ui\n')] - txt = lua.decode('utf-8') - out = dict(data=d, lua=lua, txt=txt, lua_off=start) - m = re.search(r"local ROLE = '(\w+)'", txt) - out['role'] = m.group(1) if m else None - for key, pat in (('pay', r'pay = \{(.*?)\n \},'), ('rb', r'rb = \{(.*?)\n \},')): - blk = re.search(pat, txt, re.S) - out[key] = ([tuple(int(x) for x in t) - for t in re.findall(r'\{(\d+),(\d+),(\d+)\}', blk.group(1))] - if blk else []) - out['ui'] = re.findall(r'\{(\d+),(\d+),"([a-z_]+)"\}', txt) - out['pay_base'] = d.find(MAGIC_PAY) - out['fp_base'] = d.find(MAGIC_FP) - out['pay_magic_n'] = d.count(MAGIC_PAY) - out['fp_magic_n'] = d.count(MAGIC_FP) - out['lua_n'] = d.count(lua) - return out - - -def slots_of(stock): - """固件镜像里的 119 个槽位:名字 -> (偏移, 长度),并给出名字顺序的列表。""" - sl = {} - 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 - sl[os.path.basename(p)] = (i.data_off, i.size) - return sl - - -def main(): - stock_b = open(TARGET_IMAGE, 'rb').read() - patch_b = open(PATCHED, 'rb').read() - stock = Romfs(TARGET_IMAGE) - patched = Romfs(PATCHED) - sl = slots_of(stock) - exp_name = ['Measuring%d.bin' % k for k in range(1, len(sl) + 1)] - print('=' * 78) - print('独立总校验(不读 blob_*/offsets.json、不读 .work/overlay、不读 .work/build)') - print(' 交付物 : dist/%s + dist/%s' % (FACES['replace'], FACES['restore'])) - print(' 固件 : inputs/target/vela_health.bin %d B(卷名 %s)' - % (len(stock_b), stock.volume)) - print(' 改后 : .work/anim/vela_health_anim.bin %d B' % len(patch_b)) - print('=' * 78) - ck(len(sl) == 119, '固件里 %s 下 %d 个槽位(期望 119)' % (DIRP, len(sl))) - ck(sorted(sl.keys()) == sorted(exp_name), '槽位名字就是 Measuring1..119.bin') - - faces = {} - for role, fn in FACES.items(): - f = parse_face(os.path.join(DIST, fn)) - faces[role] = f - other = patch_b if role == 'replace' else stock_b - print('\n' + '-' * 78) - print('[%s] %s %d B sha256 %s' - % (role, fn, len(f['data']), hashlib.sha256(f['data']).hexdigest())) - print('-' * 78) - - # ---- ① 头 + Lua ---- - d = f['data'] - ck(d[0:4] == b'\x5a\xa5\x34\x12', 'magic') - ck(d[5] == 0, 'b5 = %d(固件自带表盘是 0)' % d[5]) - ck(d[4] == 0, 'b4 = %d' % d[4]) - ck(d[40:50] == WF_ID.encode() + b'\x00', 'id = %r' % d[40:50]) - a32, = struct.unpack(' /dev/health') - ck(len(f['ui']) == 8, 'UIIMG %d 条(期望 8)' % len(f['ui'])) - - # ---- ② 表自洽 + 在文件里连续(完全不看 offsets.json)---- - pay, rb = f['pay'], f['rb'] - ck(len(pay) == 119 and len(rb) == 119, 'pay %d 条 / rb %d 条(期望 119/119)' - % (len(pay), len(rb))) - ck(f['pay_magic_n'] == 1 and f['fp_magic_n'] == 1, - '两个魔数各命中 1 次(payload 0x%x / fp 0x%x)' % (f['pay_base'], f['fp_base'])) - cont = 0 - for i, (o, _t, ln) in enumerate(pay): - if i == 0: - if o != f['pay_base'] + len(MAGIC_PAY): - cont += 1 - elif o != pay[i - 1][0] + pay[i - 1][2]: - cont += 1 - for i, (o, _t, ln) in enumerate(rb): - if i == 0: - if o != f['fp_base'] + len(MAGIC_FP): - cont += 1 - elif o != rb[i - 1][0] + rb[i - 1][2]: - cont += 1 - ck(cont == 0, '两张表的偏移自洽:首条紧跟魔数,之后逐条紧邻(%d 处不连续)' % cont) - ck(all(rb[i][2] == 64 for i in range(len(rb))), 'rb 每条都是 64 B') - - # ---- ③ 第 k 条落在 Measuring{k}.bin 上 ---- - bad_name = bad_len = 0 - for i, (o, t, ln) in enumerate(pay): - if t != sl[exp_name[i]][0]: - bad_name += 1 - if ln != sl[exp_name[i]][1] or rb[i][1] != t: - bad_len += 1 - ck(bad_name == 0, '第 k 条负载的目标偏移 == Measuring{k}.bin 的偏移(%d 处不符)' % bad_name) - ck(bad_len == 0, '每条长度 == 该槽位原厂长度,且 rb 的 target 与 pay 一致(%d 处不符)' - % bad_len) - ck(all(t % 512 == 0 for _o, t, _l in pay), '119 个目标偏移全部 512 对齐') - odd = sum(1 for _o, _t, l in pay if l % 4 != 0) - print(' (%d/119 条长度不是 4 的倍数 -> Lua 侧必须走 bs=512+4+1 分段写,' - '否则退化成 bs=1)' % odd) - - # ---- ④ 负载字节 == 对应镜像;⑤ 解码 ---- - bad_bytes = bad_dec = bad_hdr = 0 - big_e = 0 - for i, (o, t, ln) in enumerate(pay): - blob = d[o:o + ln] - if blob != other[t:t + ln]: - bad_bytes += 1 - if blob == (stock_b if role == 'replace' else patch_b)[t:t + ln]: - big_e += 1 - h = L.parse_header(blob) - if (h is None or h['cf'] != 0x0a or not (h['flags'] & L.FLAG_COMPRESSED) - or (h['w'], h['h']) != (FACE_W, FACE_H) or h['stride'] != FACE_W): - bad_hdr += 1 - continue - _m, cs, us = struct.unpack(' 像素区之后**正好** 1 字节尾巴(usize_tail) - if (len(raw) != us or us != 1024 + FACE_W * FACE_H + 1 - or len(raw) - 1024 - FACE_W * FACE_H != 1): - bad_dec += 1 - continue - try: - img = L.decode_lvgl_bin(blob)[1] - if img is None or img.size != (FACE_W, FACE_H): - bad_dec += 1 - except Exception: - bad_dec += 1 - ck(bad_bytes == 0, '每条负载字节 == %s镜像对应位置(%d 处不符)' - % ('改后' if role == 'replace' else '原厂', bad_bytes)) - ck(big_e == 0, '每条负载都真的与"另一侧"不同(%d 条与另一侧相同 —— 应当是 0)' % big_e) - ck(bad_hdr == 0, '每条都是 I8(0x0a) + 压缩 + 464x464 + stride 464(%d 条不符)' % bad_hdr) - ck(bad_dec == 0, '每条 RLE 解出**恰好** usize=216321 且 1024+464*464 之后正好 1 字节尾巴' - '(%d 条不符)' % bad_dec) - - # ---- ⑥ 指纹有区分度 ---- - diff_fp = sum(1 for i in range(len(pay)) - if stock_b[pay[i][1]:pay[i][1] + 64] != patch_b[pay[i][1]:pay[i][1] + 64]) - ck(diff_fp == 119, '%d/119 条的"原厂前 64 B"与"改后前 64 B"确实不同' % diff_fp) - ck(all(rb[i] == (rb[i][0], sl[exp_name[i]][0], 64) for i in range(len(rb))), - 'rb 的 target 也指向对应槽位') - - # ---- ⑦ UI 素材 ---- - badui = 0 - for o, l, n in f['ui']: - blob = open(os.path.join(PROJ, 'ui_images', n + '.bin'), 'rb').read() - if int(l) != len(blob) or d[int(o):int(o) + int(l)] != blob: - badui += 1 - ck(badui == 0, '8 张界面素材与 ui_images/*.bin 逐字节一致(%d 处不符)' % badui) - - # ================================================================ 两张盘互证 - print('\n' + '=' * 78) - print('[互证] 两张交付物互相印证(完全不碰 .work / 固件镜像)') - print('=' * 78) - rp, rr = faces['replace'], faces['restore'] - ck(len(rp['data']) == len(rr['data']), '两张盘字节数相同(%d)' % len(rp['data'])) - ck(rp['data'][:50] == rr['data'][:50], - '头 50 B(magic / b4 / b5 / id)逐字节相同') - cross = 0 - for i in range(119): - if rp['pay'][i][1] != rr['pay'][i][1]: - cross += 1 - continue - o1, t, l1 = rp['pay'][i] - o2, _t2, l2 = rr['pay'][i] - if rp['data'][o1:o1 + 64] != rr['data'][rr['rb'][i][0]:rr['rb'][i][0] + 64]: - cross += 1 - if rr['data'][o2:o2 + 64] != rp['data'][rp['rb'][i][0]:rp['rb'][i][0] + 64]: - cross += 1 - ck(cross == 0, 'replace 的负载前 64 B == restore 的指纹;restore 的负载前 64 B == ' - 'replace 的指纹(%d 处不符)' % cross) - same_ui = all(rp['data'][int(o):int(o) + int(l)] == rr['data'][int(o):int(o) + int(l)] - for o, l, _n in rp['ui']) - ck(same_ui, '两张盘的 8 张界面素材逐字节相同(同一套界面)') - ck(rp['pay_base'] == rr['pay_base'] and rp['fp_base'] == rr['fp_base'], - '两张盘的资源基址相同(0x%x / 0x%x)' % (rp['pay_base'], rr['fp_base'])) - ndiff = sum(1 for i in range(119) - if rp['data'][rp['pay'][i][0]:rp['pay'][i][0] + rp['pay'][i][2]] != - rr['data'][rr['pay'][i][0]:rr['pay'][i][0] + rr['pay'][i][2]]) - ck(ndiff == 119, '119/119 条负载在两张盘里都不一样(%d)' % ndiff) - - # ================================================================ 镜像完整性 - print('\n' + '=' * 78) - print('[镜像] 改后镜像 vs 原厂镜像') - print('=' * 78) - 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 patched.entries()] - ck(a == b, 'inode 表(路径/偏移/next/spec/size/cksum)%d 条逐位不变' % len(a)) - ck((stock.sb_size, stock.sb_cksum, stock.volume) == - (patched.sb_size, patched.sb_cksum, patched.volume), - 'superblock 与卷名不变(size %d / cksum %08x / %r)' - % (patched.sb_size, patched.sb_cksum, patched.volume)) - ck(len(stock_b) == len(patch_b), '分区长度不变(%d B)' % len(patch_b)) - slot_offs = {o for o, _l in sl.values()} - bad_file = 0 - checked = 0 - for _p, i in stock.entries(): - if i.type == 1 or i.data_off in slot_offs: - continue # 目录项 / 119 个槽位本身:槽位在下面单独查 - checked += 1 - if stock_b[i.data_off:i.data_off + i.size] != patch_b[i.data_off:i.data_off + i.size]: - bad_file += 1 - ck(bad_file == 0, '除 119 个槽位外,其余 %d 个文件的字节**逐字节**不变(%d 处不符)' - % (checked, bad_file)) - chg = [i for i in range(len(stock_b)) if stock_b[i] != patch_b[i]] - lo, hi = min(slot_offs), max(o + sl[n][1] for n, (o, _s) in sl.items()) - ck(chg and chg[0] >= lo and chg[-1] < hi, - '变化区间 0x%x..0x%x 完全落在槽位区间 0x%x..0x%x 内' - % (chg[0], chg[-1], lo, hi)) - ident = [n for n, (o, _s) in sl.items() if stock_b[o:o + sl[n][1]] == patch_b[o:o + sl[n][1]]] - ck(not ident, '119 个槽位全部被改写(未变的:%s)' % (ident or '无')) - - print('\n' + '=' * 78) - print('独立总校验:%d 项通过,%d 项失败' % (ok, fail)) - print('=' * 78) - return 1 if fail else 0 - - -if __name__ == '__main__': - sys.exit(main()) diff --git a/s5e-pressure-anim/ui_images/back.bin b/s5e-pressure-anim/ui_images/back.bin deleted file mode 100644 index 76eb40c..0000000 Binary files a/s5e-pressure-anim/ui_images/back.bin and /dev/null differ diff --git a/s5e-pressure-anim/ui_images/ic_about.bin b/s5e-pressure-anim/ui_images/ic_about.bin deleted file mode 100644 index 2476014..0000000 Binary files a/s5e-pressure-anim/ui_images/ic_about.bin and /dev/null differ diff --git a/s5e-pressure-anim/ui_images/ic_check.bin b/s5e-pressure-anim/ui_images/ic_check.bin deleted file mode 100644 index 177ead3..0000000 Binary files a/s5e-pressure-anim/ui_images/ic_check.bin and /dev/null differ diff --git a/s5e-pressure-anim/ui_images/ic_help.bin b/s5e-pressure-anim/ui_images/ic_help.bin deleted file mode 100644 index 9ed2ddb..0000000 Binary files a/s5e-pressure-anim/ui_images/ic_help.bin and /dev/null differ diff --git a/s5e-pressure-anim/ui_images/ic_qq.bin b/s5e-pressure-anim/ui_images/ic_qq.bin deleted file mode 100644 index 7db71e4..0000000 Binary files a/s5e-pressure-anim/ui_images/ic_qq.bin and /dev/null differ diff --git a/s5e-pressure-anim/ui_images/ic_replace.bin b/s5e-pressure-anim/ui_images/ic_replace.bin deleted file mode 100644 index a231ebd..0000000 Binary files a/s5e-pressure-anim/ui_images/ic_replace.bin and /dev/null differ diff --git a/s5e-pressure-anim/ui_images/ic_rollback.bin b/s5e-pressure-anim/ui_images/ic_rollback.bin deleted file mode 100644 index 19debcb..0000000 Binary files a/s5e-pressure-anim/ui_images/ic_rollback.bin and /dev/null differ diff --git a/s5e-pressure-anim/ui_images/logo.bin b/s5e-pressure-anim/ui_images/logo.bin deleted file mode 100644 index e79e0fa..0000000 Binary files a/s5e-pressure-anim/ui_images/logo.bin and /dev/null differ