diff --git a/tools/tone_artist_pack.py b/tools/tone_artist_pack.py new file mode 100644 index 0000000..3b449ba --- /dev/null +++ b/tools/tone_artist_pack.py @@ -0,0 +1,253 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""音师自助:把投放目录的 1/2/3.bin 打成 extflash_ALL_*.res,供 SoundWalkerIAP USB 烧录。 + +不走 J-Link;不改固件头文件;不重编 MCU。 +需要本机已有 ui0902_res.bin(或最近一次发布包里的 ALL.res 可拆出 UI 段)。 +""" +from __future__ import annotations + +import argparse +import shutil +import struct +import sys +from datetime import datetime +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] # firmware project +REPO = ROOT.parent.parent # 一诺国际吉他 +OUT_DIR = ROOT / "tools" / "out" +DROP_DIR = REPO / "Doc" / "音色文件" / "音师投放" +IAP_DIR = REPO / "升级" / "MCU主控升级" +UI0902_BIN = OUT_DIR / "ui0902_res.bin" +REPO_OUT = REPO / "tools" / "out" + +OFF_LOGO = 0x00000000 +OFF_CHARGING = 0x0000CB70 +OFF_BIN1 = 0x0001B8F0 +OFF_BIN2 = 0x0009D07D +OFF_BIN3 = 0x000A71AC +UI0902_RES_BASE = 0x00100000 +DAB_MAGIC = b"\xABDAB" +MAX_SONG_BIN_BYTES = 41 * 1024 +BIN2_SLOT_BYTES = OFF_BIN3 - OFF_BIN2 + + +def find_ziliao() -> Path: + for p in REPO.iterdir(): + if p.is_dir() and (p / "logo.bin").is_file() and (p / "Charg.bin").is_file(): + return p + raise SystemExit("找不到 资料/logo.bin 与 Charg.bin,无法打包。") + + +def dab_ok(blob: bytes, off: int, expect_cnt: int, label: str) -> None: + if off + 16 > len(blob): + raise SystemExit(f"{label} @0x{off:X}: 文件太短,无法校验") + magic = blob[off : off + 4] + cnt = struct.unpack_from(" None: + end = off + len(src) + if end > len(blob) or blob[off:end] != src: + raise SystemExit(f"内部校验失败:{label} 未正确写入资源包") + + +def find_ui_baseline() -> bytes: + if UI0902_BIN.is_file(): + data = UI0902_BIN.read_bytes() + print(f"UI 底图: {UI0902_BIN} ({len(data)} bytes)") + return data + + # Fallback: newest release ALL.res under repo tools/out + candidates: list[Path] = [] + if REPO_OUT.is_dir(): + candidates.extend(REPO_OUT.glob("**/extflash_ALL_*.res")) + candidates.extend(REPO_OUT.glob("extflash_ALL_*.res")) + candidates = sorted({p.resolve() for p in candidates if p.is_file()}, key=lambda p: p.stat().st_mtime, reverse=True) + for all_path in candidates: + raw = all_path.read_bytes() + if len(raw) > UI0902_RES_BASE: + ui = raw[UI0902_RES_BASE:] + print(f"UI 底图: 从发布包拆出 {all_path.name} @0x{UI0902_RES_BASE:X} ({len(ui)} bytes)") + return ui + + raise SystemExit( + "缺少 UI 底图 ui0902_res.bin。\n" + "请向开发索取一次含 UI 的基线(tools/out/ui0902_res.bin,或完整 extflash_ALL_*.res)," + "放到工程 tools/out/ 后再运行本工具。" + ) + + +def pack_tone(bin1: bytes, bin2: bytes, bin3: bytes, logo: bytes, charg: bytes) -> bytes: + if len(bin2) > MAX_SONG_BIN_BYTES: + raise SystemExit(f"2.bin 过大: {len(bin2)} > {MAX_SONG_BIN_BYTES} (41KB 上限)") + if len(bin2) > BIN2_SLOT_BYTES: + raise SystemExit( + f"2.bin 过大: {len(bin2)} > 槽位 {BIN2_SLOT_BYTES},会挤占 3.bin 固定地址 0x{OFF_BIN3:X}" + ) + + parts = [ + (OFF_LOGO, logo, "logo"), + (OFF_CHARGING, charg, "Charg"), + (OFF_BIN1, bin1, "1.bin"), + (OFF_BIN2, bin2, "2.bin"), + (OFF_BIN3, bin3, "3.bin"), + ] + blobs: list[bytes] = [] + cursor = 0 + for force_off, data, name in parts: + if cursor > force_off: + raise SystemExit(f"{name}: 前一段过大,无法放到 0x{force_off:X}") + if cursor < force_off: + blobs.append(b"\xFF" * (force_off - cursor)) + cursor = force_off + blobs.append(data) + cursor += len(data) + + packed = b"".join(blobs) + dab_ok(packed, OFF_BIN1, 31, "1.bin(节奏)") + dab_ok(packed, OFF_BIN2, 1, "2.bin(本地曲目)") + dab_ok(packed, OFF_BIN3, 3, "3.bin(万能)") + require_equals(packed, OFF_BIN1, bin1, "1.bin") + require_equals(packed, OFF_BIN2, bin2, "2.bin") + require_equals(packed, OFF_BIN3, bin3, "3.bin") + if cursor > UI0902_RES_BASE: + raise SystemExit("音色区溢出到 UI 区 (0x100000)") + return packed + + +def write_flash_guide(path: Path, all_name: str, stamp: str) -> None: + text = "\n".join( + [ + "音师音色自助包 — 请这样用 SoundWalkerIAP 刷机", + f"生成时间: {stamp}", + "", + "==== 本次只需刷外部 Flash(不用重刷 Boot/APP,除非开发另有说明) ====", + f" 文件: {all_name}", + " 地址: 0x00000000(从外部 Flash 开头整包写入)", + "", + "步骤:", + " 1. 吉他进入 USB 升级模式(参见 升级步骤.docx)", + " 2. 打开本目录的 SoundWalkerIAP.exe", + " 3. 先整片擦除外部 Flash", + f" 4. 选择 {all_name},烧录到外部 Flash @ 0x00000000", + " 5. 退出升级模式,重启,试听节奏/本地曲/万能", + "", + "注意:", + " - 必须刷本包 ALL.res,不要只刷单独的 1/2/3.bin", + " - 只刷音色、不带 UI 会导致模式选择页花屏", + " - 2.bin 必须 ≤41KB;3.bin 地址固定,不可随 2.bin 变长漂移", + " - 若增删本地曲目或改曲名,需找开发同步固件后再测", + "", + ] + ) + path.write_text(text, encoding="utf-8", newline="\n") + + +def main() -> int: + ap = argparse.ArgumentParser(description="音师自助打包 1/2/3.bin → ALL.res(USB IAP)") + ap.add_argument( + "--drop", + type=Path, + default=DROP_DIR, + help=f"投放目录(默认 {DROP_DIR})", + ) + ap.add_argument( + "--open", + action="store_true", + help="完成后用资源管理器打开输出目录", + ) + args = ap.parse_args() + drop: Path = args.drop + + print("=== 音师音色打包(USB / SoundWalkerIAP,非 J-Link)===") + print(f"投放目录: {drop}") + + bin1_p = drop / "1.bin" + bin2_p = drop / "2.bin" + bin3_p = drop / "3.bin" + missing = [p.name for p in (bin1_p, bin2_p, bin3_p) if not p.is_file()] + if missing: + raise SystemExit( + f"投放目录缺少: {', '.join(missing)}\n" + f"请把新的 1.bin / 2.bin / 3.bin 放到:\n {drop}" + ) + + ziliao = find_ziliao() + bin1 = bin1_p.read_bytes() + bin2 = bin2_p.read_bytes() + bin3 = bin3_p.read_bytes() + logo = (ziliao / "logo.bin").read_bytes() + charg = (ziliao / "Charg.bin").read_bytes() + print(f"1.bin {len(bin1)} bytes | 2.bin {len(bin2)} bytes | 3.bin {len(bin3)} bytes") + + tone = pack_tone(bin1, bin2, bin3, logo, charg) + ui = find_ui_baseline() + all_res = tone + (b"\xFF" * (UI0902_RES_BASE - len(tone))) + ui + require_equals(all_res, OFF_BIN1, bin1, "ALL/1.bin") + require_equals(all_res, OFF_BIN2, bin2, "ALL/2.bin") + require_equals(all_res, OFF_BIN3, bin3, "ALL/3.bin") + dab_ok(all_res, OFF_BIN1, 31, "ALL/1.bin") + dab_ok(all_res, OFF_BIN2, 1, "ALL/2.bin") + dab_ok(all_res, OFF_BIN3, 3, "ALL/3.bin") + + stamp = datetime.now().strftime("%Y%m%d_%H%M%S") + day = datetime.now().strftime("%Y%m%d") + pkg = OUT_DIR / f"音师音色包_{day}" + pkg.mkdir(parents=True, exist_ok=True) + all_name = f"extflash_ALL_artist_{stamp}.res" + all_path = pkg / all_name + all_path.write_bytes(all_res) + (OUT_DIR / "extflash_tone_artist.res").write_bytes(tone) + (OUT_DIR / "extflash_ALL_artist.res").write_bytes(all_res) + + iap_exe = IAP_DIR / "SoundWalkerIAP.exe" + iap_doc = IAP_DIR / "升级步骤.docx" + if not iap_exe.is_file(): + raise SystemExit(f"找不到 {iap_exe}") + shutil.copy2(iap_exe, pkg / "SoundWalkerIAP.exe") + if iap_doc.is_file(): + shutil.copy2(iap_doc, pkg / "升级步骤.docx") + write_flash_guide(pkg / "请这样刷.txt", all_name, stamp) + + print(f"OK 音色区 {len(tone)} bytes,ALL.res {len(all_res)} bytes") + print(f"OK 输出目录: {pkg}") + print(f" - {all_name}") + print(" - SoundWalkerIAP.exe") + print(" - 请这样刷.txt") + print("请用 USB + SoundWalkerIAP 刷 ALL.res,不要用 J-Link 单独烧 bin。") + + if args.open: + try: + import os + + os.startfile(str(pkg)) # type: ignore[attr-defined] + except Exception as exc: + print(f"(无法自动打开目录: {exc})") + return 0 + + +if __name__ == "__main__": + try: + sys.exit(main()) + except SystemExit as e: + code = e.code + if isinstance(code, str): + print(f"\n失败: {code}") + sys.exit(1) + if code not in (0, None): + sys.exit(int(code) if isinstance(code, int) else 1) + sys.exit(0) + except Exception as e: + print(f"\n失败: {e}") + sys.exit(1) diff --git a/tools/音师投放说明.txt b/tools/音师投放说明.txt new file mode 100644 index 0000000..7e4e0a7 --- /dev/null +++ b/tools/音师投放说明.txt @@ -0,0 +1,26 @@ +音师投放与自助更新说明 +======================== + +一、投放目录(放新的 1/2/3.bin) + 一诺国际吉他\Doc\音色文件\音师投放\ + 1.bin 普通/专业节奏 + 2.bin 本地曲目(必须 ≤41KB) + 3.bin 万能模式 + +二、一键打包(合并 ALL.res,USB 烧录,不用 J-Link) + 双击:本目录「音师更新音色.bat」 + 或命令行:python tone_artist_pack.py --open + +三、烧录 + 输出在 tools\out\音师音色包_日期\ + - extflash_ALL_artist_*.res + - SoundWalkerIAP.exe + - 请这样刷.txt + 按「请这样刷.txt」:进 USB 升级模式 → 擦除外部 Flash + → SoundWalkerIAP 将 ALL.res 烧到 0x00000000 → 重启试听 + +四、限制 + - 只更新音色时一般不用重刷 MCU + - 必须刷 ALL.res(含界面),勿只刷单独 bin,否则模式选择花屏 + - 增删本地曲/改曲名需找开发同步固件曲名表 + - 首次需本机有 tools\out\ui0902_res.bin(向开发索取一次基线即可) diff --git a/tools/音师更新音色.bat b/tools/音师更新音色.bat new file mode 100644 index 0000000..ce86831 --- /dev/null +++ b/tools/音师更新音色.bat @@ -0,0 +1,31 @@ +@echo off +chcp 65001 >nul +setlocal +cd /d "%~dp0" + +echo. +echo ======================================== +echo 音师音色更新 - 打包 ALL.res (USB IAP) +echo 不使用 J-Link,请用 SoundWalkerIAP 烧录 +echo ======================================== +echo. + +where python >nul 2>&1 +if errorlevel 1 ( + echo [错误] 未找到 python,请先安装 Python 3 并勾选 Add to PATH。 + pause + exit /b 1 +) + +python "%~dp0tone_artist_pack.py" --open +set ERR=%ERRORLEVEL% +echo. +if not "%ERR%"=="0" ( + echo 打包失败,请根据上方中文提示检查 1/2/3.bin。 + pause + exit /b %ERR% +) + +echo 打包成功。请按输出目录中的「请这样刷.txt」用 SoundWalkerIAP 刷机。 +pause +exit /b 0