feat(pack): v2 双固件 profile 打包 — fw_pack 公共库 + CLI 重构 + GUI 打包弹窗
This commit is contained in:
+115
-4
@@ -28,12 +28,20 @@ import openocd_runner
|
||||
from fw_image import (CHIP_PROFILES, FLAG_VALUE_DEFAULT, FLASH_BASE_DEFAULT,
|
||||
merge_firmware, pad_bin, parse_int)
|
||||
|
||||
# fw_pack (v2 双固件打包公共库) 位于仓库根目录: 源码模式补 sys.path,
|
||||
# 打包模式(PyInstaller --paths+hiddenimport)由运行库提供
|
||||
_REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))
|
||||
if os.path.isfile(os.path.join(_REPO_ROOT, 'fw_pack.py')) \
|
||||
and _REPO_ROOT not in sys.path:
|
||||
sys.path.insert(0, _REPO_ROOT)
|
||||
import fw_pack
|
||||
|
||||
MONO = ('Consolas', 12)
|
||||
BOLD = ('Microsoft YaHei UI', 13, 'bold')
|
||||
TITLE_FONT = ('Microsoft YaHei UI', 18, 'bold')
|
||||
|
||||
APP_NAME = 'DAPLink Flash'
|
||||
APP_VERSION = '2.5.2'
|
||||
APP_VERSION = '2.6.0'
|
||||
REPO_URL = 'https://gitea.hulk.wang/hulk/gd32e230f8_firmware_merge_tool'
|
||||
|
||||
OK_BLUE = '#3B8ED0'
|
||||
@@ -110,6 +118,7 @@ class App(ctk.CTk):
|
||||
self._op_buttons = []
|
||||
self.v_pad_result = tk.StringVar(value='')
|
||||
self.v_merge_result = tk.StringVar(value='')
|
||||
self.v_pack_result = tk.StringVar(value='')
|
||||
|
||||
self._build()
|
||||
self._log(f'{APP_NAME} v{APP_VERSION} — OpenOCD 烧录 · 固件补齐 · 镜像拼接')
|
||||
@@ -159,8 +168,11 @@ class App(ctk.CTk):
|
||||
ctk.CTkLabel(top, text=APP_NAME, font=TITLE_FONT).pack(side='left')
|
||||
ctk.CTkLabel(top, text='GD32 固件烧录',
|
||||
text_color='gray60').pack(side='left', padx=10, pady=(6, 0))
|
||||
ctk.CTkButton(top, text='打包 (v2)', width=96, height=26,
|
||||
command=self._open_pack_dialog).pack(side='left', padx=(16, 6),
|
||||
pady=(4, 0))
|
||||
ctk.CTkButton(top, text='补齐 (Pad)', width=88, height=26,
|
||||
command=self._open_pad_dialog).pack(side='left', padx=(16, 6),
|
||||
command=self._open_pad_dialog).pack(side='left', padx=(6, 6),
|
||||
pady=(4, 0))
|
||||
ctk.CTkButton(top, text='拼接 (Merge)', width=100, height=26,
|
||||
command=self._open_merge_dialog).pack(side='left', pady=(4, 0))
|
||||
@@ -288,7 +300,7 @@ class App(ctk.CTk):
|
||||
self.btn_stop.grid(row=1, column=2, sticky='ew', padx=3, pady=3)
|
||||
|
||||
# ============================================================
|
||||
# 固件工具弹窗: 补齐 / 拼接
|
||||
# 固件工具弹窗: 打包(v2) / 补齐 / 拼接
|
||||
# ============================================================
|
||||
def _center_dialog(self, dlg, w, h):
|
||||
"""弹窗叠在主窗口中上部"""
|
||||
@@ -297,6 +309,41 @@ class App(ctk.CTk):
|
||||
y = self.winfo_rooty() + max(0, (self.winfo_height() - h) // 4)
|
||||
dlg.geometry(f'{w}x{h}+{x}+{y}')
|
||||
|
||||
def _open_pack_dialog(self):
|
||||
dlg = ctk.CTkToplevel(self)
|
||||
dlg.title('固件工具 — 打包 (v2 双固件)')
|
||||
dlg.resizable(False, False)
|
||||
dlg.transient(self)
|
||||
box = ctk.CTkFrame(dlg, fg_color='transparent')
|
||||
box.pack(fill='both', expand=True, padx=18, pady=(14, 12))
|
||||
ctk.CTkLabel(box, text='打包 (v2 双固件)', font=BOLD, anchor='w').pack(fill='x')
|
||||
ctk.CTkLabel(box, text='按 profile 布局自动识别 A/B 槽, 产出 UPDATE / B / FULL + MD5 清单',
|
||||
text_color='gray60', anchor='w').pack(fill='x', pady=(0, 6))
|
||||
row = ctk.CTkFrame(box, fg_color='transparent')
|
||||
row.pack(fill='x', pady=(4, 0))
|
||||
ctk.CTkLabel(row, text='Profile:', width=150, anchor='w').pack(side='left')
|
||||
ctk.CTkComboBox(row, values=list(fw_pack.BUILTIN_PROFILES),
|
||||
variable=self.v['pack_profile']).pack(
|
||||
side='left', fill='x', expand=True)
|
||||
ctk.CTkButton(row, text='浏览 JSON', width=84,
|
||||
command=self._browse_pack_profile).pack(side='left', padx=(6, 0))
|
||||
self._field(box, 'Bootloader (BL)', self.v['pack_bl'], self._browse_pack_bl)
|
||||
self._field(box, 'A 包固件 (运行槽)', self.v['pack_a'], self._browse_pack_a)
|
||||
self._field(box, 'B 包固件 (保底槽)', self.v['pack_b'], self._browse_pack_b)
|
||||
self._field(box, '输出目录', self.v['pack_out'], self._browse_pack_out)
|
||||
opts = ctk.CTkFrame(box, fg_color='transparent')
|
||||
opts.pack(fill='x', pady=(6, 0))
|
||||
ctk.CTkCheckBox(opts, text='生成 FULL 整片镜像', variable=self.v['pack_full'],
|
||||
checkbox_width=18, checkbox_height=18).pack(side='left')
|
||||
ctk.CTkButton(opts, text='执行打包', width=96,
|
||||
command=self._on_pack).pack(side='right')
|
||||
ctk.CTkLabel(box, textvariable=self.v_pack_result, text_color=OK_BLUE,
|
||||
justify='left', anchor='w').pack(fill='x', pady=(8, 0))
|
||||
ctk.CTkLabel(box, text='A 包→*_UPDATE.bin(OA/Ymodem) B 包→*_B.bin(满槽+记录) '
|
||||
'FULL→整片镜像(@0x08000000)+*.md5.txt',
|
||||
text_color='gray50', anchor='w').pack(fill='x', pady=(2, 0))
|
||||
self._center_dialog(dlg, 720, 400)
|
||||
|
||||
def _open_pad_dialog(self):
|
||||
dlg = ctk.CTkToplevel(self)
|
||||
dlg.title('固件工具 — 补齐 (Pad)')
|
||||
@@ -501,6 +548,38 @@ class App(ctk.CTk):
|
||||
if path:
|
||||
self.v['merge_out'].set(path)
|
||||
|
||||
def _browse_pack_profile(self):
|
||||
path = filedialog.askopenfilename(
|
||||
title='选择 profile JSON',
|
||||
filetypes=[('Profile JSON', '*.json'), ('所有文件', '*.*')])
|
||||
if path:
|
||||
self.v['pack_profile'].set(path)
|
||||
|
||||
def _browse_pack_bl(self):
|
||||
path = filedialog.askopenfilename(
|
||||
title='选择 Bootloader BIN', filetypes=[('BIN 文件', '*.bin'), ('所有文件', '*.*')])
|
||||
if path:
|
||||
self.v['pack_bl'].set(path)
|
||||
|
||||
def _browse_pack_a(self):
|
||||
path = filedialog.askopenfilename(
|
||||
title='选择 A 包固件 BIN (运行槽)', filetypes=[('BIN 文件', '*.bin'), ('所有文件', '*.*')])
|
||||
if path:
|
||||
self.v['pack_a'].set(path)
|
||||
if not self.v['pack_out'].get().strip():
|
||||
self.v['pack_out'].set(os.path.dirname(path))
|
||||
|
||||
def _browse_pack_b(self):
|
||||
path = filedialog.askopenfilename(
|
||||
title='选择 B 包固件 BIN (保底槽)', filetypes=[('BIN 文件', '*.bin'), ('所有文件', '*.*')])
|
||||
if path:
|
||||
self.v['pack_b'].set(path)
|
||||
|
||||
def _browse_pack_out(self):
|
||||
path = filedialog.askdirectory(title='选择产物输出目录')
|
||||
if path:
|
||||
self.v['pack_out'].set(path)
|
||||
|
||||
def _on_chip_change(self, *_a):
|
||||
self.v['flash_size'].set(openocd_runner.chip_flash_size(self.v['chip'].get()))
|
||||
self.v['target'].set(openocd_runner.chip_target(self.v['chip'].get()))
|
||||
@@ -634,8 +713,39 @@ class App(ctk.CTk):
|
||||
self._set_status('就绪')
|
||||
|
||||
# ============================================================
|
||||
# 固件工具: 补齐 / 拼接
|
||||
# 固件工具: 打包(v2) / 补齐 / 拼接
|
||||
# ============================================================
|
||||
def _on_pack(self):
|
||||
"""v2 双固件打包: 与命令行共用 fw_pack.pack_all, 结果与校验流进主日志"""
|
||||
profile = self.v['pack_profile'].get().strip()
|
||||
a = self.v['pack_a'].get().strip()
|
||||
b = self.v['pack_b'].get().strip()
|
||||
bl = self.v['pack_bl'].get().strip()
|
||||
out_dir = self.v['pack_out'].get().strip() or None
|
||||
if not a and not b:
|
||||
messagebox.showerror('缺少文件', '请至少选择 A 包或 B 包固件 BIN。')
|
||||
return
|
||||
for p, tag in ((a, 'A 包'), (b, 'B 包'), (bl, 'BL')):
|
||||
if p and not os.path.isfile(p):
|
||||
messagebox.showerror('缺少文件', f'{tag}文件不存在:\n{p}')
|
||||
return
|
||||
self._log('== 打包 (v2) ==')
|
||||
try:
|
||||
outcome = fw_pack.pack_all(
|
||||
profile, a or None, b_path=b or None, bl_path=bl or None,
|
||||
out_dir=out_dir, make_full=self.v['pack_full'].get(),
|
||||
progress=lambda line: self._log_q.put(('log', ' ' + line)))
|
||||
except fw_pack.FwPackError as exc:
|
||||
messagebox.showerror('打包失败', str(exc))
|
||||
self._log('打包失败: %s' % exc)
|
||||
return
|
||||
names = {'update': 'UPDATE', 'b': 'B 包', 'full': 'FULL', 'md5': 'MD5 清单'}
|
||||
self.v_pack_result.set('完成: ' + ', '.join(
|
||||
f"{names[k]} {os.path.basename(p)}" for k, p in outcome.paths.items()))
|
||||
self._log('打包完成: %s' % ', '.join(outcome.paths.values()))
|
||||
self._set_status('打包完成')
|
||||
self._save()
|
||||
|
||||
def _on_pad(self):
|
||||
src = self.v['pad_in'].get().strip()
|
||||
dst = self.v['pad_out'].get().strip()
|
||||
@@ -737,6 +847,7 @@ class App(ctk.CTk):
|
||||
ctk.CTkLabel(box, text=f'v{APP_VERSION} — GD32E230 固件烧录与打包工具',
|
||||
text_color='gray60', anchor='w').pack(anchor='w', pady=(0, 12))
|
||||
for line in ('OpenOCD 烧录: 检测 / 烧录 / 擦除 / 读取 / 复位 (DAPLink·GD-Link·ST-Link·J-Link)',
|
||||
'打包 v2: 双固件 profile 布局 → UPDATE / B / FULL 镜像 + MD5 清单',
|
||||
'补齐 Pad: BIN 尾部补 0xFF 对齐页边界, 适配 Bootloader Y-Modem',
|
||||
'拼接 Merge: Bootloader + APP + 有效标志 → 整片 Flash 镜像',
|
||||
'发布包内置 OpenOCD 工具链, 免安装解压即用',
|
||||
|
||||
@@ -28,6 +28,9 @@ BASE = os.path.dirname(SRC)
|
||||
DIST = os.path.join(BASE, 'release')
|
||||
WORK = os.path.join(SRC, 'build')
|
||||
ICO = os.path.join(SRC, 'logos', 'logo_flash.ico')
|
||||
# 仓库根: fw_pack.py(v2 打包公共库)的规范位置, PyInstaller 需要显式加入
|
||||
# 搜索路径并声明 hiddenimport(源码里是带 sys.path 回退的动态导入)
|
||||
REPO_ROOT = os.path.dirname(BASE)
|
||||
|
||||
|
||||
def ensure_pyinstaller():
|
||||
@@ -41,13 +44,19 @@ def ensure_pyinstaller():
|
||||
|
||||
|
||||
def extra_collect_args():
|
||||
"""CustomTkinter 的主题/字体等数据文件必须显式收集, 否则打包后 GUI 启动报错"""
|
||||
"""CustomTkinter 的主题/字体等数据文件必须显式收集, 否则打包后 GUI 启动报错;
|
||||
fw_pack 是仓库根的公共库, 需进 module 搜索路径 + hiddenimport"""
|
||||
args = []
|
||||
try:
|
||||
import customtkinter # noqa: F401
|
||||
return ['--collect-all', 'customtkinter']
|
||||
args += ['--collect-all', 'customtkinter']
|
||||
except ImportError:
|
||||
print('警告: 未安装 customtkinter, 打包产物将无法启动 GUI!')
|
||||
return []
|
||||
if os.path.isfile(os.path.join(REPO_ROOT, 'fw_pack.py')):
|
||||
args += ['--paths', REPO_ROOT, '--hiddenimport', 'fw_pack']
|
||||
else:
|
||||
print('警告: 仓库根缺少 fw_pack.py, 打包产物将无法使用"打包 v2"功能!')
|
||||
return args
|
||||
|
||||
|
||||
def remove_stale(out_dir, name):
|
||||
|
||||
@@ -56,6 +56,13 @@ DEFAULTS = {
|
||||
"flag_addr": "0x0800FFFC",
|
||||
"merge_flash": "0x10000",
|
||||
"flag_value": "0xEEEEEEEE",
|
||||
# 打包 (v2 双固件, fw_pack)
|
||||
"pack_profile": "ceramixnano_mb_v51",
|
||||
"pack_bl": "",
|
||||
"pack_a": "",
|
||||
"pack_b": "",
|
||||
"pack_out": "",
|
||||
"pack_full": True,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -26,6 +26,8 @@ DAPLink Flash — GD32 固件烧录 · 补齐 · 镜像拼接 — 入口与命
|
||||
python flash_gui.py --reset 复位运行
|
||||
python flash_gui.py --pad app.bin 补齐到 1K 边界(app_pad.bin)
|
||||
python flash_gui.py --merge bl.bin app.bin 拼接整片镜像(*_BL.bin)
|
||||
python flash_gui.py --pack a.bin b.bin --pack-bl bl.bin
|
||||
v2 双固件打包(UPDATE/B/FULL+MD5)
|
||||
python flash_gui.py --version 打印 OpenOCD 版本
|
||||
"""
|
||||
|
||||
@@ -40,6 +42,14 @@ import openocd_runner
|
||||
from fw_image import (APP_ADDR_DEFAULT, FLASH_BASE_DEFAULT, FLAG_ADDR_DEFAULT,
|
||||
FLAG_VALUE_DEFAULT, merge_firmware, pad_bin, parse_int)
|
||||
|
||||
# fw_pack (v2 双固件打包公共库) 位于仓库根目录: 源码模式补 sys.path,
|
||||
# 打包模式(PyInstaller --paths+hiddenimport)由运行库提供
|
||||
_REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))
|
||||
if os.path.isfile(os.path.join(_REPO_ROOT, 'fw_pack.py')) \
|
||||
and _REPO_ROOT not in sys.path:
|
||||
sys.path.insert(0, _REPO_ROOT)
|
||||
import fw_pack
|
||||
|
||||
|
||||
def _toolchain(args):
|
||||
"""命令行工具链解析: 参数 > 配置文件 > 自动定位(内置/本机/PATH)"""
|
||||
@@ -121,6 +131,37 @@ def run_merge(args):
|
||||
return 0
|
||||
|
||||
|
||||
def run_pack(args):
|
||||
"""v2 双固件打包(GUI '打包 v2' 的命令行形态, 共用 fw_pack.pack_all)"""
|
||||
try:
|
||||
fw_pack.self_test()
|
||||
profile = fw_pack.load_profile(args.pack_profile)
|
||||
except fw_pack.FwPackError as exc:
|
||||
sys.exit(f'{exc}')
|
||||
found = {}
|
||||
for path in args.pack:
|
||||
if not os.path.isfile(path):
|
||||
sys.exit(f'找不到文件: {path}')
|
||||
try:
|
||||
slot = fw_pack.detect_slot(open(path, 'rb').read(), profile)
|
||||
except fw_pack.FwPackError as exc:
|
||||
sys.exit(f'{path}: {exc}')
|
||||
if slot in found:
|
||||
sys.exit(f'{path} 与 {found[slot]} 都被识别为 {slot.upper()} 包, '
|
||||
f'一个槽只能有一个输入')
|
||||
found[slot] = path
|
||||
a, b = found.get('a'), found.get('b')
|
||||
if not a and not b:
|
||||
sys.exit('未提供可识别的固件输入 (A 包或 B 包)')
|
||||
try:
|
||||
fw_pack.pack_all(args.pack_profile, a, b_path=b,
|
||||
bl_path=args.pack_bl or None, out_dir=args.output,
|
||||
make_full=not args.pack_no_full)
|
||||
except fw_pack.FwPackError as exc:
|
||||
sys.exit(f'打包失败: {exc}')
|
||||
return 0
|
||||
|
||||
|
||||
def _ensure_parent(path):
|
||||
parent = os.path.dirname(os.path.abspath(path))
|
||||
if parent:
|
||||
@@ -145,9 +186,18 @@ def main():
|
||||
op.add_argument('--pad', metavar='FILE', dest='pad', help='补齐 BIN 到页边界')
|
||||
op.add_argument('--merge', nargs=2, metavar=('BL', 'APP'),
|
||||
help='拼接 Bootloader + APP → 整片镜像')
|
||||
op.add_argument('--pack', nargs='+', metavar='BIN',
|
||||
help='v2 双固件打包: A/B 包自动识别 → UPDATE/B/FULL+MD5')
|
||||
ap.add_argument('--pack-profile', default='ceramixnano_mb_v51',
|
||||
help='--pack 布局 profile(内置名或 JSON 路径)')
|
||||
ap.add_argument('--pack-bl', default=None, metavar='BIN',
|
||||
help='--pack 使用的 Bootloader bin')
|
||||
ap.add_argument('--pack-no-full', action='store_true',
|
||||
help='--pack 只出 UPDATE/B 包, 不拼 FULL')
|
||||
ap.add_argument('--align', default='1024', help='--pad 对齐字节(默认 1024)')
|
||||
ap.add_argument('--pad-byte', default='0xFF', help='--pad 填充字节(默认 0xFF)')
|
||||
ap.add_argument('-o', '--output', help='--pad/--merge 输出路径(默认自动命名)')
|
||||
ap.add_argument('-o', '--output',
|
||||
help='输出路径: --pad/--merge 为文件, --pack 为目录(默认自动命名)')
|
||||
ap.add_argument('--app-addr', default='0x%08X' % APP_ADDR_DEFAULT,
|
||||
help='--merge APP 地址(默认 0x08002000)')
|
||||
ap.add_argument('--flag-addr', default='0x%08X' % FLAG_ADDR_DEFAULT,
|
||||
@@ -174,6 +224,8 @@ def main():
|
||||
ap.add_argument('--scripts', help='OpenOCD scripts 目录(默认自动定位)')
|
||||
args = ap.parse_args()
|
||||
|
||||
if args.pack:
|
||||
sys.exit(run_pack(args))
|
||||
if args.pad:
|
||||
sys.exit(run_pad(args))
|
||||
if args.merge:
|
||||
|
||||
Reference in New Issue
Block a user