- feat(client): 界面换成墨黑蓝底 + 单支红的矩形语言,切角全部去掉,仅装甲条保留平行四边形 - feat(client): 自托管 HarmonyOS Sans SC 与 JetBrains Mono,中文与拉丁数字逐字回退 - feat(client): 新增启动序列、首启门、用户端 HUD、裁判端控制台、设置抽屉与固件维护页 - feat(client): 裁判端中缝与下方控制台可拖,鼠标落在哪一路下方就看哪一路 - feat(client): 设置抽屉常驻实时预览,与画面共用同一份准星绘制代码 - feat(client): 动效改用 Material 3 的缓动曲线与时长,逐条替换掉自造的贝塞尔 - feat(version): 版本号与 CHANGELOG 由 scripts/version.mjs 从 git 历史算出,不再手写 - feat(firmware): Hello 报文携带语义化版本、构建号与自报协议号,负载向后兼容 - feat(client): 固件版本显示在绑定列表与裁判端每一格;协议不匹配时给出具体原因 - feat(firmware): 固件包版本与客户端同源,都从 version.json 读 - fix(client): getCurrentWindow() 在没有 Tauri 注入时同步抛异常,浏览器预览整棵树被卸载 - fix(client): 启动序列的淡出 effect 清掉了自己刚设的 setTimeout,界面永远停在启动页 - fix(client): 设置抽屉左侧导航的锚点没挂到 section 上,且用错了坐标系 - fix(protocol): Hello 改用宽松帧头解析,让协议不匹配的板子能被解释而不是静默消失 - fix(build): Windows 上 bindgen 加载不了 libclang 的同目录依赖,构建脚本补上搜索路径 - fix(build): esp-idf-sys 在长路径下拒绝构建,固件构建改用短的目标目录 - build(client): 队标图标,含光学尺寸分级与手工拼的多尺寸 ICO - build(ci): 守门 job 增加版本一致性检查,产物名改为 rmic-2026-* - docs: 补充界面语言、版本机制与协议通告布局的说明
139 lines
4.7 KiB
Python
139 lines
4.7 KiB
Python
#!/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())
|