Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9a5d00ecf5 | ||
|
|
337e53d099 |
+24
-13
@@ -9,7 +9,7 @@
|
|||||||
> (Windows 10/11 x64,免安装、无需 Python,**内置 OpenOCD 工具链**)。
|
> (Windows 10/11 x64,免安装、无需 Python,**内置 OpenOCD 工具链**)。
|
||||||
> - **要改代码/重新打包**:见 §5、§6。
|
> - **要改代码/重新打包**:见 §5、§6。
|
||||||
>
|
>
|
||||||
> 界面与构建方式对齐 DLPilot 项目(CustomTkinter 深色双栏仪表盘 +
|
> 界面与构建方式对齐 DLPilot 项目(CustomTkinter 深色卡片仪表盘 +
|
||||||
> PyInstaller onefile + Gitea Release 发布)。更新:2026-09-14
|
> PyInstaller onefile + Gitea Release 发布)。更新:2026-09-14
|
||||||
|
|
||||||
## 1. 功能总览
|
## 1. 功能总览
|
||||||
@@ -31,28 +31,38 @@
|
|||||||
1. 打开 [Releases 页面](https://gitea.hulk.wang/hulk/gd32e230f8_firmware_merge_tool/releases),
|
1. 打开 [Releases 页面](https://gitea.hulk.wang/hulk/gd32e230f8_firmware_merge_tool/releases),
|
||||||
取最新的 `DAPLinkFlash-vX.Y.Z-win64.zip`;
|
取最新的 `DAPLinkFlash-vX.Y.Z-win64.zip`;
|
||||||
2. 解压到**任意可写目录**(配置 `flash_gui_settings.json` 写在 exe 旁边);
|
2. 解压到**任意可写目录**(配置 `flash_gui_settings.json` 写在 exe 旁边);
|
||||||
3. 双击 `DAPLinkFlash.exe`。首次启动比脚本慢一两秒(onefile 解压),属正常现象。
|
3. 进入解压出的 `DAPLinkFlash` 文件夹,双击 `DAPLinkFlash.exe`。
|
||||||
|
目录版(onedir)无临时解压等待,启动接近秒开。
|
||||||
|
|
||||||
包内包含:
|
包内包含:
|
||||||
|
|
||||||
```
|
```
|
||||||
DAPLinkFlash/
|
DAPLinkFlash/
|
||||||
├── DAPLinkFlash.exe 主程序(单文件自包含)
|
├── DAPLinkFlash.exe 主程序(启动器)
|
||||||
|
├── _internal/ 运行库(PyInstaller onedir, 须与 exe 同目录)
|
||||||
├── toolchain/openocd/ 内置 OpenOCD(GUI 自动定位)
|
├── toolchain/openocd/ 内置 OpenOCD(GUI 自动定位)
|
||||||
│ ├── bin/openocd.exe + libftdi1.dll / libusb-1.0.dll
|
│ ├── bin/openocd.exe + libftdi1.dll / libusb-1.0.dll
|
||||||
│ └── openocd/scripts/ interface / target 配置脚本
|
│ └── openocd/scripts/ interface / target 配置脚本
|
||||||
└── README.md 本说明
|
└── README.md 本说明
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> **SmartScreen 拦截("Windows 已保护你的电脑")**:exe 未做代码签名,
|
||||||
|
> 浏览器下载的压缩包带"来自网络"标记。任选其一绕过:
|
||||||
|
> ① 点"更多信息 → 仍要运行"(每台电脑每个文件仅一次);
|
||||||
|
> ② 下载后**先右键 zip → 属性 → 勾选"解除锁定"→ 确定**,再解压,包内所有文件都不会再拦;
|
||||||
|
> ③ 用 7-Zip / WinRAR 解压(不继承网络标记)。
|
||||||
|
|
||||||
## 3. GUI 使用
|
## 3. GUI 使用
|
||||||
|
|
||||||
主窗口为双栏仪表盘:
|
主窗口为单列仪表盘,补齐/拼接经顶栏按钮弹出独立窗口:
|
||||||
|
|
||||||
```
|
```
|
||||||
左列 右列
|
顶栏 DAPLink Flash [补齐 (Pad)] [拼接 (Merge)] 关于 Dark/Light/System
|
||||||
┌ 探针与工具链 [自动定位] ┐ ┌ 补齐 (Pad) — Y-Modem 对齐 ┐
|
┌ 探针与工具链 [自动定位] ┐
|
||||||
├ 芯片与固件(型号/固件/基地址)┤ ├ 拼接 (Merge) — BL+APP+标志 ┤
|
├ 芯片与固件(型号/固件/基地址) ┤
|
||||||
└ 操作: 检测/烧录/擦除/读取/复位/停止 ┘ └ 运行日志 — OpenOCD 输出 ┘
|
├ 运行日志 — OpenOCD 输出 ┤
|
||||||
|
└ 操作: 检测/烧录/擦除/读取/复位/停止 ┘
|
||||||
|
弹窗 补齐 (Pad) / 拼接 (Merge) — 非模态, 配置与主窗口共享
|
||||||
```
|
```
|
||||||
|
|
||||||
1. **选工具链**:默认自动定位(内置 → 本机 `D:/toolchain` → PATH),左卡右上
|
1. **选工具链**:默认自动定位(内置 → 本机 `D:/toolchain` → PATH),左卡右上
|
||||||
@@ -62,7 +72,7 @@ DAPLinkFlash/
|
|||||||
3. **选固件**:点"浏览"选 BIN/HEX/ELF;BIN 确认基地址(完整镜像 `0x08000000`,
|
3. **选固件**:点"浏览"选 BIN/HEX/ELF;BIN 确认基地址(完整镜像 `0x08000000`,
|
||||||
单独 APP `0x08002000`);
|
单独 APP `0x08002000`);
|
||||||
4. **点"检测设备"**确认链路通 → **点"烧录固件"**;输出实时显示在右下日志区;
|
4. **点"检测设备"**确认链路通 → **点"烧录固件"**;输出实时显示在右下日志区;
|
||||||
5. **固件工具**(右列):
|
5. **固件工具**(顶栏按钮弹出的独立窗口,非模态):
|
||||||
- 补齐:选输入 BIN 后输出名自动带出(`*_pad.bin`),对齐默认 1K,点"执行补齐";
|
- 补齐:选输入 BIN 后输出名自动带出(`*_pad.bin`),对齐默认 1K,点"执行补齐";
|
||||||
- 拼接:选 Bootloader 与 APP 后输出名自动带出(`*_BL.bin`),地址按典型布局
|
- 拼接:选 Bootloader 与 APP 后输出名自动带出(`*_BL.bin`),地址按典型布局
|
||||||
预填,点"执行拼接"。
|
预填,点"执行拼接"。
|
||||||
@@ -112,8 +122,7 @@ FlashTool/
|
|||||||
│ ├── publish_gitea.py # 上传到 Gitea Release
|
│ ├── publish_gitea.py # 上传到 Gitea Release
|
||||||
│ └── logos/ # 图标源(make_ico.py + png/ico)
|
│ └── logos/ # 图标源(make_ico.py + png/ico)
|
||||||
├── release/ # 构建与发布产物(机器生成区, 不手改/不入库)
|
├── release/ # 构建与发布产物(机器生成区, 不手改/不入库)
|
||||||
│ ├── DAPLinkFlash.exe # 打包单文件版
|
│ ├── DAPLinkFlash/ # onedir 程序目录: exe + _internal/ + toolchain/
|
||||||
│ ├── toolchain/openocd/ # package_release.py 复制的内置工具链
|
|
||||||
│ └── *.zip # 发布包(上传 Gitea Release)
|
│ └── *.zip # 发布包(上传 Gitea Release)
|
||||||
├── toolchain/openocd/ # (可选) vendor 进仓库的工具链, 不入库
|
├── toolchain/openocd/ # (可选) vendor 进仓库的工具链, 不入库
|
||||||
├── start_flash_gui.bat # 源码模式双击启动器
|
├── start_flash_gui.bat # 源码模式双击启动器
|
||||||
@@ -130,8 +139,8 @@ OpenOCD 用本机安装(默认找 `D:/toolchain/openocd`)或先跑一次
|
|||||||
```powershell
|
```powershell
|
||||||
cd src
|
cd src
|
||||||
python logos/make_ico.py # (可选) 改图标后重新生成 png/ico, 需 Pillow
|
python logos/make_ico.py # (可选) 改图标后重新生成 png/ico, 需 Pillow
|
||||||
python build_exe.py # 打包 → ../release/DAPLinkFlash.exe(约 19MB;
|
python build_exe.py # 打包 → ../release/DAPLinkFlash/(onedir 目录版,
|
||||||
# 自动装 PyInstaller, CTk 主题数据自动收集)
|
# 启动秒开; 自动装 PyInstaller, CTk 主题数据自动收集)
|
||||||
python build_exe.py --debug # 额外出一个带控制台的调试变体(命令行模式可见输出)
|
python build_exe.py --debug # 额外出一个带控制台的调试变体(命令行模式可见输出)
|
||||||
python package_release.py 0.1.0 # 组装 release/DAPLinkFlash-v0.1.0-win64.zip
|
python package_release.py 0.1.0 # 组装 release/DAPLinkFlash-v0.1.0-win64.zip
|
||||||
# 含内置 OpenOCD 工具链(来源见脚本说明)
|
# 含内置 OpenOCD 工具链(来源见脚本说明)
|
||||||
@@ -143,6 +152,8 @@ python publish_gitea.py DAPLinkFlash-v0.1.0-win64.zip --insecure
|
|||||||
|
|
||||||
## 7. 注意事项 / FAQ
|
## 7. 注意事项 / FAQ
|
||||||
|
|
||||||
|
- **"Windows 已保护你的电脑"(SmartScreen)**:exe 无代码签名所致,
|
||||||
|
处理见 §2 的三种绕过方式;要彻底消除需购买代码签名证书对 exe 签名;
|
||||||
- **"找不到 OpenOCD"**:点"自动定位";发布包用户确认解压后 `toolchain/openocd/`
|
- **"找不到 OpenOCD"**:点"自动定位";发布包用户确认解压后 `toolchain/openocd/`
|
||||||
目录和 exe 在一起;
|
目录和 exe 在一起;
|
||||||
- **烧录失败排查**:①"检测设备"先验链路;② 探针序列号留空可排除序列号问题;
|
- **烧录失败排查**:①"检测设备"先验链路;② 探针序列号留空可排除序列号问题;
|
||||||
|
|||||||
+98
-59
@@ -33,17 +33,23 @@ BOLD = ('Microsoft YaHei UI', 13, 'bold')
|
|||||||
TITLE_FONT = ('Microsoft YaHei UI', 18, 'bold')
|
TITLE_FONT = ('Microsoft YaHei UI', 18, 'bold')
|
||||||
|
|
||||||
APP_NAME = 'DAPLink Flash'
|
APP_NAME = 'DAPLink Flash'
|
||||||
APP_VERSION = '2.0.1'
|
APP_VERSION = '2.2.0'
|
||||||
REPO_URL = 'https://gitea.hulk.wang/hulk/gd32e230f8_firmware_merge_tool'
|
REPO_URL = 'https://gitea.hulk.wang/hulk/gd32e230f8_firmware_merge_tool'
|
||||||
|
|
||||||
OK_BLUE = '#3B8ED0'
|
OK_BLUE = '#3B8ED0'
|
||||||
|
|
||||||
|
|
||||||
def get_icon_path():
|
def get_icon_path():
|
||||||
"""工具图标: 源码模式取 logos/ 下, 打包模式取 exe 内嵌资源(_MEIPASS)"""
|
"""工具图标: 源码模式取 logos/ 下, 打包模式取资源目录
|
||||||
|
(onefile 在 _MEIPASS 临时目录, onedir 在 exe 旁 _internal/)"""
|
||||||
if getattr(sys, 'frozen', False):
|
if getattr(sys, 'frozen', False):
|
||||||
p = os.path.join(getattr(sys, '_MEIPASS', ''), 'logo_flash.ico')
|
exe_dir = os.path.dirname(os.path.abspath(sys.executable))
|
||||||
else:
|
candidates = (os.path.join(exe_dir, '_internal', 'logo_flash.ico'),
|
||||||
|
os.path.join(exe_dir, 'logo_flash.ico'))
|
||||||
|
for p in candidates:
|
||||||
|
if os.path.exists(p):
|
||||||
|
return p
|
||||||
|
return None
|
||||||
p = os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
p = os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
||||||
'logos', 'logo_flash.ico')
|
'logos', 'logo_flash.ico')
|
||||||
return p if os.path.exists(p) else None
|
return p if os.path.exists(p) else None
|
||||||
@@ -73,8 +79,11 @@ class App(ctk.CTk):
|
|||||||
'hulk.DAPLinkFlash')
|
'hulk.DAPLinkFlash')
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
self.geometry('1360x880')
|
self.geometry('960x950')
|
||||||
self.minsize(1200, 780)
|
self.minsize(860, 800)
|
||||||
|
# 构建完成前先隐藏窗口: CTk 控件是逐个绘制显示的, 不隐藏会出现
|
||||||
|
# "界面一项一项加载出来"的观感, 构建完 deiconify 一次性呈现
|
||||||
|
self.withdraw()
|
||||||
ic = get_icon_path()
|
ic = get_icon_path()
|
||||||
if ic:
|
if ic:
|
||||||
try:
|
try:
|
||||||
@@ -99,16 +108,26 @@ class App(ctk.CTk):
|
|||||||
self.proc = None
|
self.proc = None
|
||||||
self._poll_job = None
|
self._poll_job = None
|
||||||
self._op_buttons = []
|
self._op_buttons = []
|
||||||
|
self.v_pad_result = tk.StringVar(value='')
|
||||||
|
self.v_merge_result = tk.StringVar(value='')
|
||||||
|
|
||||||
self._build()
|
self._build()
|
||||||
self._log(f'{APP_NAME} v{APP_VERSION} — OpenOCD 烧录 · 固件补齐 · 镜像拼接')
|
self._log(f'{APP_NAME} v{APP_VERSION} — OpenOCD 烧录 · 固件补齐 · 镜像拼接')
|
||||||
ver = openocd_runner.version(self.cfg['openocd'])
|
self._log('工具链: %s (%s)' % (self.cfg['openocd'], self._toolchain_source))
|
||||||
self._log('工具链: %s (%s)%s' % (self.cfg['openocd'], self._toolchain_source,
|
# openocd --version 要起子进程, 杀毒软件下可能阻塞数秒, 挪到主循环空闲后补记
|
||||||
' ' + ver if ver else ''))
|
self.after(300, self._log_toolchain_version)
|
||||||
self._poll()
|
self._poll()
|
||||||
self.protocol('WM_DELETE_WINDOW', self._on_close)
|
self.protocol('WM_DELETE_WINDOW', self._on_close)
|
||||||
if auto_close > 0:
|
if auto_close > 0:
|
||||||
self.after(auto_close * 1000, self._on_close)
|
self.after(auto_close * 1000, self._on_close)
|
||||||
|
# 显示窗口必须等事件循环跑起来: 对从未映射过的 withdrawn 窗口,
|
||||||
|
# 在 __init__ 里同步 deiconify 不会完成映射 → 窗口永远不出现(看似闪退)
|
||||||
|
self.after(120, self.deiconify)
|
||||||
|
|
||||||
|
def _log_toolchain_version(self):
|
||||||
|
ver = openocd_runner.version(self.cfg['openocd'])
|
||||||
|
if ver:
|
||||||
|
self._log(' ' + ver)
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# 布局
|
# 布局
|
||||||
@@ -138,8 +157,13 @@ class App(ctk.CTk):
|
|||||||
top = ctk.CTkFrame(self, fg_color='transparent')
|
top = ctk.CTkFrame(self, fg_color='transparent')
|
||||||
top.pack(fill='x', padx=12, pady=(10, 2))
|
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=APP_NAME, font=TITLE_FONT).pack(side='left')
|
||||||
ctk.CTkLabel(top, text='GD32 固件烧录 · 补齐 · 镜像拼接',
|
ctk.CTkLabel(top, text='GD32 固件烧录',
|
||||||
text_color='gray60').pack(side='left', padx=10, pady=(6, 0))
|
text_color='gray60').pack(side='left', padx=10, pady=(6, 0))
|
||||||
|
ctk.CTkButton(top, text='补齐 (Pad)', width=88, height=26,
|
||||||
|
command=self._open_pad_dialog).pack(side='left', padx=(16, 6),
|
||||||
|
pady=(4, 0))
|
||||||
|
ctk.CTkButton(top, text='拼接 (Merge)', width=100, height=26,
|
||||||
|
command=self._open_merge_dialog).pack(side='left', pady=(4, 0))
|
||||||
seg = ctk.CTkSegmentedButton(top, values=['Dark', 'Light', 'System'],
|
seg = ctk.CTkSegmentedButton(top, values=['Dark', 'Light', 'System'],
|
||||||
command=self._set_appearance)
|
command=self._set_appearance)
|
||||||
seg.set('System')
|
seg.set('System')
|
||||||
@@ -152,15 +176,9 @@ class App(ctk.CTk):
|
|||||||
|
|
||||||
body = ctk.CTkFrame(self, fg_color='transparent')
|
body = ctk.CTkFrame(self, fg_color='transparent')
|
||||||
body.pack(fill='both', expand=True, padx=12, pady=(4, 12))
|
body.pack(fill='both', expand=True, padx=12, pady=(4, 12))
|
||||||
body.grid_columnconfigure(0, weight=1, uniform='col')
|
body.grid_rowconfigure(2, weight=1) # 运行日志吃掉剩余高度
|
||||||
body.grid_columnconfigure(1, weight=1, uniform='col')
|
body.grid_columnconfigure(0, weight=1)
|
||||||
body.grid_rowconfigure(0, weight=1)
|
self._build_main(body)
|
||||||
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 = ctk.CTkFrame(self, fg_color='transparent')
|
||||||
status.pack(fill='x', padx=14, pady=(0, 8))
|
status.pack(fill='x', padx=14, pady=(0, 8))
|
||||||
@@ -169,9 +187,9 @@ class App(ctk.CTk):
|
|||||||
ctk.CTkLabel(status, text=f'v{APP_VERSION}',
|
ctk.CTkLabel(status, text=f'v{APP_VERSION}',
|
||||||
text_color='gray60').pack(side='right')
|
text_color='gray60').pack(side='right')
|
||||||
|
|
||||||
# -- 左列: 探针与工具链 / 芯片与固件 / 操作 --
|
# -- 主列: 探针与工具链 / 芯片与固件 / 运行日志 / 操作 --
|
||||||
def _build_left(self, col):
|
def _build_main(self, col):
|
||||||
col.grid_rowconfigure(2, weight=1) # 操作卡片吸底
|
col.grid_rowconfigure(2, weight=1) # 日志卡片吃掉剩余高度
|
||||||
col.grid_columnconfigure(0, weight=1)
|
col.grid_columnconfigure(0, weight=1)
|
||||||
|
|
||||||
self.v_toolchain = tk.StringVar(
|
self.v_toolchain = tk.StringVar(
|
||||||
@@ -218,8 +236,15 @@ class App(ctk.CTk):
|
|||||||
checkbox_width=18, checkbox_height=18).pack(
|
checkbox_width=18, checkbox_height=18).pack(
|
||||||
side='left', padx=(0, 14))
|
side='left', padx=(0, 14))
|
||||||
|
|
||||||
|
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')
|
||||||
|
|
||||||
card, c = self._card(col, '操作')
|
card, c = self._card(col, '操作')
|
||||||
card.grid(row=2, column=0, sticky='sew', pady=(10, 0))
|
card.grid(row=3, column=0, sticky='ew', pady=(10, 0))
|
||||||
grid = ctk.CTkFrame(c, fg_color='transparent')
|
grid = ctk.CTkFrame(c, fg_color='transparent')
|
||||||
grid.pack(fill='x')
|
grid.pack(fill='x')
|
||||||
grid.grid_columnconfigure((0, 1, 2), weight=1, uniform='op')
|
grid.grid_columnconfigure((0, 1, 2), weight=1, uniform='op')
|
||||||
@@ -235,18 +260,30 @@ class App(ctk.CTk):
|
|||||||
command=self._on_stop, state='disabled')
|
command=self._on_stop, state='disabled')
|
||||||
self.btn_stop.grid(row=1, column=2, sticky='ew', padx=3, pady=3)
|
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)
|
def _center_dialog(self, dlg, w, h):
|
||||||
|
"""弹窗叠在主窗口中上部"""
|
||||||
|
dlg.update_idletasks()
|
||||||
|
x = self.winfo_rootx() + max(0, (self.winfo_width() - w) // 2)
|
||||||
|
y = self.winfo_rooty() + max(0, (self.winfo_height() - h) // 4)
|
||||||
|
dlg.geometry(f'{w}x{h}+{x}+{y}')
|
||||||
|
|
||||||
card, c = self._card(col, '补齐 (Pad) — 尾部补 0xFF 到页边界, '
|
def _open_pad_dialog(self):
|
||||||
'适配 Bootloader Y-Modem 整包接收')
|
dlg = ctk.CTkToplevel(self)
|
||||||
card.grid(row=0, column=0, sticky='ew')
|
dlg.title('固件工具 — 补齐 (Pad)')
|
||||||
self._field(c, '输入 BIN', self.v['pad_in'], self._browse_pad_in)
|
dlg.resizable(False, False)
|
||||||
self._field(c, '输出 BIN', self.v['pad_out'], self._browse_pad_out)
|
dlg.transient(self)
|
||||||
row = ctk.CTkFrame(c, fg_color='transparent')
|
box = ctk.CTkFrame(dlg, fg_color='transparent')
|
||||||
row.pack(fill='x', pady=(6, 0))
|
box.pack(fill='both', expand=True, padx=18, pady=(14, 12))
|
||||||
|
ctk.CTkLabel(box, text='补齐 (Pad)', font=BOLD, anchor='w').pack(fill='x')
|
||||||
|
ctk.CTkLabel(box, text='尾部补 0xFF 到页边界, 适配 Bootloader Y-Modem 整包接收',
|
||||||
|
text_color='gray60', anchor='w').pack(fill='x', pady=(0, 6))
|
||||||
|
self._field(box, '输入 BIN', self.v['pad_in'], self._browse_pad_in)
|
||||||
|
self._field(box, '输出 BIN', self.v['pad_out'], self._browse_pad_out)
|
||||||
|
row = ctk.CTkFrame(box, fg_color='transparent')
|
||||||
|
row.pack(fill='x', pady=(8, 0))
|
||||||
ctk.CTkLabel(row, text='对齐:', width=40, anchor='e').pack(side='left')
|
ctk.CTkLabel(row, text='对齐:', width=40, anchor='e').pack(side='left')
|
||||||
ctk.CTkComboBox(row, width=86, state='readonly',
|
ctk.CTkComboBox(row, width=86, state='readonly',
|
||||||
values=['1024', '2048', '4096'],
|
values=['1024', '2048', '4096'],
|
||||||
@@ -256,17 +293,27 @@ class App(ctk.CTk):
|
|||||||
).pack(side='left', padx=(4, 12))
|
).pack(side='left', padx=(4, 12))
|
||||||
ctk.CTkButton(row, text='执行补齐', width=96,
|
ctk.CTkButton(row, text='执行补齐', width=96,
|
||||||
command=self._on_pad).pack(side='left')
|
command=self._on_pad).pack(side='left')
|
||||||
self.v_pad_result = tk.StringVar(value='')
|
ctk.CTkLabel(box, textvariable=self.v_pad_result, text_color=OK_BLUE,
|
||||||
ctk.CTkLabel(c, textvariable=self.v_pad_result, text_color=OK_BLUE,
|
justify='left', anchor='w').pack(fill='x', pady=(8, 0))
|
||||||
justify='left', anchor='w').pack(fill='x', pady=(4, 0))
|
ctk.CTkLabel(box, text='Bootloader 的 Y-Modem 按 1K 整包接收, OTA 升级包发送前先补齐',
|
||||||
|
text_color='gray50', anchor='w').pack(fill='x', pady=(2, 0))
|
||||||
|
self._center_dialog(dlg, 680, 300)
|
||||||
|
|
||||||
card, c = self._card(col, '拼接 (Merge) — Bootloader + APP + 有效标志 → 整片镜像')
|
def _open_merge_dialog(self):
|
||||||
card.grid(row=1, column=0, sticky='ew', pady=(10, 0))
|
dlg = ctk.CTkToplevel(self)
|
||||||
self._field(c, 'Bootloader', self.v['bootloader'], self._browse_bl)
|
dlg.title('固件工具 — 拼接 (Merge)')
|
||||||
self._field(c, 'Application', self.v['app'], self._browse_app)
|
dlg.resizable(False, False)
|
||||||
self._field(c, '输出文件', self.v['merge_out'], self._browse_merge_out)
|
dlg.transient(self)
|
||||||
row = ctk.CTkFrame(c, fg_color='transparent')
|
box = ctk.CTkFrame(dlg, fg_color='transparent')
|
||||||
row.pack(fill='x', pady=(6, 0))
|
box.pack(fill='both', expand=True, padx=18, pady=(14, 12))
|
||||||
|
ctk.CTkLabel(box, text='拼接 (Merge)', font=BOLD, anchor='w').pack(fill='x')
|
||||||
|
ctk.CTkLabel(box, text='Bootloader + APP + 有效标志 → 整片 Flash 镜像',
|
||||||
|
text_color='gray60', anchor='w').pack(fill='x', pady=(0, 6))
|
||||||
|
self._field(box, 'Bootloader', self.v['bootloader'], self._browse_bl)
|
||||||
|
self._field(box, 'Application', self.v['app'], self._browse_app)
|
||||||
|
self._field(box, '输出文件', self.v['merge_out'], self._browse_merge_out)
|
||||||
|
row = ctk.CTkFrame(box, fg_color='transparent')
|
||||||
|
row.pack(fill='x', pady=(8, 0))
|
||||||
for label, key, w in (('APP 地址', 'app_addr', 104),
|
for label, key, w in (('APP 地址', 'app_addr', 104),
|
||||||
('标志地址', 'flag_addr', 104),
|
('标志地址', 'flag_addr', 104),
|
||||||
('Flash', 'merge_flash', 84),
|
('Flash', 'merge_flash', 84),
|
||||||
@@ -274,22 +321,14 @@ class App(ctk.CTk):
|
|||||||
ctk.CTkLabel(row, text=label + ':', anchor='e').pack(side='left')
|
ctk.CTkLabel(row, text=label + ':', anchor='e').pack(side='left')
|
||||||
ctk.CTkEntry(row, width=w, textvariable=self.v[key]
|
ctk.CTkEntry(row, width=w, textvariable=self.v[key]
|
||||||
).pack(side='left', padx=(4, 10))
|
).pack(side='left', padx=(4, 10))
|
||||||
ctk.CTkButton(row, text='执行拼接', width=96,
|
ctk.CTkButton(box, text='执行拼接', width=96,
|
||||||
command=self._on_merge).pack(side='left')
|
command=self._on_merge).pack(anchor='w', pady=(8, 0))
|
||||||
self.v_merge_result = tk.StringVar(value='')
|
ctk.CTkLabel(box, textvariable=self.v_merge_result, text_color=OK_BLUE,
|
||||||
ctk.CTkLabel(c, textvariable=self.v_merge_result, text_color=OK_BLUE,
|
justify='left', anchor='w').pack(fill='x', pady=(2, 0))
|
||||||
justify='left', anchor='w').pack(fill='x', pady=(4, 0))
|
ctk.CTkLabel(box, text='典型布局: 0x08000000 Bootloader 8KB | 0x08002000 APP | '
|
||||||
ctk.CTkLabel(c, text='典型布局: 0x08000000 Bootloader 8KB | 0x08002000 APP | '
|
|
||||||
'0x0800FFFC 有效标志 0xEEEEEEEE | Flash 64KB',
|
'0x0800FFFC 有效标志 0xEEEEEEEE | Flash 64KB',
|
||||||
text_color='gray50', justify='left', anchor='w',
|
text_color='gray50', anchor='w').pack(fill='x', pady=(2, 0))
|
||||||
).pack(fill='x', pady=(2, 0))
|
self._center_dialog(dlg, 720, 420)
|
||||||
|
|
||||||
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')
|
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# 输入行辅助
|
# 输入行辅助
|
||||||
|
|||||||
+26
-20
@@ -1,27 +1,28 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
"""
|
||||||
把 flash_gui.py 打包成单文件 exe(工具名: DAPLinkFlash)
|
把 flash_gui.py 打包成目录版 exe(工具名: DAPLinkFlash)
|
||||||
================================================================
|
================================================================
|
||||||
使用 PyInstaller --onefile --noconsole: Python 解释器 + 标准库 + tkinter +
|
使用 PyInstaller --onedir --noconsole: 解释器 + 标准库 + tkinter +
|
||||||
customtkinter 全部打进一个 exe, 目标机器无需安装任何依赖。
|
customtkinter 进 exe + _internal/ 目录, 目标机器无需安装任何依赖。
|
||||||
OpenOCD 工具链不进 exe, 由 package_release.py 以 toolchain/ 目录随包分发
|
不用 --onefile: onefile 每次启动都要把几十 MB 解压到临时目录, 杀毒
|
||||||
(scripts 目录含 1000+ 文件, 进 onefile 会拖慢每次启动的解压)。
|
软件下明显拖慢开窗; 本工具本来就以文件夹形式发布(toolchain/ 随包),
|
||||||
|
onedir 解压一次即用, 启动接近秒开。
|
||||||
|
|
||||||
用法:
|
用法:
|
||||||
python build_exe.py 自动安装 PyInstaller(如缺)并打包
|
python build_exe.py 自动安装 PyInstaller(如缺)并打包
|
||||||
python build_exe.py --debug 额外打一个带控制台的变体(命令行模式可见输出)
|
python build_exe.py --debug 额外打一个带控制台的变体(命令行模式可见输出)
|
||||||
|
|
||||||
产物: release/DAPLinkFlash.exe(及 --debug 时的 DAPLinkFlash-console.exe)
|
产物: release/DAPLinkFlash/(DAPLinkFlash.exe + _internal/)
|
||||||
首次运行 onefile exe 会先解压到临时目录, 启动比脚本慢一两秒, 属正常现象。
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
|
|
||||||
# 本脚本位于 src/, 打包产物输出到 FlashTool/release/
|
# 本脚本位于 src/, 打包产物输出到 FlashTool/release/DAPLinkFlash/
|
||||||
SRC = os.path.dirname(os.path.abspath(__file__))
|
SRC = os.path.dirname(os.path.abspath(__file__))
|
||||||
BASE = os.path.dirname(SRC)
|
BASE = os.path.dirname(SRC)
|
||||||
DIST = os.path.join(BASE, 'release')
|
DIST = os.path.join(BASE, 'release')
|
||||||
@@ -49,38 +50,43 @@ def extra_collect_args():
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
def remove_stale(exe_path):
|
def remove_stale(out_dir, name):
|
||||||
"""删除旧 exe; 若被占用(上次验证的进程还在), 先结束同名进程再重试"""
|
"""删除旧输出目录; exe 被占用(上次验证的进程还在)先结束同名进程再重试"""
|
||||||
if not os.path.exists(exe_path):
|
if not os.path.isdir(out_dir):
|
||||||
return
|
return
|
||||||
|
exe = os.path.join(out_dir, name + '.exe')
|
||||||
|
if os.path.exists(exe):
|
||||||
try:
|
try:
|
||||||
os.remove(exe_path)
|
shutil.rmtree(out_dir)
|
||||||
return
|
return
|
||||||
except PermissionError:
|
except PermissionError:
|
||||||
pass
|
pass
|
||||||
print(f'{exe_path} 被占用, 结束残留进程后重试...')
|
print(f'{exe} 被占用, 结束残留进程后重试...')
|
||||||
subprocess.run(['taskkill', '/F', '/IM', os.path.basename(exe_path)],
|
subprocess.run(['taskkill', '/F', '/IM', name + '.exe'],
|
||||||
capture_output=True)
|
capture_output=True)
|
||||||
time.sleep(1.0)
|
time.sleep(1.0)
|
||||||
os.remove(exe_path)
|
shutil.rmtree(out_dir, ignore_errors=True)
|
||||||
|
|
||||||
|
|
||||||
def build(name, extra_args):
|
def build(name, extra_args):
|
||||||
remove_stale(os.path.join(DIST, name + '.exe'))
|
out_dir = os.path.join(DIST, name)
|
||||||
|
remove_stale(out_dir, name)
|
||||||
cmd = [sys.executable, '-m', 'PyInstaller',
|
cmd = [sys.executable, '-m', 'PyInstaller',
|
||||||
'--onefile', '--clean', '--name', name,
|
'--onedir', '--clean', '--name', name,
|
||||||
'--distpath', DIST, '--workpath', WORK, '--specpath', WORK,
|
'--distpath', DIST, '--workpath', WORK, '--specpath', WORK,
|
||||||
] + extra_args + extra_collect_args() + [os.path.join(SRC, 'flash_gui.py')]
|
] + extra_args + extra_collect_args() + [os.path.join(SRC, 'flash_gui.py')]
|
||||||
if os.path.exists(ICO):
|
if os.path.exists(ICO):
|
||||||
# exe 文件图标 + 运行时窗口图标资源(从 _MEIPASS 读取)
|
# exe 文件图标 + 运行时窗口图标资源(onedir 落在 _internal/, GUI 从那里读)
|
||||||
cmd += ['--icon', ICO, '--add-data', f'{ICO};.']
|
cmd += ['--icon', ICO, '--add-data', f'{ICO};.']
|
||||||
else:
|
else:
|
||||||
print('警告: 未找到 logos/logo_flash.ico(先运行 logos/make_ico.py), '
|
print('警告: 未找到 logos/logo_flash.ico(先运行 logos/make_ico.py), '
|
||||||
'本次打包不带图标')
|
'本次打包不带图标')
|
||||||
print('\n>>', ' '.join(cmd))
|
print('\n>>', ' '.join(cmd))
|
||||||
subprocess.check_call(cmd)
|
subprocess.check_call(cmd)
|
||||||
out = os.path.join(DIST, name + '.exe')
|
out = os.path.join(out_dir, name + '.exe')
|
||||||
print(f'完成: {out} ({os.path.getsize(out) / 1024 / 1024:.1f} MB)')
|
total = sum(os.path.getsize(os.path.join(r, f))
|
||||||
|
for r, _d, fs in os.walk(out_dir) for f in fs)
|
||||||
|
print(f'完成: {out} (目录共 {total / 1024 / 1024:.1f} MB)')
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import argparse
|
|||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
import config
|
import config
|
||||||
import openocd_runner
|
import openocd_runner
|
||||||
@@ -194,8 +195,21 @@ def main():
|
|||||||
sys.exit(run_openocd(args, '复位芯片', openocd_runner.script_reset()))
|
sys.exit(run_openocd(args, '复位芯片', openocd_runner.script_reset()))
|
||||||
|
|
||||||
# 无操作参数 → 启动 GUI(实现在 app_gui.py)
|
# 无操作参数 → 启动 GUI(实现在 app_gui.py)
|
||||||
|
try:
|
||||||
import app_gui
|
import app_gui
|
||||||
app_gui.run_gui(args.seconds)
|
app_gui.run_gui(args.seconds)
|
||||||
|
except SystemExit:
|
||||||
|
raise
|
||||||
|
except Exception:
|
||||||
|
# --noconsole 打包后异常无处可见, 落一份 crash.log 到程序目录便于远程定位
|
||||||
|
import traceback
|
||||||
|
try:
|
||||||
|
with open(os.path.join(config.BASE_DIR, 'crash.log'), 'w',
|
||||||
|
encoding='utf-8') as f:
|
||||||
|
f.write(time.strftime('%Y-%m-%d %H:%M:%S\n') + traceback.format_exc())
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ def version(exe):
|
|||||||
"""openocd --version 首行(如 Open On-Chip Debugger 0.12.0), 失败返回 None"""
|
"""openocd --version 首行(如 Open On-Chip Debugger 0.12.0), 失败返回 None"""
|
||||||
try:
|
try:
|
||||||
out = subprocess.run([exe, "--version"], capture_output=True, text=True,
|
out = subprocess.run([exe, "--version"], capture_output=True, text=True,
|
||||||
timeout=10, creationflags=subprocess.CREATE_NO_WINDOW
|
timeout=5, creationflags=subprocess.CREATE_NO_WINDOW
|
||||||
if os.name == "nt" else 0)
|
if os.name == "nt" else 0)
|
||||||
line = (out.stdout or out.stderr or "").strip().splitlines()
|
line = (out.stdout or out.stderr or "").strip().splitlines()
|
||||||
return line[0].strip() if line else None
|
return line[0].strip() if line else None
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
"""
|
||||||
组装 DAPLink Flash 发布包: exe + 内置 OpenOCD 工具链 → 文件夹 + zip(解压即用)
|
组装 DAPLink Flash 发布包: 程序目录(exe + 内置 OpenOCD 工具链) → zip(解压即用)
|
||||||
================================================================
|
================================================================
|
||||||
release/ 本身就是可直接使用的完整文件夹(exe + toolchain/ + 说明),
|
release/DAPLinkFlash/ 本身就是可直接使用的完整文件夹
|
||||||
再原样压成 zip —— 两者内容始终一致。
|
(exe + _internal/ + toolchain/ + 说明), 再原样压成 zip —— 内容始终一致。
|
||||||
|
|
||||||
zip 解压后根目录为 DAPLinkFlash/, 与 exe 运行目录结构一致:
|
zip 解压后根目录为 DAPLinkFlash/, 与 exe 运行目录结构一致:
|
||||||
DAPLinkFlash/
|
DAPLinkFlash/
|
||||||
├── DAPLinkFlash.exe 主程序(先运行 build_exe.py 打包)
|
├── DAPLinkFlash.exe 主程序(先运行 build_exe.py 打包)
|
||||||
|
├── _internal/ PyInstaller onedir 运行库
|
||||||
├── toolchain/openocd/ 内置 OpenOCD(bin + scripts, GUI 自动定位)
|
├── toolchain/openocd/ 内置 OpenOCD(bin + scripts, GUI 自动定位)
|
||||||
└── README.md 使用说明
|
└── README.md 使用说明
|
||||||
|
|
||||||
@@ -35,6 +36,7 @@ SRC = os.path.dirname(os.path.abspath(__file__))
|
|||||||
BASE = os.path.dirname(SRC)
|
BASE = os.path.dirname(SRC)
|
||||||
RELEASE = os.path.join(BASE, 'release')
|
RELEASE = os.path.join(BASE, 'release')
|
||||||
APP_NAME = 'DAPLinkFlash'
|
APP_NAME = 'DAPLinkFlash'
|
||||||
|
APP_DIR = os.path.join(RELEASE, APP_NAME) # onedir 输出目录 = 解压后的程序文件夹
|
||||||
|
|
||||||
|
|
||||||
def detect_version():
|
def detect_version():
|
||||||
@@ -68,8 +70,8 @@ def resolve_openocd_src(explicit):
|
|||||||
|
|
||||||
|
|
||||||
def copy_toolchain(openocd_src):
|
def copy_toolchain(openocd_src):
|
||||||
"""复制 bin(openocd.exe + 运行 DLL)与 openocd/scripts 到 release/toolchain/"""
|
"""复制 bin(openocd.exe + 运行 DLL)与 openocd/scripts 到程序目录 toolchain/"""
|
||||||
dst_root = os.path.join(RELEASE, 'toolchain', 'openocd')
|
dst_root = os.path.join(APP_DIR, 'toolchain', 'openocd')
|
||||||
if os.path.isdir(dst_root):
|
if os.path.isdir(dst_root):
|
||||||
shutil.rmtree(dst_root)
|
shutil.rmtree(dst_root)
|
||||||
count = 0
|
count = 0
|
||||||
@@ -92,31 +94,29 @@ def main():
|
|||||||
args = ap.parse_args()
|
args = ap.parse_args()
|
||||||
|
|
||||||
version = (args.version or detect_version()).lstrip('v')
|
version = (args.version or detect_version()).lstrip('v')
|
||||||
exe = os.path.join(RELEASE, APP_NAME + '.exe')
|
exe = os.path.join(APP_DIR, APP_NAME + '.exe')
|
||||||
readme = os.path.join(BASE, 'README.md')
|
readme = os.path.join(BASE, 'README.md')
|
||||||
if not os.path.exists(exe):
|
if not os.path.exists(exe):
|
||||||
sys.exit(f'缺少 release/{APP_NAME}.exe, 先运行: python build_exe.py')
|
sys.exit(f'缺少 {APP_DIR}/{APP_NAME}.exe, 先运行: python build_exe.py')
|
||||||
if not os.path.exists(readme):
|
if not os.path.exists(readme):
|
||||||
sys.exit(f'缺少 {readme}')
|
sys.exit(f'缺少 {readme}')
|
||||||
openocd_src = resolve_openocd_src(args.openocd_src)
|
openocd_src = resolve_openocd_src(args.openocd_src)
|
||||||
|
|
||||||
n_files = copy_toolchain(openocd_src)
|
n_files = copy_toolchain(openocd_src)
|
||||||
shutil.copy2(readme, os.path.join(RELEASE, 'README.md'))
|
shutil.copy2(readme, os.path.join(APP_DIR, 'README.md'))
|
||||||
|
|
||||||
zip_name = f'{APP_NAME}-v{version}-win64.zip'
|
zip_name = f'{APP_NAME}-v{version}-win64.zip'
|
||||||
zip_path = os.path.join(RELEASE, zip_name)
|
zip_path = os.path.join(RELEASE, zip_name)
|
||||||
with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as z:
|
with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as z:
|
||||||
z.write(exe, f'{APP_NAME}/{APP_NAME}.exe')
|
for root, _dirs, files in os.walk(APP_DIR):
|
||||||
z.write(os.path.join(RELEASE, 'README.md'), f'{APP_NAME}/README.md')
|
|
||||||
tool_root = os.path.join(RELEASE, 'toolchain')
|
|
||||||
for root, _dirs, files in os.walk(tool_root):
|
|
||||||
for f in files:
|
for f in files:
|
||||||
|
if f == 'flash_gui_settings.json': # 本机运行配置不随包分发
|
||||||
|
continue
|
||||||
full = os.path.join(root, f)
|
full = os.path.join(root, f)
|
||||||
z.write(full, os.path.join(APP_NAME, os.path.relpath(full, RELEASE)))
|
z.write(full, os.path.join(APP_NAME, os.path.relpath(full, APP_DIR)))
|
||||||
|
|
||||||
exe_mb = os.path.getsize(exe) / 1024 / 1024
|
|
||||||
zip_mb = os.path.getsize(zip_path) / 1024 / 1024
|
zip_mb = os.path.getsize(zip_path) / 1024 / 1024
|
||||||
print(f'发布文件夹: {RELEASE} (exe {exe_mb:.1f} MB + 内置工具链 {n_files} 个文件, 平铺即用)')
|
print(f'发布文件夹: {APP_DIR} (exe + _internal + 内置工具链 {n_files} 个文件, 平铺即用)')
|
||||||
print(f'发布包: {zip_path} ({zip_mb:.1f} MB)')
|
print(f'发布包: {zip_path} ({zip_mb:.1f} MB)')
|
||||||
print('下一步上传: python publish_gitea.py', zip_name)
|
print('下一步上传: python publish_gitea.py', zip_name)
|
||||||
|
|
||||||
|
|||||||
@@ -31,8 +31,9 @@
|
|||||||
日常烧录优先使用图形界面工具,不需要打开 VSCode,也不需要记忆 OpenOCD 命令。
|
日常烧录优先使用图形界面工具,不需要打开 VSCode,也不需要记忆 OpenOCD 命令。
|
||||||
|
|
||||||
- **免安装版**:到 [Gitea Releases](https://gitea.hulk.wang/hulk/gd32e230f8_firmware_merge_tool/releases)
|
- **免安装版**:到 [Gitea Releases](https://gitea.hulk.wang/hulk/gd32e230f8_firmware_merge_tool/releases)
|
||||||
下载 `DAPLinkFlash-vX.Y.Z-win64.zip`,解压双击 `DAPLinkFlash.exe` 即可,
|
下载 `DAPLinkFlash-vX.Y.Z-win64.zip`,解压后进入 `DAPLinkFlash` 文件夹
|
||||||
**包内自带 OpenOCD 工具链**(`toolchain/openocd/`),目标机器无需任何环境。
|
双击 `DAPLinkFlash.exe` 即可,**包内自带 OpenOCD 工具链**
|
||||||
|
(`toolchain/openocd/`),目标机器无需任何环境。
|
||||||
- **源码运行**:`python FlashTool/src/flash_gui.py` 或双击 `FlashTool\start_flash_gui.bat`
|
- **源码运行**:`python FlashTool/src/flash_gui.py` 或双击 `FlashTool\start_flash_gui.bat`
|
||||||
(需 Python 3.10+ 与 `pip install customtkinter`;OpenOCD 默认找 `D:/toolchain`)。
|
(需 Python 3.10+ 与 `pip install customtkinter`;OpenOCD 默认找 `D:/toolchain`)。
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user