# -*- coding: utf-8 -*- """把 S5 41mm(q63) 的充电动画搬进 S4 eSIM 的 system 分区(逐帧等长覆盖)。 目标:inputs/target/vela_system.bin 的 charging/charge0..56.bin(464x464 I8 RLE) 源 :inputs/source/vela_system.bin 的 charging/charge0..60.bin(464x464 I8 RLE) 编码策略(v2,2026-10-02 换掉旧方案): 旧方案把整帧重新量化成 256 色(MEDIANCUT)—— 结果**圆环本身被毁**:亮区平均差 27~52, 因为 MEDIANCUT 按像素数量分配调色板,而亮环只占少部分像素 -> 分不到几个颜色。 新方案**直接沿用 q63 原帧的调色板 + 索引**(零量化误差,圆环逐像素完全一致), 只把"很暗的像素"归到一个索引上换取 RLE 长度,阈值从 0 起逐档加大,取**能塞进槽位的最小阈值**。 阈值只影响接近黑的雾面区(4~32/255),亮环完全不动。 等长要点: 1) 数据总长必须等于原槽位长度;usize 尾巴必须补够(少一字节设备就不画) 2) pad_rle_safe 只用 00 00 / 80 这类空指令补齐 """ import os, sys, json, struct, shutil, hashlib sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from paths import INPUTS, WORK from romfs import Romfs import lvgl_bin as L from PIL import Image import numpy as np STOCK = os.path.join(INPUTS, 'target', 'vela_system.bin') # S4 eSIM SOURCE = os.path.join(INPUTS, 'source', 'vela_system.bin') # q63 OUTDIR = os.path.join(WORK, 'charge') PATCHED = os.path.join(OUTDIR, 'vela_system_charge.bin') DIRP = 'charging/' SAMPLE = None if (len(sys.argv) > 1 and sys.argv[1] == 'all') else [0, 14, 28, 42, 56] THRS = (0, 4, 8, 12, 16, 20, 24, 32, 40, 48, 64, 96, 128, 192, 255) FALLBACK_COLORS = (256, 192, 128, 96, 64, 48, 32) def num_of(path): return int(path.rsplit('charge', 1)[-1].replace('.bin', '')) def collect(rom): d = {} for p, i in rom.entries(): if i.type == 1 or not p.startswith(DIRP) or not p.endswith('.bin'): continue d[num_of(p)] = (p, i, rom.read(i)) return d def src_indexed(blob): """取出 I8 RLE 帧的原始调色板(256x4 BGRA)与索引图(H,W)。""" h = L.parse_header(blob) method, csize, usize = struct.unpack(' 0: n = min(short, 127) ex += bytes((n, 0)) short -= n return comp + bytes(ex) def main(): os.makedirs(OUTDIR, exist_ok=True) stock, src = Romfs(STOCK), Romfs(SOURCE) slots, srcs = collect(stock), collect(src) tl, sl = sorted(slots), sorted(srcs) print('目标 %d 帧(%d..%d,%.1f MB) 源 %d 帧(%d..%d,%.1f MB)' % ( len(tl), tl[0], tl[-1], sum(len(slots[n][2]) for n in tl) / 1048576, len(sl), sl[0], sl[-1], sum(len(srcs[n][2]) for n in sl) / 1048576)) todo = tl if SAMPLE is None else [n for n in SAMPLE if n in slots] print('本次要改的帧:%s' % todo) records, decode_ck, used, fallback = [], 0, {}, 0 for n in todo: path, ino, blob = slots[n] h = L.parse_header(blob) w, ht, stride = h['w'], h['h'], max(h['stride'], h['w']) method, csize, usize = struct.unpack(' 60 e_br = float(d[br].mean()) if br.any() else 0.0 e_dk = float(d[~br].mean()) if (~br).any() else 0.0 used[how] = used.get(how, 0) + 1 print(' 帧 %-3d <- q63 第 %-3d 帧 槽位 %6d B 编码 %6d B 亮区均差 %.2f 暗区均差 %.2f %s' % (n, m, len(blob), len(comp), e_br, e_dk, how)) records.append(dict(name='charge%d' % n, src='charge%d' % m, offset=ino.data_off, length=len(blob), data=new, how=how)) print('回解自检通过 %d / %d(兜底 %d 帧)' % (decode_ck, len(records), fallback)) print('编码方式分布:%s' % used) shutil.copyfile(STOCK, PATCHED) with open(PATCHED, 'r+b') as f: for r in records: f.seek(r['offset']) f.write(r['data']) json.dump(dict(device='Xiaomi Watch S4 eSIM (o62lte) vela_system.bin', target_dir=DIRP, source='Xiaomi Watch S5 41mm (q63) charging frames', sample='all' if SAMPLE is None else SAMPLE, image_sha256=hashlib.sha256(stock.data).hexdigest(), records=[dict(name=r['name'], offset=r['offset'], length=r['length'], src=r['src'], how=r['how']) for r in records]), open(os.path.join(OUTDIR, 'manifest.json'), 'w', encoding='utf-8'), indent=1, ensure_ascii=False) r2 = Romfs(PATCHED) a = [(p, i.off, i.next, i.spec, i.size, i.cksum) for p, i in stock.entries()] b = [(p, i.off, i.next, i.spec, i.size, i.cksum) for p, i in r2.entries()] print('\ninode 表逐位不变:%s(%d 条)' % (a == b, len(a))) print('superblock 一致:%s' % (stock.sb_size == r2.sb_size and stock.sb_cksum == r2.sb_cksum)) sh = open(PATCHED, 'rb').read() print('回放(改后镜像 == 计划字节):%s' % all( sh[r['offset']:r['offset'] + r['length']] == r['data'] for r in records)) print('负载合计 %.2f MB;写出 %s' % (sum(r['length'] for r in records) / 1048576, os.path.join(OUTDIR, 'manifest.json'))) def pal_lum(pal, vals): return pal[vals][:, :3].max(axis=1) # 只取最大值,通道序无关 def rgb_of(pal, idx): return pal[idx][:, :, [2, 1, 0]] if __name__ == '__main__': main()