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

137 lines
6.7 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""按「角色」切负载:replace(只负责替换)/ restore(只负责还原)。
拆盘的要点:一个 .face 只装一份负载,体积减半。
* replace 盘:pay = q63 的帧(新字节);rb = **原厂字节前 64 B 的指纹**(只用于版本校验)
* restore 盘:pay = 原厂字节; rb = **新字节前 64 B 的指纹**
指纹表让「写入前版本校验 + 写入后回读比对」两道保险都还在,代价只有 117*64 ≈ 7.5 KB。
两份盘用同一个 watchface id,装其中一份就会顶掉另一份,设备上始终只占一份空间。
产出:
<out>/payload[_restore]/ NNN_<name>.bin + payload.lua
<out>/blob_<role>/payload.bin 单条目:魔数 + 顺序拼接
<out>/blob_<role>/fp.bin 指纹单条目
<out>/flash.sh | restore.sh 线性 nsh 脚本(replace 盘还有 restore.sh 的对应物)
<out>/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 <this dir>/<file> -of /dev/system bs=4 seek=offset/4 count=length/4\n')
f.write('return {\n')
for r in rows:
f.write(' { name = "%s", file = "%s", offset = %d, length = %d },\n'
% (r['name'], r['file'], r['offset'], r['length']))
f.write('}\n')
return rows
lua_rows = write_pack(FPDIR)
MAGIC_PAY = b'\x00S4C-PAYv1\x00\xa5\x5a\xc3\x3c' # 15 B,必须全局唯一
MAGIC_FP = b'\x00S4C-FPv1\x00\x5a\xa5\x3c\xc3'
os.makedirs(BLOBDIR, exist_ok=True)
for f in os.listdir(BLOBDIR):
os.remove(os.path.join(BLOBDIR, f))
pay_blob = MAGIC_PAY + b''.join(r['data'] for r in emitted)
fp_blob = MAGIC_FP + b''.join(r['fp'] for r in emitted)
open(os.path.join(BLOBDIR, 'payload.bin'), 'wb').write(pay_blob)
open(os.path.join(BLOBDIR, 'fp.bin'), 'wb').write(fp_blob)
rel, off, fpoff = [], 0, 0
for r in emitted:
rel.append(dict(name=r['name'], rel=off, fprel=fpoff, target=r['offset'],
length=r['length'], fplen=len(r['fp'])))
off += r['length']
fpoff += len(r['fp'])
assert fpoff == len(fp_blob) - len(MAGIC_FP), (fpoff, len(fp_blob))
json.dump(dict(role=ROLE, magic_len=len(MAGIC_PAY), magic_fp_len=len(MAGIC_FP), fp_len=FP_LEN,
records=rel, pay_bytes=len(pay_blob), fp_bytes=len(fp_blob)),
open(os.path.join(BLOBDIR, 'offsets.json'), 'w', encoding='utf-8'), indent=1)
print('blob_%s/ 单条目: payload.bin %d B + fp.bin %d B(%d 条记录)'
% (ROLE, len(pay_blob), len(fp_blob), len(rel)))
def script(fname, rows, header, base, verify, tail):
with open(os.path.join(OUT, fname), 'w', encoding='utf-8', newline='\n') as f:
f.write('#!/bin/sh\n')
for line in header:
f.write('# ' + line + '\n')
f.write('\nDEV=/dev/system\nBASE=' + base + '\n\n')
f.write('echo "target check"\nls -l $DEV\n\n')
for r in rows:
b = 4 if (r['offset'] % 4 == 0 and r['length'] % 4 == 0) else 1
f.write('echo "== ' + r['name'] + ' ==\n')
f.write('dd if=$BASE/%s of=$DEV bs=%d seek=%d count=%d\n'
% (r['file'], b, r['offset'] // b, r['length'] // b))
if verify:
f.write('dd if=$DEV of=/tmp/chk.bin bs=%d skip=%d count=%d\n'
% (b, r['offset'] // b, r['length'] // b))
f.write('cmp /tmp/chk.bin $BASE/%s\n\n' % r['file'])
f.write(tail + '\n')
if ROLE == 'replace':
script('flash.sh', lua_rows,
['S5 41mm 充电动画 -> S4 eSIM(只写负载)',
'把 flash.sh 与 payload/ 放到同一个目录,例如 /data/s4panim/',
'然后: sh /data/s4panim/flash.sh 最后: reboot',
'还原用 restore.sh 或安装还原盘 S4ChgRestore.face'],
'/data/s4panim/payload', True,
'rm /tmp/chk.bin\necho "all done - run reboot to apply"')
else:
script('restore.sh', lua_rows,
['把 S4 eSIM 的充电动画写回原厂(还原盘)'],
'/data/s4panim/payload_restore', True,
'rm /tmp/chk.bin\necho "restore done - run reboot to apply"')
json.dump(dict(device='Xiaomi Watch S4 eSIM (o62lte)', target='/dev/system',
role=ROLE, kind='charging-anim',
dd='dd if=<dir>/<file> of=/dev/system bs=4 seek=<offset/4> count=<length/4>',
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)))