#!/usr/bin/env python3 """把一个已构建的固件目录封装成客户端认的官方 bundle。 官方 bundle 是一个目录,至少包含: bundle.json esp32s3-app.bin partition-table.bin bootloader.bin # 可选,客户端默认不写 客户端会先校验 bundle.json 里声明的 SHA256,再读设备当前分区表、选择空闲槽位、 写 App + 整段回读;只有回读通过才切换分区表。它不会按这里的 partition-table.bin 直接覆盖设备分区表——设备上的 NVS/PHY 布局以设备当前表为准。 用法(在仓库根目录): python scripts/make_bundle.py --firmware target/firmware 可选参数: --name "ESP32-S3 图传固件" --version "..." 默认读 BUILD-INFO.json 的 build_variant+source_revision --app-partition-size 0x140000 --flash-size 16mb --flash-mode dio --flash-frequency 40mhz --notes "..." """ from __future__ import annotations import argparse import hashlib import json import sys from pathlib import Path def sha256(path: Path) -> str: digest = hashlib.sha256() with path.open("rb") as handle: for chunk in iter(lambda: handle.read(1024 * 1024), b""): digest.update(chunk) return digest.hexdigest() def read_build_info(firmware: Path) -> dict: path = firmware / "BUILD-INFO.json" if not path.exists(): return {} try: return json.loads(path.read_text(encoding="utf-8")) except (OSError, json.JSONDecodeError): return {} def main() -> int: parser = argparse.ArgumentParser(description="生成官方固件 bundle.json") parser.add_argument("--firmware", default="target/firmware", help="固件产物目录") parser.add_argument("--name", default="ESP32-S3 图传固件") parser.add_argument("--version", default="") parser.add_argument("--app", default="esp32s3-app.bin") parser.add_argument("--partition-table", default="partition-table.bin") parser.add_argument("--bootloader", default="bootloader.bin") parser.add_argument("--app-partition-size", default="0x140000") parser.add_argument("--flash-size", default="16mb") parser.add_argument("--flash-mode", default="dio") parser.add_argument("--flash-frequency", default="40mhz") parser.add_argument("--chip", default="esp32s3") parser.add_argument("--notes", default="官方 bundle:客户端只写 App 与 patch 后的分区表,不重写 bootloader;旧 App/分区表会先备份。") args = parser.parse_args() firmware = Path(args.firmware) if not firmware.is_dir(): print(f"固件目录不存在:{firmware}", file=sys.stderr) return 1 app = firmware / args.app partition_table = firmware / args.partition_table bootloader = firmware / args.bootloader for required, label in ((app, "App"), (partition_table, "分区表")): if not required.is_file(): print(f"{label}文件不存在:{required}", file=sys.stderr) return 1 if partition_table.stat().st_size != 0xC00: print( f"分区表应为 0xC00 字节,实际 {partition_table.stat().st_size}", file=sys.stderr, ) return 1 build = read_build_info(firmware) version = args.version if not version: variant = build.get("build_variant") revision = build.get("source_revision") if variant and revision: version = f"{variant}+{revision[:7]}" else: version = "0.1.0" manifest = { "format": 1, "name": args.name, "version": version, "chip": args.chip, "app": args.app, "appSha256": sha256(app), "partitionTable": args.partition_table, "partitionTableSha256": sha256(partition_table), "appPartitionSize": args.app_partition_size, "flashSize": args.flash_size, "flashMode": args.flash_mode, "flashFrequency": args.flash_frequency, "notes": args.notes, } if bootloader.is_file(): manifest["bootloader"] = args.bootloader manifest["bootloaderSha256"] = sha256(bootloader) else: print(f"提示:没有 {bootloader},bundle 将不包含 bootloader", file=sys.stderr) output = firmware / "bundle.json" output.write_text( json.dumps(manifest, ensure_ascii=False, indent=2) + "\n", encoding="utf-8", ) print(f"已写入 {output}") print(f" {manifest['name']} {manifest['version']}") print(f" App {app.name} · {app.stat().st_size} 字节 · {manifest['appSha256']}") print(f" App 分区 {manifest['appPartitionSize']} · Flash {manifest['flashSize']}") return 0 if __name__ == "__main__": raise SystemExit(main())