refactor(flash-gui): 对齐 DLPilot 重构 GUI 并内置 OpenOCD 发布链
This commit is contained in:
@@ -0,0 +1,664 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
DAPLink Flash 主界面 — CustomTkinter 双栏仪表盘
|
||||
================================================================
|
||||
布局:
|
||||
左列 探针与工具链 | 芯片与固件 | 操作(检测/烧录/擦除/读取/复位/停止)
|
||||
右列 补齐 Pad(Y-Modem 对齐) | 拼接 Merge(BL+APP+标志) | 运行日志
|
||||
默认跟随系统深/浅色外观, 顶栏可切换 Dark/Light/System。
|
||||
|
||||
本模块只管界面: OpenOCD 子进程与命令构建在 openocd_runner,
|
||||
固件补齐/拼接在 fw_image, 运行配置在 config。
|
||||
"""
|
||||
|
||||
import ctypes
|
||||
import os
|
||||
import queue
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import tkinter as tk
|
||||
from tkinter import filedialog, messagebox
|
||||
|
||||
import customtkinter as ctk
|
||||
|
||||
import config
|
||||
import openocd_runner
|
||||
from fw_image import (CHIP_PROFILES, FLAG_VALUE_DEFAULT, FLASH_BASE_DEFAULT,
|
||||
merge_firmware, pad_bin, parse_int)
|
||||
|
||||
MONO = ('Consolas', 12)
|
||||
BOLD = ('Microsoft YaHei UI', 13, 'bold')
|
||||
TITLE_FONT = ('Microsoft YaHei UI', 18, 'bold')
|
||||
|
||||
APP_NAME = 'DAPLink Flash'
|
||||
APP_VERSION = '2.0.0'
|
||||
REPO_URL = 'https://gitea.hulk.wang/hulk/gd32e230f8_firmware_merge_tool'
|
||||
|
||||
OK_BLUE = '#3B8ED0'
|
||||
|
||||
|
||||
def get_icon_path():
|
||||
"""工具图标: 源码模式取 logos/ 下, 打包模式取 exe 内嵌资源(_MEIPASS)"""
|
||||
if getattr(sys, 'frozen', False):
|
||||
p = os.path.join(getattr(sys, '_MEIPASS', ''), 'logo_flash.ico')
|
||||
else:
|
||||
p = os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
||||
'logos', 'logo_flash.ico')
|
||||
return p if os.path.exists(p) else None
|
||||
|
||||
|
||||
def _apply_dropdown_theme():
|
||||
"""CTk 默认下拉菜单是白底, 在深色界面里风格突兀; 改为随外观自适应。
|
||||
在创建任何控件前调用, 对全部 CTkComboBox/菜单生效。"""
|
||||
ctk.ThemeManager.theme['DropdownMenu']['fg_color'] = ('#F2F2F2', '#2B2B2B')
|
||||
ctk.ThemeManager.theme['DropdownMenu']['hover_color'] = ('#D8D8D8', '#3C3C3C')
|
||||
ctk.ThemeManager.theme['DropdownMenu']['text_color'] = ('#1A1A1A', '#DADADA')
|
||||
|
||||
|
||||
def run_gui(auto_close=0):
|
||||
"""入口: 配置外观并启动主窗口"""
|
||||
ctk.set_appearance_mode('System')
|
||||
_apply_dropdown_theme()
|
||||
App(auto_close).mainloop()
|
||||
|
||||
|
||||
class App(ctk.CTk):
|
||||
def __init__(self, auto_close=0):
|
||||
super().__init__()
|
||||
self.title(f'{APP_NAME} v{APP_VERSION} — GD32 固件烧录 · 补齐 · 镜像拼接')
|
||||
try: # 任务栏独立分组
|
||||
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(
|
||||
'hulk.DAPLinkFlash')
|
||||
except Exception:
|
||||
pass
|
||||
self.geometry('1360x880')
|
||||
self.minsize(1200, 780)
|
||||
ic = get_icon_path()
|
||||
if ic:
|
||||
try:
|
||||
self.iconbitmap(ic) # 窗口标题栏图标
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
self.cfg = config.load_settings()
|
||||
self._toolchain_source = '已配置'
|
||||
if not os.path.isfile(self.cfg['openocd']):
|
||||
# 未配置或配置指向的路径不存在 → 自动定位(内置/本机/PATH)
|
||||
exe, scripts, self._toolchain_source = openocd_runner.locate_toolchain()
|
||||
self.cfg['openocd'], self.cfg['scripts'] = exe, scripts
|
||||
self.v = {}
|
||||
for key, val in self.cfg.items():
|
||||
self.v[key] = tk.BooleanVar(value=val) if isinstance(val, bool) \
|
||||
else tk.StringVar(value=val)
|
||||
for key in ('openocd', 'scripts'):
|
||||
self.v[key].trace_add('write', lambda *_a: self._mark_custom_toolchain())
|
||||
|
||||
self._log_q = queue.Queue()
|
||||
self.proc = None
|
||||
self._poll_job = None
|
||||
self._op_buttons = []
|
||||
|
||||
self._build()
|
||||
self._log(f'{APP_NAME} v{APP_VERSION} — OpenOCD 烧录 · 固件补齐 · 镜像拼接')
|
||||
ver = openocd_runner.version(self.cfg['openocd'])
|
||||
self._log('工具链: %s (%s)%s' % (self.cfg['openocd'], self._toolchain_source,
|
||||
' ' + ver if ver else ''))
|
||||
self._poll()
|
||||
self.protocol('WM_DELETE_WINDOW', self._on_close)
|
||||
if auto_close > 0:
|
||||
self.after(auto_close * 1000, self._on_close)
|
||||
|
||||
# ============================================================
|
||||
# 布局
|
||||
# ============================================================
|
||||
def _set_appearance(self, mode):
|
||||
ctk.set_appearance_mode(mode)
|
||||
|
||||
def _card(self, parent, title, button=None, command=None, right_label=None):
|
||||
"""卡片容器: 圆角边框 + 标题行(可带右侧小按钮/右对齐标签)。
|
||||
卡片自身用 grid 参与列布局; 卡片内部一律用 pack。"""
|
||||
card = ctk.CTkFrame(parent, corner_radius=10, border_width=1,
|
||||
border_color=('gray70', 'gray30'))
|
||||
head = ctk.CTkFrame(card, fg_color='transparent')
|
||||
head.pack(fill='x', padx=12, pady=(8, 0))
|
||||
ctk.CTkLabel(head, text=title, font=BOLD, anchor='w').pack(side='left', fill='y')
|
||||
if right_label is not None:
|
||||
ctk.CTkLabel(head, textvariable=right_label,
|
||||
text_color='gray60').pack(side='right', fill='y')
|
||||
if button:
|
||||
ctk.CTkButton(head, text=button, width=72,
|
||||
command=command).pack(side='right', fill='y')
|
||||
content = ctk.CTkFrame(card, fg_color='transparent')
|
||||
content.pack(fill='both', expand=True, padx=12, pady=(2, 10))
|
||||
return card, content
|
||||
|
||||
def _build(self):
|
||||
top = ctk.CTkFrame(self, fg_color='transparent')
|
||||
top.pack(fill='x', padx=12, pady=(10, 2))
|
||||
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))
|
||||
seg = ctk.CTkSegmentedButton(top, values=['Dark', 'Light', 'System'],
|
||||
command=self._set_appearance)
|
||||
seg.set('System')
|
||||
seg.pack(side='right')
|
||||
ctk.CTkButton(top, text='关于', width=48, height=24,
|
||||
fg_color='transparent', border_width=1,
|
||||
border_color=('gray55', 'gray35'),
|
||||
text_color='gray60', hover_color=('gray82', 'gray22'),
|
||||
command=self._show_about).pack(side='right', padx=(0, 12))
|
||||
|
||||
body = ctk.CTkFrame(self, fg_color='transparent')
|
||||
body.pack(fill='both', expand=True, padx=12, pady=(4, 12))
|
||||
body.grid_columnconfigure(0, weight=1, uniform='col')
|
||||
body.grid_columnconfigure(1, weight=1, uniform='col')
|
||||
body.grid_rowconfigure(0, weight=1)
|
||||
left = ctk.CTkFrame(body, fg_color='transparent')
|
||||
left.grid(row=0, column=0, sticky='nsew')
|
||||
right = ctk.CTkFrame(body, fg_color='transparent')
|
||||
right.grid(row=0, column=1, sticky='nsew', padx=(10, 0))
|
||||
self._build_left(left)
|
||||
self._build_right(right)
|
||||
|
||||
status = ctk.CTkFrame(self, fg_color='transparent')
|
||||
status.pack(fill='x', padx=14, pady=(0, 8))
|
||||
self.v_status = tk.StringVar(value='状态: 就绪')
|
||||
ctk.CTkLabel(status, textvariable=self.v_status, anchor='w').pack(side='left')
|
||||
ctk.CTkLabel(status, text=f'v{APP_VERSION}',
|
||||
text_color='gray60').pack(side='right')
|
||||
|
||||
# -- 左列: 探针与工具链 / 芯片与固件 / 操作 --
|
||||
def _build_left(self, col):
|
||||
col.grid_rowconfigure(2, weight=1) # 操作卡片吸底
|
||||
col.grid_columnconfigure(0, weight=1)
|
||||
|
||||
self.v_toolchain = tk.StringVar(
|
||||
value='工具链: ' + self._toolchain_source)
|
||||
card, c = self._card(col, '探针与工具链', button='自动定位',
|
||||
command=self._relocate_toolchain,
|
||||
right_label=self.v_toolchain)
|
||||
card.grid(row=0, column=0, sticky='ew')
|
||||
self._field(c, 'OpenOCD 路径', self.v['openocd'], self._browse_openocd)
|
||||
self._field(c, '脚本目录', self.v['scripts'], self._browse_scripts)
|
||||
probe, _w = self._field_pair(
|
||||
c, '烧录器', self.v['probe'], 'Interface', self.v['interface'],
|
||||
cls1=(ctk.CTkComboBox, {'state': 'readonly',
|
||||
'values': list(openocd_runner.PROBE_PROFILES)}),
|
||||
cls2=(ctk.CTkComboBox,
|
||||
{'values': sorted(set(openocd_runner.PROBE_PROFILES.values()) - {''})}))
|
||||
probe.configure(command=lambda _=None: self._on_probe_change())
|
||||
self._field_pair(c, '速度 (kHz)', self.v['speed'], '探针序列号',
|
||||
self.v['serial'], width2=150)
|
||||
self._field_pair(
|
||||
c, 'Target', self.v['target'], '额外命令', self.v['extra'],
|
||||
cls1=(ctk.CTkComboBox, {'values': ['target/gd32e23x.cfg']}))
|
||||
|
||||
card, c = self._card(col, '芯片与固件')
|
||||
card.grid(row=1, column=0, sticky='ew', pady=(10, 0))
|
||||
chip, _flash = self._field_pair(
|
||||
c, '芯片型号', self.v['chip'], 'Flash 大小', self.v['flash_size'],
|
||||
cls1=(ctk.CTkComboBox, {'state': 'readonly',
|
||||
'values': list(CHIP_PROFILES)}),
|
||||
width2=100)
|
||||
chip.configure(command=lambda _=None: self._on_chip_change())
|
||||
_flash.configure(state='disabled')
|
||||
self._field(c, '固件文件', self.v['firmware'], self._browse_firmware)
|
||||
self._field(c, '基地址 (仅 BIN)', self.v['base'])
|
||||
ctk.CTkLabel(c, text='HEX/ELF 自动使用文件内地址; 完整镜像 0x08000000, '
|
||||
'单独烧 APP 0x08002000',
|
||||
text_color='gray50', justify='left', anchor='w',
|
||||
).pack(fill='x', pady=(2, 0))
|
||||
opts = ctk.CTkFrame(c, fg_color='transparent')
|
||||
opts.pack(fill='x', pady=(6, 0))
|
||||
for key, name in (('erase', '烧录前擦除'), ('verify', '烧录后校验'),
|
||||
('reset', '烧录后复位运行')):
|
||||
ctk.CTkCheckBox(opts, text=name, variable=self.v[key],
|
||||
checkbox_width=18, checkbox_height=18).pack(
|
||||
side='left', padx=(0, 14))
|
||||
|
||||
card, c = self._card(col, '操作')
|
||||
card.grid(row=2, column=0, sticky='sew', pady=(10, 0))
|
||||
grid = ctk.CTkFrame(c, fg_color='transparent')
|
||||
grid.pack(fill='x')
|
||||
grid.grid_columnconfigure((0, 1, 2), weight=1, uniform='op')
|
||||
for i, (name, cmd) in enumerate((
|
||||
('检测设备', self._on_detect), ('烧录固件', self._on_flash),
|
||||
('整片擦除', self._on_erase), ('读取 Flash', self._on_read),
|
||||
('复位芯片', self._on_reset))):
|
||||
btn = ctk.CTkButton(grid, text=name, height=34, command=cmd)
|
||||
btn.grid(row=i // 3, column=i % 3, sticky='ew', padx=3, pady=3)
|
||||
self._op_buttons.append(btn)
|
||||
self.btn_stop = ctk.CTkButton(grid, text='停止', height=34,
|
||||
fg_color='gray40', hover_color='gray25',
|
||||
command=self._on_stop, state='disabled')
|
||||
self.btn_stop.grid(row=1, column=2, sticky='ew', padx=3, pady=3)
|
||||
|
||||
# -- 右列: 补齐 / 拼接 / 日志 --
|
||||
def _build_right(self, col):
|
||||
col.grid_rowconfigure(2, weight=1) # 日志卡片吃掉剩余高度
|
||||
col.grid_columnconfigure(0, weight=1)
|
||||
|
||||
card, c = self._card(col, '补齐 (Pad) — 尾部补 0xFF 到页边界, '
|
||||
'适配 Bootloader Y-Modem 整包接收')
|
||||
card.grid(row=0, column=0, sticky='ew')
|
||||
self._field(c, '输入 BIN', self.v['pad_in'], self._browse_pad_in)
|
||||
self._field(c, '输出 BIN', self.v['pad_out'], self._browse_pad_out)
|
||||
row = ctk.CTkFrame(c, fg_color='transparent')
|
||||
row.pack(fill='x', pady=(6, 0))
|
||||
ctk.CTkLabel(row, text='对齐:', width=40, anchor='e').pack(side='left')
|
||||
ctk.CTkComboBox(row, width=86, state='readonly',
|
||||
values=['1024', '2048', '4096'],
|
||||
variable=self.v['pad_align']).pack(side='left', padx=(4, 12))
|
||||
ctk.CTkLabel(row, text='填充字节:', width=64, anchor='e').pack(side='left')
|
||||
ctk.CTkEntry(row, width=76, textvariable=self.v['pad_byte']
|
||||
).pack(side='left', padx=(4, 12))
|
||||
ctk.CTkButton(row, text='执行补齐', width=96,
|
||||
command=self._on_pad).pack(side='left')
|
||||
self.v_pad_result = tk.StringVar(value='')
|
||||
ctk.CTkLabel(c, textvariable=self.v_pad_result, text_color=OK_BLUE,
|
||||
justify='left', anchor='w').pack(fill='x', pady=(4, 0))
|
||||
|
||||
card, c = self._card(col, '拼接 (Merge) — Bootloader + APP + 有效标志 → 整片镜像')
|
||||
card.grid(row=1, column=0, sticky='ew', pady=(10, 0))
|
||||
self._field(c, 'Bootloader', self.v['bootloader'], self._browse_bl)
|
||||
self._field(c, 'Application', self.v['app'], self._browse_app)
|
||||
self._field(c, '输出文件', self.v['merge_out'], self._browse_merge_out)
|
||||
row = ctk.CTkFrame(c, fg_color='transparent')
|
||||
row.pack(fill='x', pady=(6, 0))
|
||||
for label, key, w in (('APP 地址', 'app_addr', 104),
|
||||
('标志地址', 'flag_addr', 104),
|
||||
('Flash', 'merge_flash', 84),
|
||||
('标志值', 'flag_value', 104)):
|
||||
ctk.CTkLabel(row, text=label + ':', anchor='e').pack(side='left')
|
||||
ctk.CTkEntry(row, width=w, textvariable=self.v[key]
|
||||
).pack(side='left', padx=(4, 10))
|
||||
ctk.CTkButton(row, text='执行拼接', width=96,
|
||||
command=self._on_merge).pack(side='left')
|
||||
self.v_merge_result = tk.StringVar(value='')
|
||||
ctk.CTkLabel(c, textvariable=self.v_merge_result, text_color=OK_BLUE,
|
||||
justify='left', anchor='w').pack(fill='x', pady=(4, 0))
|
||||
ctk.CTkLabel(c, text='典型布局: 0x08000000 Bootloader 8KB | 0x08002000 APP | '
|
||||
'0x0800FFFC 有效标志 0xEEEEEEEE | Flash 64KB',
|
||||
text_color='gray50', justify='left', anchor='w',
|
||||
).pack(fill='x', pady=(2, 0))
|
||||
|
||||
card, c = self._card(col, '运行日志 — OpenOCD 输出与固件工具结果',
|
||||
button='清空', command=self._clear_log)
|
||||
card.grid(row=2, column=0, sticky='nsew', pady=(10, 0))
|
||||
self.txt_log = ctk.CTkTextbox(c, font=MONO)
|
||||
self.txt_log.pack(fill='both', expand=True)
|
||||
self.txt_log.configure(state='disabled')
|
||||
|
||||
# ============================================================
|
||||
# 输入行辅助
|
||||
# ============================================================
|
||||
@staticmethod
|
||||
def _mk_widget(parent, cls_info, var):
|
||||
"""按控件类绑定变量: CTkComboBox 用 variable, 其余用 textvariable"""
|
||||
cls, kw = cls_info
|
||||
if issubclass(cls, ctk.CTkComboBox):
|
||||
return cls(parent, variable=var, **kw)
|
||||
return cls(parent, textvariable=var, **kw)
|
||||
|
||||
def _field(self, parent, label, var, browse=None):
|
||||
"""一行: 标签 + 输入框(+ 浏览按钮), pack 布局"""
|
||||
row = ctk.CTkFrame(parent, fg_color='transparent')
|
||||
row.pack(fill='x', pady=(4, 0))
|
||||
ctk.CTkLabel(row, text=label, width=150, anchor='w').pack(side='left')
|
||||
ctk.CTkEntry(row, textvariable=var).pack(side='left', fill='x', expand=True)
|
||||
if browse:
|
||||
ctk.CTkButton(row, text='浏览', width=56,
|
||||
command=browse).pack(side='left', padx=(6, 0))
|
||||
return row
|
||||
|
||||
def _field_pair(self, parent, label1, var1, label2, var2,
|
||||
cls1=(ctk.CTkEntry, {}), cls2=(ctk.CTkEntry, {}),
|
||||
width2=None):
|
||||
"""一行放两个字段: label1[控件1] label2[控件2(可定宽)]"""
|
||||
row = ctk.CTkFrame(parent, fg_color='transparent')
|
||||
row.pack(fill='x', pady=(4, 0))
|
||||
ctk.CTkLabel(row, text=label1, width=150, anchor='w').pack(side='left')
|
||||
w1 = self._mk_widget(row, cls1, var1)
|
||||
w1.pack(side='left', fill='x', expand=True)
|
||||
ctk.CTkLabel(row, text=label2, width=90, anchor='e').pack(side='left')
|
||||
if width2:
|
||||
cls2 = (cls2[0], {**cls2[1], 'width': width2})
|
||||
w2 = self._mk_widget(row, cls2, var2)
|
||||
w2.pack(side='left', padx=(4, 0))
|
||||
return w1, w2
|
||||
|
||||
# ============================================================
|
||||
# 路径选择与联动
|
||||
# ============================================================
|
||||
def _browse_openocd(self):
|
||||
path = filedialog.askopenfilename(
|
||||
title='选择 OpenOCD', filetypes=[('OpenOCD', 'openocd.exe'), ('所有文件', '*.*')])
|
||||
if path:
|
||||
self.v['openocd'].set(path)
|
||||
|
||||
def _browse_scripts(self):
|
||||
path = filedialog.askdirectory(title='选择 OpenOCD scripts 目录')
|
||||
if path:
|
||||
self.v['scripts'].set(path)
|
||||
|
||||
def _relocate_toolchain(self):
|
||||
exe, scripts, source = openocd_runner.locate_toolchain()
|
||||
self.v['openocd'].set(exe)
|
||||
self.v['scripts'].set(scripts)
|
||||
self._toolchain_source = source
|
||||
self.v_toolchain.set('工具链: ' + source)
|
||||
ver = openocd_runner.version(exe)
|
||||
self._log('工具链定位: %s (%s)%s' % (exe, source, ' ' + ver if ver else ''))
|
||||
|
||||
def _mark_custom_toolchain(self, *_a):
|
||||
if self._toolchain_source != '自定义':
|
||||
self._toolchain_source = '自定义'
|
||||
self.v_toolchain.set('工具链: 自定义')
|
||||
|
||||
def _browse_firmware(self):
|
||||
path = filedialog.askopenfilename(
|
||||
title='选择固件',
|
||||
filetypes=[('固件文件', '*.bin *.hex *.elf'), ('所有文件', '*.*')])
|
||||
if path:
|
||||
self.v['firmware'].set(path)
|
||||
|
||||
def _browse_pad_in(self):
|
||||
path = filedialog.askopenfilename(
|
||||
title='选择需要补齐的 BIN', filetypes=[('BIN 文件', '*.bin'), ('所有文件', '*.*')])
|
||||
if path:
|
||||
self.v['pad_in'].set(path)
|
||||
name, ext = os.path.splitext(path)
|
||||
self.v['pad_out'].set(name + '_pad' + ext)
|
||||
|
||||
def _browse_pad_out(self):
|
||||
path = filedialog.asksaveasfilename(
|
||||
title='保存补齐后的 BIN', defaultextension='.bin',
|
||||
filetypes=[('BIN 文件', '*.bin')])
|
||||
if path:
|
||||
self.v['pad_out'].set(path)
|
||||
|
||||
def _browse_bl(self):
|
||||
path = filedialog.askopenfilename(
|
||||
title='选择 Bootloader BIN', filetypes=[('BIN 文件', '*.bin'), ('所有文件', '*.*')])
|
||||
if path:
|
||||
self.v['bootloader'].set(path)
|
||||
self._suggest_merge_output()
|
||||
|
||||
def _browse_app(self):
|
||||
path = filedialog.askopenfilename(
|
||||
title='选择 APP BIN', filetypes=[('BIN 文件', '*.bin'), ('所有文件', '*.*')])
|
||||
if path:
|
||||
self.v['app'].set(path)
|
||||
self._suggest_merge_output()
|
||||
|
||||
def _suggest_merge_output(self):
|
||||
if self.v['app'].get():
|
||||
name, ext = os.path.splitext(os.path.basename(self.v['app'].get()))
|
||||
self.v['merge_out'].set(
|
||||
os.path.join(os.path.dirname(self.v['app'].get()), name + '_BL' + ext))
|
||||
|
||||
def _browse_merge_out(self):
|
||||
path = filedialog.asksaveasfilename(
|
||||
title='保存拼接后的镜像', defaultextension='.bin',
|
||||
filetypes=[('BIN 文件', '*.bin')])
|
||||
if path:
|
||||
self.v['merge_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()))
|
||||
|
||||
def _on_probe_change(self, *_a):
|
||||
cfg = openocd_runner.PROBE_PROFILES.get(self.v['probe'].get(), '')
|
||||
if cfg:
|
||||
self.v['interface'].set(cfg)
|
||||
|
||||
# ============================================================
|
||||
# OpenOCD 烧录操作
|
||||
# ============================================================
|
||||
def _check_tools(self):
|
||||
if not os.path.isfile(self.v['openocd'].get()):
|
||||
messagebox.showerror('缺少工具',
|
||||
'找不到 OpenOCD:\n%s\n\n'
|
||||
'可点"自动定位"使用内置/本机工具链。'
|
||||
% self.v['openocd'].get())
|
||||
return False
|
||||
if not os.path.isdir(self.v['scripts'].get()):
|
||||
messagebox.showerror('缺少工具',
|
||||
'找不到 OpenOCD scripts 目录:\n%s' % self.v['scripts'].get())
|
||||
return False
|
||||
return True
|
||||
|
||||
def _build_cmd(self, script):
|
||||
return openocd_runner.build_cmd(
|
||||
self.v['openocd'].get(), self.v['scripts'].get(),
|
||||
self.v['interface'].get(), self.v['target'].get(),
|
||||
self.v['speed'].get(), self.v['serial'].get(),
|
||||
self.v['extra'].get(), script)
|
||||
|
||||
def _start(self, title, script):
|
||||
if self.proc is not None:
|
||||
self._log('已有操作正在执行, 请先停止。')
|
||||
return
|
||||
if not self._check_tools():
|
||||
return
|
||||
cmd = self._build_cmd(script)
|
||||
self._busy(True)
|
||||
self._log('== %s ==' % title)
|
||||
self._log('命令: ' + subprocess.list2cmdline(cmd))
|
||||
self.proc = openocd_runner.OcdProcess(
|
||||
cmd,
|
||||
on_line=lambda line: self._log_q.put(('log', line)),
|
||||
on_done=lambda code: self._log_q.put(('done', (title, code))))
|
||||
self.proc.start()
|
||||
|
||||
def _on_detect(self):
|
||||
self._start('检测设备', openocd_runner.script_detect())
|
||||
|
||||
def _on_erase(self):
|
||||
try:
|
||||
self._start('整片擦除', openocd_runner.script_erase(self.v['flash_size'].get()))
|
||||
except ValueError as exc:
|
||||
messagebox.showerror('参数错误', str(exc))
|
||||
|
||||
def _on_flash(self):
|
||||
filepath = self.v['firmware'].get().strip()
|
||||
if not filepath or not os.path.isfile(filepath):
|
||||
messagebox.showerror('缺少固件', '请先选择固件文件。')
|
||||
return
|
||||
try:
|
||||
script = openocd_runner.script_flash(
|
||||
filepath, self.v['base'].get(),
|
||||
erase=self.v['erase'].get(), verify=self.v['verify'].get(),
|
||||
reset=self.v['reset'].get())
|
||||
except ValueError as exc:
|
||||
messagebox.showerror('参数错误', str(exc))
|
||||
return
|
||||
self._start('烧录固件', script)
|
||||
|
||||
def _on_read(self):
|
||||
out_path = filedialog.asksaveasfilename(
|
||||
title='保存 Flash 内容', defaultextension='.bin',
|
||||
initialfile='flash_dump.bin', filetypes=[('BIN 文件', '*.bin')])
|
||||
if not out_path:
|
||||
return
|
||||
try:
|
||||
self._start('读取 Flash',
|
||||
openocd_runner.script_read(out_path, self.v['flash_size'].get()))
|
||||
except ValueError as exc:
|
||||
messagebox.showerror('参数错误', str(exc))
|
||||
|
||||
def _on_reset(self):
|
||||
self._start('复位芯片', openocd_runner.script_reset())
|
||||
|
||||
def _on_stop(self):
|
||||
if self.proc is not None:
|
||||
self.proc.terminate()
|
||||
self._log('已发送停止请求。')
|
||||
else:
|
||||
self._log('当前没有正在执行的操作。')
|
||||
|
||||
def _poll(self):
|
||||
"""工作线程 → GUI 主线程: 80ms 轮询日志队列并同步忙碌状态"""
|
||||
try:
|
||||
while True:
|
||||
kind, payload = self._log_q.get_nowait()
|
||||
if kind == 'log':
|
||||
self._log(payload)
|
||||
elif kind == 'done':
|
||||
title, code = payload
|
||||
if code == 0:
|
||||
self._log('[完成] %s' % title)
|
||||
self._set_status('%s 完成' % title)
|
||||
else:
|
||||
self._log('[失败] %s (退出码 %d)' % (title, code))
|
||||
self._set_status('%s 失败' % title)
|
||||
self._busy(False)
|
||||
self._save()
|
||||
except queue.Empty:
|
||||
pass
|
||||
self._poll_job = self.after(80, self._poll)
|
||||
|
||||
def _busy(self, busy):
|
||||
state = 'disabled' if busy else 'normal'
|
||||
for btn in self._op_buttons:
|
||||
btn.configure(state=state)
|
||||
self.btn_stop.configure(state='normal' if busy else 'disabled')
|
||||
if busy:
|
||||
self._set_status('正在执行...')
|
||||
else:
|
||||
self._set_status('就绪')
|
||||
|
||||
# ============================================================
|
||||
# 固件工具: 补齐 / 拼接
|
||||
# ============================================================
|
||||
def _on_pad(self):
|
||||
src = self.v['pad_in'].get().strip()
|
||||
dst = self.v['pad_out'].get().strip()
|
||||
align = parse_int(self.v['pad_align'].get())
|
||||
pad_byte = parse_int(self.v['pad_byte'].get())
|
||||
if not src or not os.path.isfile(src):
|
||||
messagebox.showerror('缺少文件', '请选择需要补齐的 BIN。')
|
||||
return
|
||||
if not dst:
|
||||
messagebox.showerror('缺少文件', '请设置输出文件。')
|
||||
return
|
||||
if align is None or align <= 0:
|
||||
messagebox.showerror('参数错误', '对齐字节无效。')
|
||||
return
|
||||
if pad_byte is None:
|
||||
pad_byte = 0xFF
|
||||
try:
|
||||
original = os.path.getsize(src)
|
||||
padded = pad_bin(src, dst, align, pad_byte)
|
||||
self.v_pad_result.set(
|
||||
'原始 %d B → 补齐后 %d B (+%d), 对齐 %d B'
|
||||
% (original, padded, padded - original, align))
|
||||
self._log('补齐完成: %s → %s' % (src, dst))
|
||||
self._set_status('补齐完成')
|
||||
self._save()
|
||||
except Exception as exc:
|
||||
messagebox.showerror('补齐失败', str(exc))
|
||||
self._log('补齐失败: %s' % exc)
|
||||
|
||||
def _on_merge(self):
|
||||
bl = self.v['bootloader'].get().strip()
|
||||
app = self.v['app'].get().strip()
|
||||
out = self.v['merge_out'].get().strip()
|
||||
app_addr = parse_int(self.v['app_addr'].get())
|
||||
flag_addr = parse_int(self.v['flag_addr'].get())
|
||||
flash_size = parse_int(self.v['merge_flash'].get())
|
||||
flag_value = parse_int(self.v['flag_value'].get())
|
||||
if not bl or not os.path.isfile(bl):
|
||||
messagebox.showerror('缺少文件', '请选择 Bootloader BIN。')
|
||||
return
|
||||
if not app or not os.path.isfile(app):
|
||||
messagebox.showerror('缺少文件', '请选择 APP BIN。')
|
||||
return
|
||||
if not out:
|
||||
messagebox.showerror('缺少文件', '请设置输出文件。')
|
||||
return
|
||||
if app_addr is None or flag_addr is None or flash_size is None:
|
||||
messagebox.showerror('参数错误', '地址或 Flash 大小无效。')
|
||||
return
|
||||
if flag_value is None:
|
||||
flag_value = FLAG_VALUE_DEFAULT
|
||||
try:
|
||||
info = merge_firmware(bl, app, out, app_addr, flag_addr,
|
||||
flash_size, flag_value)
|
||||
self.v_merge_result.set(
|
||||
'BL %d B @0x%08X | APP %d B @0x%08X | 镜像 %d B | 标志 @0x%08X'
|
||||
% (info['bootloader_size'], FLASH_BASE_DEFAULT, info['app_size'],
|
||||
info['app_addr'], info['output_size'], info['flag_addr']))
|
||||
self._log('拼接完成: %s' % out)
|
||||
self._set_status('拼接完成')
|
||||
self._save()
|
||||
except Exception as exc:
|
||||
messagebox.showerror('拼接失败', str(exc))
|
||||
self._log('拼接失败: %s' % exc)
|
||||
|
||||
# ============================================================
|
||||
# 日志 / 状态 / 配置 / 退出
|
||||
# ============================================================
|
||||
def _log(self, line):
|
||||
self.txt_log.configure(state='normal')
|
||||
self.txt_log.insert('end', time_stamp() + line + '\n')
|
||||
if int(self.txt_log.index('end-1c').split('.')[0]) > 800:
|
||||
self.txt_log.delete('1.0', '2.0') # 只保留最近约 800 行
|
||||
self.txt_log.see('end')
|
||||
self.txt_log.configure(state='disabled')
|
||||
|
||||
def _clear_log(self):
|
||||
self.txt_log.configure(state='normal')
|
||||
self.txt_log.delete('1.0', 'end')
|
||||
self.txt_log.configure(state='disabled')
|
||||
|
||||
def _set_status(self, text):
|
||||
self.v_status.set('状态: ' + text)
|
||||
|
||||
def _save(self):
|
||||
for key, var in self.v.items():
|
||||
self.cfg[key] = var.get()
|
||||
self.cfg['openocd'] = self.v['openocd'].get()
|
||||
config.save_settings(self.cfg)
|
||||
|
||||
def _show_about(self):
|
||||
dlg = ctk.CTkToplevel(self)
|
||||
dlg.title(f'关于 {APP_NAME}')
|
||||
dlg.resizable(False, False)
|
||||
dlg.after(150, dlg.grab_set())
|
||||
box = ctk.CTkFrame(dlg, fg_color='transparent')
|
||||
box.pack(padx=26, pady=20)
|
||||
ctk.CTkLabel(box, text=APP_NAME, font=TITLE_FONT, anchor='w').pack(anchor='w')
|
||||
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)',
|
||||
'补齐 Pad: BIN 尾部补 0xFF 对齐页边界, 适配 Bootloader Y-Modem',
|
||||
'拼接 Merge: Bootloader + APP + 有效标志 → 整片 Flash 镜像',
|
||||
'发布包内置 OpenOCD 工具链, 免安装解压即用',
|
||||
'作者: Hulk'):
|
||||
ctk.CTkLabel(box, text='· ' + line, anchor='w').pack(anchor='w', pady=1)
|
||||
ctk.CTkLabel(box, text='仓库: ' + REPO_URL, anchor='w',
|
||||
text_color=OK_BLUE).pack(anchor='w', pady=(12, 0))
|
||||
ctk.CTkButton(box, text='关闭', width=84,
|
||||
command=dlg.destroy).pack(anchor='e', pady=(14, 0))
|
||||
|
||||
def _on_close(self):
|
||||
if self.proc is not None:
|
||||
if not messagebox.askyesno('确认退出', '烧录操作仍在进行, 是否停止并退出?'):
|
||||
return
|
||||
self.proc.terminate()
|
||||
if self._poll_job:
|
||||
try:
|
||||
self.after_cancel(self._poll_job)
|
||||
except Exception:
|
||||
pass
|
||||
self._poll_job = None
|
||||
self._save()
|
||||
self.destroy()
|
||||
|
||||
|
||||
def time_stamp():
|
||||
return time.strftime('%H:%M:%S ')
|
||||
Reference in New Issue
Block a user