diff --git a/FlashTool/README.md b/FlashTool/README.md index 4bdc0f2..2fde743 100644 --- a/FlashTool/README.md +++ b/FlashTool/README.md @@ -31,19 +31,27 @@ 1. 打开 [Releases 页面](https://gitea.hulk.wang/hulk/gd32e230f8_firmware_merge_tool/releases), 取最新的 `DAPLinkFlash-vX.Y.Z-win64.zip`; 2. 解压到**任意可写目录**(配置 `flash_gui_settings.json` 写在 exe 旁边); -3. 双击 `DAPLinkFlash.exe`。首次启动比脚本慢一两秒(onefile 解压),属正常现象。 +3. 进入解压出的 `DAPLinkFlash` 文件夹,双击 `DAPLinkFlash.exe`。 + 目录版(onedir)无临时解压等待,启动接近秒开。 包内包含: ``` DAPLinkFlash/ -├── DAPLinkFlash.exe 主程序(单文件自包含) +├── DAPLinkFlash.exe 主程序(启动器) +├── _internal/ 运行库(PyInstaller onedir, 须与 exe 同目录) ├── toolchain/openocd/ 内置 OpenOCD(GUI 自动定位) │ ├── bin/openocd.exe + libftdi1.dll / libusb-1.0.dll │ └── openocd/scripts/ interface / target 配置脚本 └── README.md 本说明 ``` +> **SmartScreen 拦截("Windows 已保护你的电脑")**:exe 未做代码签名, +> 浏览器下载的压缩包带"来自网络"标记。任选其一绕过: +> ① 点"更多信息 → 仍要运行"(每台电脑每个文件仅一次); +> ② 下载后**先右键 zip → 属性 → 勾选"解除锁定"→ 确定**,再解压,包内所有文件都不会再拦; +> ③ 用 7-Zip / WinRAR 解压(不继承网络标记)。 + ## 3. GUI 使用 主窗口为单列仪表盘,补齐/拼接经顶栏按钮弹出独立窗口: @@ -114,8 +122,7 @@ FlashTool/ │ ├── publish_gitea.py # 上传到 Gitea Release │ └── logos/ # 图标源(make_ico.py + png/ico) ├── release/ # 构建与发布产物(机器生成区, 不手改/不入库) -│ ├── DAPLinkFlash.exe # 打包单文件版 -│ ├── toolchain/openocd/ # package_release.py 复制的内置工具链 +│ ├── DAPLinkFlash/ # onedir 程序目录: exe + _internal/ + toolchain/ │ └── *.zip # 发布包(上传 Gitea Release) ├── toolchain/openocd/ # (可选) vendor 进仓库的工具链, 不入库 ├── start_flash_gui.bat # 源码模式双击启动器 @@ -132,8 +139,8 @@ OpenOCD 用本机安装(默认找 `D:/toolchain/openocd`)或先跑一次 ```powershell cd src python logos/make_ico.py # (可选) 改图标后重新生成 png/ico, 需 Pillow -python build_exe.py # 打包 → ../release/DAPLinkFlash.exe(约 19MB; - # 自动装 PyInstaller, CTk 主题数据自动收集) +python build_exe.py # 打包 → ../release/DAPLinkFlash/(onedir 目录版, + # 启动秒开; 自动装 PyInstaller, CTk 主题数据自动收集) python build_exe.py --debug # 额外出一个带控制台的调试变体(命令行模式可见输出) python package_release.py 0.1.0 # 组装 release/DAPLinkFlash-v0.1.0-win64.zip # 含内置 OpenOCD 工具链(来源见脚本说明) @@ -145,6 +152,8 @@ python publish_gitea.py DAPLinkFlash-v0.1.0-win64.zip --insecure ## 7. 注意事项 / FAQ +- **"Windows 已保护你的电脑"(SmartScreen)**:exe 无代码签名所致, + 处理见 §2 的三种绕过方式;要彻底消除需购买代码签名证书对 exe 签名; - **"找不到 OpenOCD"**:点"自动定位";发布包用户确认解压后 `toolchain/openocd/` 目录和 exe 在一起; - **烧录失败排查**:①"检测设备"先验链路;② 探针序列号留空可排除序列号问题; diff --git a/FlashTool/src/app_gui.py b/FlashTool/src/app_gui.py index 34f9c76..d92eed7 100644 --- a/FlashTool/src/app_gui.py +++ b/FlashTool/src/app_gui.py @@ -33,19 +33,25 @@ BOLD = ('Microsoft YaHei UI', 13, 'bold') TITLE_FONT = ('Microsoft YaHei UI', 18, 'bold') APP_NAME = 'DAPLink Flash' -APP_VERSION = '2.1.0' +APP_VERSION = '2.2.0' REPO_URL = 'https://gitea.hulk.wang/hulk/gd32e230f8_firmware_merge_tool' OK_BLUE = '#3B8ED0' def get_icon_path(): - """工具图标: 源码模式取 logos/ 下, 打包模式取 exe 内嵌资源(_MEIPASS)""" + """工具图标: 源码模式取 logos/ 下, 打包模式取资源目录 + (onefile 在 _MEIPASS 临时目录, onedir 在 exe 旁 _internal/)""" 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') + exe_dir = os.path.dirname(os.path.abspath(sys.executable)) + 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__)), + 'logos', 'logo_flash.ico') return p if os.path.exists(p) else None @@ -75,6 +81,9 @@ class App(ctk.CTk): pass self.geometry('960x950') self.minsize(860, 800) + # 构建完成前先隐藏窗口: CTk 控件是逐个绘制显示的, 不隐藏会出现 + # "界面一项一项加载出来"的观感, 构建完 deiconify 一次性呈现 + self.withdraw() ic = get_icon_path() if ic: try: @@ -104,13 +113,21 @@ class App(ctk.CTk): 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._log('工具链: %s (%s)' % (self.cfg['openocd'], self._toolchain_source)) + # openocd --version 要起子进程, 杀毒软件下可能阻塞数秒, 挪到主循环空闲后补记 + self.after(300, self._log_toolchain_version) self._poll() self.protocol('WM_DELETE_WINDOW', self._on_close) if auto_close > 0: 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) # ============================================================ # 布局 diff --git a/FlashTool/src/build_exe.py b/FlashTool/src/build_exe.py index fc1647b..21b3920 100644 --- a/FlashTool/src/build_exe.py +++ b/FlashTool/src/build_exe.py @@ -1,27 +1,28 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- """ -把 flash_gui.py 打包成单文件 exe(工具名: DAPLinkFlash) +把 flash_gui.py 打包成目录版 exe(工具名: DAPLinkFlash) ================================================================ -使用 PyInstaller --onefile --noconsole: Python 解释器 + 标准库 + tkinter + -customtkinter 全部打进一个 exe, 目标机器无需安装任何依赖。 -OpenOCD 工具链不进 exe, 由 package_release.py 以 toolchain/ 目录随包分发 -(scripts 目录含 1000+ 文件, 进 onefile 会拖慢每次启动的解压)。 +使用 PyInstaller --onedir --noconsole: 解释器 + 标准库 + tkinter + +customtkinter 进 exe + _internal/ 目录, 目标机器无需安装任何依赖。 +不用 --onefile: onefile 每次启动都要把几十 MB 解压到临时目录, 杀毒 +软件下明显拖慢开窗; 本工具本来就以文件夹形式发布(toolchain/ 随包), +onedir 解压一次即用, 启动接近秒开。 用法: python build_exe.py 自动安装 PyInstaller(如缺)并打包 python build_exe.py --debug 额外打一个带控制台的变体(命令行模式可见输出) -产物: release/DAPLinkFlash.exe(及 --debug 时的 DAPLinkFlash-console.exe) -首次运行 onefile exe 会先解压到临时目录, 启动比脚本慢一两秒, 属正常现象。 +产物: release/DAPLinkFlash/(DAPLinkFlash.exe + _internal/) """ import os +import shutil import subprocess import sys import time -# 本脚本位于 src/, 打包产物输出到 FlashTool/release/ +# 本脚本位于 src/, 打包产物输出到 FlashTool/release/DAPLinkFlash/ SRC = os.path.dirname(os.path.abspath(__file__)) BASE = os.path.dirname(SRC) DIST = os.path.join(BASE, 'release') @@ -49,38 +50,43 @@ def extra_collect_args(): return [] -def remove_stale(exe_path): - """删除旧 exe; 若被占用(上次验证的进程还在), 先结束同名进程再重试""" - if not os.path.exists(exe_path): +def remove_stale(out_dir, name): + """删除旧输出目录; exe 被占用(上次验证的进程还在)先结束同名进程再重试""" + if not os.path.isdir(out_dir): return - try: - os.remove(exe_path) - return - except PermissionError: - pass - print(f'{exe_path} 被占用, 结束残留进程后重试...') - subprocess.run(['taskkill', '/F', '/IM', os.path.basename(exe_path)], - capture_output=True) - time.sleep(1.0) - os.remove(exe_path) + exe = os.path.join(out_dir, name + '.exe') + if os.path.exists(exe): + try: + shutil.rmtree(out_dir) + return + except PermissionError: + pass + print(f'{exe} 被占用, 结束残留进程后重试...') + subprocess.run(['taskkill', '/F', '/IM', name + '.exe'], + capture_output=True) + time.sleep(1.0) + shutil.rmtree(out_dir, ignore_errors=True) 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', - '--onefile', '--clean', '--name', name, + '--onedir', '--clean', '--name', name, '--distpath', DIST, '--workpath', WORK, '--specpath', WORK, ] + extra_args + extra_collect_args() + [os.path.join(SRC, 'flash_gui.py')] if os.path.exists(ICO): - # exe 文件图标 + 运行时窗口图标资源(从 _MEIPASS 读取) + # exe 文件图标 + 运行时窗口图标资源(onedir 落在 _internal/, GUI 从那里读) cmd += ['--icon', ICO, '--add-data', f'{ICO};.'] else: print('警告: 未找到 logos/logo_flash.ico(先运行 logos/make_ico.py), ' '本次打包不带图标') print('\n>>', ' '.join(cmd)) subprocess.check_call(cmd) - out = os.path.join(DIST, name + '.exe') - print(f'完成: {out} ({os.path.getsize(out) / 1024 / 1024:.1f} MB)') + out = os.path.join(out_dir, name + '.exe') + 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(): diff --git a/FlashTool/src/flash_gui.py b/FlashTool/src/flash_gui.py index 6ee94fa..240f736 100644 --- a/FlashTool/src/flash_gui.py +++ b/FlashTool/src/flash_gui.py @@ -33,6 +33,7 @@ import argparse import os import subprocess import sys +import time import config import openocd_runner @@ -194,8 +195,21 @@ def main(): sys.exit(run_openocd(args, '复位芯片', openocd_runner.script_reset())) # 无操作参数 → 启动 GUI(实现在 app_gui.py) - import app_gui - app_gui.run_gui(args.seconds) + try: + import app_gui + 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__': diff --git a/FlashTool/src/openocd_runner.py b/FlashTool/src/openocd_runner.py index d6b2bf2..f89e63c 100644 --- a/FlashTool/src/openocd_runner.py +++ b/FlashTool/src/openocd_runner.py @@ -87,7 +87,7 @@ def version(exe): """openocd --version 首行(如 Open On-Chip Debugger 0.12.0), 失败返回 None""" try: 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) line = (out.stdout or out.stderr or "").strip().splitlines() return line[0].strip() if line else None diff --git a/FlashTool/src/package_release.py b/FlashTool/src/package_release.py index a2d06d9..3713a9f 100644 --- a/FlashTool/src/package_release.py +++ b/FlashTool/src/package_release.py @@ -1,14 +1,15 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- """ -组装 DAPLink Flash 发布包: exe + 内置 OpenOCD 工具链 → 文件夹 + zip(解压即用) +组装 DAPLink Flash 发布包: 程序目录(exe + 内置 OpenOCD 工具链) → zip(解压即用) ================================================================ -release/ 本身就是可直接使用的完整文件夹(exe + toolchain/ + 说明), -再原样压成 zip —— 两者内容始终一致。 +release/DAPLinkFlash/ 本身就是可直接使用的完整文件夹 +(exe + _internal/ + toolchain/ + 说明), 再原样压成 zip —— 内容始终一致。 zip 解压后根目录为 DAPLinkFlash/, 与 exe 运行目录结构一致: DAPLinkFlash/ ├── DAPLinkFlash.exe 主程序(先运行 build_exe.py 打包) + ├── _internal/ PyInstaller onedir 运行库 ├── toolchain/openocd/ 内置 OpenOCD(bin + scripts, GUI 自动定位) └── README.md 使用说明 @@ -35,6 +36,7 @@ SRC = os.path.dirname(os.path.abspath(__file__)) BASE = os.path.dirname(SRC) RELEASE = os.path.join(BASE, 'release') APP_NAME = 'DAPLinkFlash' +APP_DIR = os.path.join(RELEASE, APP_NAME) # onedir 输出目录 = 解压后的程序文件夹 def detect_version(): @@ -68,8 +70,8 @@ def resolve_openocd_src(explicit): def copy_toolchain(openocd_src): - """复制 bin(openocd.exe + 运行 DLL)与 openocd/scripts 到 release/toolchain/""" - dst_root = os.path.join(RELEASE, 'toolchain', 'openocd') + """复制 bin(openocd.exe + 运行 DLL)与 openocd/scripts 到程序目录 toolchain/""" + dst_root = os.path.join(APP_DIR, 'toolchain', 'openocd') if os.path.isdir(dst_root): shutil.rmtree(dst_root) count = 0 @@ -92,31 +94,29 @@ def main(): args = ap.parse_args() 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') 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): sys.exit(f'缺少 {readme}') openocd_src = resolve_openocd_src(args.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_path = os.path.join(RELEASE, zip_name) with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as z: - z.write(exe, f'{APP_NAME}/{APP_NAME}.exe') - 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 root, _dirs, files in os.walk(APP_DIR): for f in files: + if f == 'flash_gui_settings.json': # 本机运行配置不随包分发 + continue 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 - 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('下一步上传: python publish_gitea.py', zip_name) diff --git a/README.md b/README.md index 2774d1d..dad5906 100644 --- a/README.md +++ b/README.md @@ -31,8 +31,9 @@ 日常烧录优先使用图形界面工具,不需要打开 VSCode,也不需要记忆 OpenOCD 命令。 - **免安装版**:到 [Gitea Releases](https://gitea.hulk.wang/hulk/gd32e230f8_firmware_merge_tool/releases) - 下载 `DAPLinkFlash-vX.Y.Z-win64.zip`,解压双击 `DAPLinkFlash.exe` 即可, - **包内自带 OpenOCD 工具链**(`toolchain/openocd/`),目标机器无需任何环境。 + 下载 `DAPLinkFlash-vX.Y.Z-win64.zip`,解压后进入 `DAPLinkFlash` 文件夹 + 双击 `DAPLinkFlash.exe` 即可,**包内自带 OpenOCD 工具链** + (`toolchain/openocd/`),目标机器无需任何环境。 - **源码运行**:`python FlashTool/src/flash_gui.py` 或双击 `FlashTool\start_flash_gui.bat` (需 Python 3.10+ 与 `pip install customtkinter`;OpenOCD 默认找 `D:/toolchain`)。