Initial Commit
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
"""按「角色」切负载:replace(只负责替换)/ restore(只负责还原)。
|
||||
|
||||
拆盘的要点:一个 .face 只装一份负载,体积减半。
|
||||
* replace 盘:pay = q63 的帧(新字节);rb = **原厂字节前 64 B 的指纹**(只用于版本校验)
|
||||
* restore 盘:pay = 原厂字节; rb = **新字节前 64 B 的指纹**
|
||||
指纹表让「写入前版本校验 + 写入后回读比对」两道保险都还在,代价只有 117*64 ≈ 7.5 KB。
|
||||
两份盘用同一个 watchface id,装其中一份就会顶掉另一份,设备上始终只占一份空间。
|
||||
|
||||
产出:
|
||||
<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)))
|
||||
Reference in New Issue
Block a user