From 935665e7e80c170948a48316367cd0adb9308622 Mon Sep 17 00:00:00 2001 From: yw1573 Date: Tue, 28 Jul 2026 14:56:49 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E7=A7=BB=E9=99=A4CLI=E6=A8=A1?= =?UTF-8?q?=E5=9D=97=EF=BC=8C=E9=A1=B9=E7=9B=AE=E6=94=B9=E4=B8=BA=E7=BA=AF?= =?UTF-8?q?GUI=E6=A1=8C=E9=9D=A2=E5=BA=94=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + README.md | 158 +--------------------------- a1_swap_mod_packer/cli.py | 212 -------------------------------------- build_win.cmd | 10 -- docs/how_it_works.md | 60 ++--------- run_cli.py | 4 - tests/test_version.py | 20 +--- 7 files changed, 14 insertions(+), 451 deletions(-) delete mode 100644 a1_swap_mod_packer/cli.py delete mode 100644 run_cli.py diff --git a/.gitignore b/.gitignore index 8d5e974..db67b7b 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,4 @@ Thumbs.db /settings.json /reference codec.py/code/ +code diff --git a/README.md b/README.md index a68d073..6ce9935 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ A1 Swap Mod Packer 是一个开源的 3MF 打包工具,专为 Bambu Lab A1 Swa ## 主要功能 - 拖放式批量打包 GUI。 -- 可重复执行的自动化 CLI。 +- 简洁高效的桌面 GUI。 - 支持多个 `.3mf` 输入文件。 - 每个文件独立份数。 - 自动从 `Metadata/slice_info.config` 读取并汇总时间和耗材。 @@ -65,24 +65,6 @@ python -m a1_swap_mod_packer.gui python run_gui.py ``` -启动 CLI: - -```bash -python -m a1_swap_mod_packer.cli --help -``` - -或 - -```bash -python run_cli.py -``` - -查看版本: - -```bash -python -m a1_swap_mod_packer.cli --version -``` - ## 构建 Windows 可执行文件 仓库包含 Windows 构建脚本: @@ -102,7 +84,6 @@ build/onefile/ ```text build/onefile/ a1packer.exe - a1packer-cli.exe gcode_patches.ini swap_gcode/ x.png 输出预览图示例 @@ -145,10 +126,9 @@ py -3.13 -m venv .venv build_win.cmd ``` -构建脚本执行两次 Nuitka 编译: +构建脚本执行一次 Nuitka 编译: - GUI:`run_gui.py` → `build/onefile/a1packer.exe` -- CLI:`run_cli.py` → `build/onefile/a1packer-cli.exe` GUI 编译启用 PySide6 插件并包含当前界面所需的 Qt 插件组: @@ -158,22 +138,7 @@ platforms,imageformats,styles,iconengines ### 构建后验证 -发布目录前,先确认 CLI 能识别外部资源: - -```cmd -build\onefile\a1packer-cli.exe --version -build\onefile\a1packer-cli.exe list-swap-gcode -``` - -第二条命令应列出: - -```text -build/onefile/swap_gcode/ -``` - -中的文件。 - -同时启动一次 `build/onefile/a1packer.exe`,确认: +启动一次 `build/onefile/a1packer.exe`,确认: - 换盘 G-code 下拉框列出已复制的模板。 - 修改 GUI 选项会创建或更新 `build/onefile/settings.json`。 @@ -206,7 +171,7 @@ GUI 中使用方法: ## 可编辑 G-code 补丁 -GUI 和 CLI 都读取固定补丁文件: +GUI 读取固定补丁文件: ```text gcode_patches.ini @@ -375,12 +340,6 @@ gcode_patches.ini 如果预览图缺失或无法读取,打包器保留可用基础预览,仍尝试施加面板标签。 -CLI 可通过以下选项禁用预览重写: - -```bash ---no-preview-label -``` - ### 批处理模式 #### 合并模式 @@ -506,112 +465,3 @@ settings.json - 构建成功后清空输入列表。 - 输出目录。 - 输出文件名规则。 - -## CLI 使用示例 - -列出可用的换盘 G-code 文件: - -```bash -python -m a1_swap_mod_packer.cli list-swap-gcode -``` - -单源文件 × 5 份: - -```bash -python -m a1_swap_mod_packer.cli build \ - --item "SC05720_01.gcode(1).3mf" 5 \ - --swap-gcode "a1_swap.gcode" \ - --cool-bed 45 \ - --wait 45 \ - --show-plate-number \ - -o "5 Plates - SC05720_01.gcode(1).3mf" -``` - -多源文件合并输出: - -```bash -python -m a1_swap_mod_packer.cli build \ - --item "A.3mf" 2 \ - --item "B.3mf" 3 \ - --swap-gcode "a1_swap.gcode" \ - -o "5 Plates - A_and_B.3mf" -``` - -使用累加元数据模式: - -```bash -python -m a1_swap_mod_packer.cli build \ - --item "A.3mf" 5 \ - --swap-gcode "a1_swap.gcode" \ - --metadata-mode sum \ - -o "5 Plates - A.3mf" -``` - -禁用 G-code 补丁: - -```bash -python -m a1_swap_mod_packer.cli build \ - --item "A.3mf" 5 \ - --swap-gcode "a1_swap.gcode" \ - --no-gcode-patches \ - -o "5 Plates - A.3mf" -``` - -位置参数输入 + 统一份数: - -```bash -python -m a1_swap_mod_packer.cli build \ - "A.3mf" "B.3mf" \ - --copies 2 \ - --swap-gcode "a1_swap.gcode" \ - -o "4 Plates - A_and_B.3mf" -``` - -指定换盘 G-code 目录和 ZIP 压缩级别: - -```bash -python -m a1_swap_mod_packer.cli build \ - --item "A.3mf" 5 \ - --swap-gcode "a1_swap.gcode" \ - --swap-gcode-dir "D:\SwapMod\swap_gcode" \ - --zip-level 9 \ - -o "5 Plates - A.3mf" -``` - -禁用最后换盘和预览标签重写: - -```bash -python -m a1_swap_mod_packer.cli build \ - --item "A.3mf" 5 \ - --swap-gcode "a1_swap.gcode" \ - --no-swap-after-final \ - --no-preview-label \ - -o "5 Plates - A.3mf" -``` - -禁用换盘后等待时间: - -```bash -python -m a1_swap_mod_packer.cli build \ - --item "A.3mf" 5 \ - --swap-gcode "a1_swap.gcode" \ - --no-eject-wait \ - -o "5 Plates - A.3mf" -``` - -CLI 常用选项速查: - -| 选项 | 含义 | -|---|---| -| `--version` | 打印当前版本 | -| `list-swap-gcode` | 列出换盘 G-code 目录中的文件 | -| `--item PATH COPIES` | 添加一个带独立份数的输入;可多次使用 | -| 位置参数 `inputs` + `--copies N` | 添加多个使用相同份数的输入 | -| `--swap-gcode-dir DIR` | 使用自定义换盘 G-code 目录 | -| `--no-bed-cooldown` | 不在换盘块前插入 `M190` | -| `--no-swap-after-final` | 最后一块盘后不执行换盘块 | -| `--no-eject-wait` | 不插入换盘后等待时间(`G4 P...` 行) | -| `--line-ending lf\|crlf` | 选择生成 G-code 的换行符;默认 `crlf` | -| `--zip-level 1-9` | zlib-ng Deflate 压缩级别;默认 `7` | -| `--no-preview-label` | 不重写预览图标签/合成图 | -| `--no-gcode-patches` | 不应用 `gcode_patches.ini` 中的规则 | diff --git a/a1_swap_mod_packer/cli.py b/a1_swap_mod_packer/cli.py deleted file mode 100644 index 6d18267..0000000 --- a/a1_swap_mod_packer/cli.py +++ /dev/null @@ -1,212 +0,0 @@ -"""命令行接口模块:定义参数解析器和命令处理函数。 - -提供 `build`(构建打包 3MF)和 `list-swap-gcode`(列出换盘 G-code 文件) -两个子命令,以及 `main` 入口函数。 -""" - -from __future__ import annotations - -import argparse -import sys -from pathlib import Path - -from . import APP_NAME, APP_TITLE, __version__ -from .core import ( - BuildOptions, - DEFAULT_ZIP_COMPRESS_LEVEL, - PlateJob, - build_packed_3mf, - list_swap_gcode_files, -) -from .paths import default_patch_config_path, default_swap_gcode_dir -from .printer import PrinterConfig, send_to_printer - - -def parse_item(values: list[str]) -> PlateJob: - """解析 --item 参数的键值对,生成一个 PlateJob 实例。 - - Args: - values: 包含两个字符串的列表 [路径, 份数] - - Returns: - PlateJob: 对应的板作业对象 - - Raises: - argparse.ArgumentTypeError: 参数格式或类型不合法时抛出 - """ - # --item 必须提供恰好两个值:路径和份数 - if len(values) != 2: - raise argparse.ArgumentTypeError("每个 --item 需要一个路径和一个份数。") - path = Path(values[0]) - # 份数必须可以解析为整数 - try: - copies = int(values[1]) - except ValueError as exc: - raise argparse.ArgumentTypeError(f"无效的份数:{values[1]}") from exc - return PlateJob(path, copies) - - -def build_command(args: argparse.Namespace) -> int: - """执行 'build' 子命令:收集输入、组装选项、调用构建。 - - Args: - args: 解析后的命令行命名空间 - - Returns: - int: 退出码,成功为 0 - """ - jobs: list[PlateJob] = [] - # 处理 --item 参数(每个可独立指定份数) - for item in args.item or []: - jobs.append(parse_item(item)) - # 处理位置参数输入(共用 --copies 份数) - for input_path in args.inputs or []: - jobs.append(PlateJob(Path(input_path), args.copies)) - # 没有输入文件时直接报错退出 - if not jobs: - raise SystemExit("未提供输入 3MF 文件。") - # 根据 --no-bed-cooldown 决定是否写入床温指令 - cool_bed_temp = None if args.no_bed_cooldown else args.cool_bed - options = BuildOptions( - swap_gcode=args.swap_gcode, - output_3mf=Path(args.output), - cool_bed_temp=cool_bed_temp, - wait_after_eject_seconds=0 if args.no_eject_wait else args.wait, - show_plate_number=args.show_plate_number, - swap_after_final=not args.no_swap_after_final, - metadata_mode=args.metadata_mode, - line_ending=args.line_ending, - add_preview_label=not args.no_preview_label, - apply_gcode_patches=not args.no_gcode_patches, - swap_gcode_dir=Path(args.swap_gcode_dir) if args.swap_gcode_dir else None, - zip_compress_level=args.zip_level, - ) - # 调用核心构建函数 - result = build_packed_3mf(jobs, options) - # 输出构建结果摘要 - print(f"输出:{result.output_3mf}") - print(f"板数:{result.plate_count}") - print(f"G-code MD5:{result.gcode_md5}") - if result.total_prediction_seconds is not None: - print(f"源文件打印时间:{int(result.total_prediction_seconds)} 秒") - if result.total_weight_grams is not None: - print(f"源文件耗材重量:{result.total_weight_grams:.2f} g") - if args.send_to_printer: - config = PrinterConfig( - ip=args.printer_ip, - access_code=args.printer_access_code, - serial=args.printer_serial, - ) - try: - send_to_printer(result.output_3mf, config) - print(f"已发送至打印机:{result.output_3mf}") - except ImportError: - print("错误:未安装 bambulabs_api,请执行 pip install bambulabs_api", file=sys.stderr) - return 1 - except Exception as exc: - print(f"发送至打印机失败:{exc}", file=sys.stderr) - return 1 - return 0 - - -def list_swap_gcode_command(args: argparse.Namespace) -> int: - """执行 'list-swap-gcode' 子命令:列出可用的换盘 G-code 文件。 - - Args: - args: 解析后的命令行命名空间 - - Returns: - int: 退出码,成功为 0 - """ - # 解析目录:优先使用命令行参数,否则使用默认目录 - directory = Path(args.swap_gcode_dir) if args.swap_gcode_dir else default_swap_gcode_dir() - files = list_swap_gcode_files(directory) - # 目录为空时给出提示 - if not files: - print(f"在 {directory} 中未找到换盘 G-code 文件") - return 0 - for path in files: - print(path.name) - return 0 - - -def create_parser() -> argparse.ArgumentParser: - """创建并配置命令行参数解析器。 - - 包含两个子命令: - - build: 构建打包的 3MF 文件 - - list-swap-gcode: 列出换盘 G-code 文件 - - Returns: - argparse.ArgumentParser: 配置完成的参数解析器 - """ - parser = argparse.ArgumentParser( - prog="a1-swap-mod-packer", - description=f"{APP_TITLE} - 将重复的 Bambu A1 SwapMod 面板打包为一个 3MF 作业。", - ) - parser.add_argument("--version", action="version", version=f"{APP_NAME} {__version__}") - subparsers = parser.add_subparsers(dest="command", required=True) - - # ---- build 子命令 ---- - build = subparsers.add_parser("build", help="构建一个打包的 3MF 文件。") - # 位置参数:输入文件路径(共用统一的份数) - build.add_argument("inputs", nargs="*", help="输入 3MF 文件。使用 --copies 为所有文件设置相同的份数。") - # --item 参数:每个文件可独立指定份数 - build.add_argument("--item", nargs=2, action="append", metavar=("PATH", "COPIES"), help="添加一个带有独立份数的输入 3MF 文件。可重复使用。") - build.add_argument("-o", "--output", required=True, help="输出 3MF 路径。") - # 换盘 G-code 相关 - build.add_argument("--swap-gcode", required=True, help="换盘 G-code 文件名,位于 swap_gcode 中,或显式文件路径。") - build.add_argument("--swap-gcode-dir", default=None, help=f"模板目录。默认值:{default_swap_gcode_dir()}") - # 份数与温度 - build.add_argument("--copies", type=int, default=1, help="位置参数输入文件的复制份数。") - build.add_argument("--cool-bed", type=int, default=45, help="执行换盘 G-code 前等待的床温。") - build.add_argument("--no-bed-cooldown", action="store_true", help="不在换料代码前插入 M190。") - # 时间与编号 - build.add_argument("--wait", type=int, default=45, help="板换盘后等待的秒数。") - build.add_argument("--no-eject-wait", action="store_true", help="不插入换盘后等待指令 G4。") - build.add_argument("--show-plate-number", action="store_true", help="每板号向 M73 R 值增加 100 小时。") - # 行为开关 - build.add_argument("--no-swap-after-final", action="store_true", help="最后一块盘后不执行换盘 G-code。") - build.add_argument("--metadata-mode", choices=("source", "sum"), default="sum", help="如何写入 slice_info 的 prediction 和 weight。") - build.add_argument("--line-ending", choices=("lf", "crlf"), default="crlf", help="生成 G-code 的换行符。") - build.add_argument("--zip-level", type=int, choices=range(1, 10), default=DEFAULT_ZIP_COMPRESS_LEVEL, metavar="1-9", help="输出 3MF 的 zlib-ng Deflate 压缩级别。默认值:7。") - build.add_argument("--no-preview-label", action="store_true", help="不重写预览图标签/合成图。") - build.add_argument("--no-gcode-patches", action="store_true", help=f"不应用来自 {default_patch_config_path()} 的可编辑补丁。") - # 发送至打印机 - build.add_argument("--send-to-printer", action="store_true", help="构建后将 3MF 发送至打印机并开始打印。") - build.add_argument("--printer-ip", default="", help="打印机 IP 地址(配合 --send-to-printer 使用)。") - build.add_argument("--printer-access-code", default="", help="打印机访问码(配合 --send-to-printer 使用)。") - build.add_argument("--printer-serial", default="", help="打印机序列号(配合 --send-to-printer 使用)。") - # 绑定命令处理函数 - build.set_defaults(func=build_command) - - # ---- list-swap-gcode 子命令 ---- - list_cmd = subparsers.add_parser("list-swap-gcode", help="列出 swap_gcode 目录中的文件。") - list_cmd.add_argument("--swap-gcode-dir", default=None, help=f"模板目录。默认值:{default_swap_gcode_dir()}") - list_cmd.set_defaults(func=list_swap_gcode_command) - return parser - - -def main(argv: list[str] | None = None) -> int: - """命令行入口函数:解析参数并分发到对应的子命令处理函数。 - - Args: - argv: 命令行参数列表,为 None 时使用 sys.argv - - Returns: - int: 程序退出码,0 表示成功,1 表示失败 - """ - # 构建解析器并解析参数 - parser = create_parser() - args = parser.parse_args(argv) - try: - # 根据子命令调用绑定的处理函数 - return int(args.func(args)) - except Exception as exc: - # 异常信息输出到 stderr,返回非零退出码 - print(f"错误:{exc}", file=sys.stderr) - return 1 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/build_win.cmd b/build_win.cmd index 841562b..f418d92 100644 --- a/build_win.cmd +++ b/build_win.cmd @@ -21,16 +21,6 @@ if errorlevel 1 exit /b %ERRORLEVEL% echo. echo GUI build done. -echo Next step is building CLI. - -"%PY%" -m nuitka ^ - --mode=onefile ^ - --msvc=latest ^ - --output-dir="%DIST%" ^ - --output-filename=a1packer-cli.exe ^ - --remove-output ^ - run_cli.py -if errorlevel 1 exit /b %ERRORLEVEL% robocopy "swap_gcode" "%DIST%\swap_gcode" /E if %ERRORLEVEL% GEQ 8 exit /b %ERRORLEVEL% diff --git a/docs/how_it_works.md b/docs/how_it_works.md index 14cf3c5..08e0ae8 100644 --- a/docs/how_it_works.md +++ b/docs/how_it_works.md @@ -151,7 +151,7 @@ G4 P45000 ; 等 45 秒(可选) ### 合成预览图(可选) -如果启用了预览(CLI 中默认开启): +如果启用了预览: 1. 从最多 9 个**不同输入文件**中各取一张 `plate_N.png`(优先取活动面板的预览图) 2. 按网格拼接(1/2/4/6/9 宫格) @@ -189,66 +189,36 @@ BuildResult( --- -## 两种使用方式 +## 使用方式 -核心构建函数只有一个:`build_packed_3mf(jobs, options)`,CLI 和 GUI 会在调用它之前做不同的准备工作。 +核心构建函数只有一个:`build_packed_3mf(jobs, options)`,GUI 在调用它之前做不同的准备工作。 ```mermaid flowchart TB - subgraph CLI["CLI 入口: run_cli.py → cli.py"] - C1["argparse 解析参数"] --> C2["收集 --item / 位置参数"] - C2 --> C3["构造 BuildOptions"] - C3 --> BUILD["build_packed_3mf()"] - end subgraph GUI["GUI 入口: run_gui.py → gui.py"] G1["拖放/添加文件 → 表格"] --> G2["每行: 3MF路径 + 份数"] G2 --> G3["控件值 → BuildOptions"] G3 --> G4{"批处理模式?"} - G4 -- 合并 --> BUILD + G4 -- 合并 --> BUILD["build_packed_3mf()"] G4 -- 独立 --> G5["逐行调 build_packed_3mf()
ProcessPoolExecutor 并行"] end ``` -### CLI - -一条命令搞定。指定输入文件、份数、输出路径和各项参数: - -```bash -python -m a1_swap_mod_packer.cli build \ - --item "A.3mf" 3 \ - --swap-gcode "LX_1.02.26.gcode" \ - --cool-bed 45 \ - --wait 45 \ - --show-plate-number \ - -o "3 Plates - A.3mf" -``` - -CLI 做了这些: - -1. `argparse` 解析参数(文件、份数、换盘模板、冷却温度、等待秒数...) -2. 拼成 `PlateJob` 列表和 `BuildOptions` -3. 调一次 `build_packed_3mf()`(合并模式) -4. 把结果打印到 stdout - -新增 `--no-eject-wait` 选项可禁用换盘后等待时间(`G4 P...` 行),与 GUI 中的勾选框对应。 - -**所有输入合并成 1 个输出**。参数靠命令行传,没有交互、没有预览。 - ### GUI -窗口操作,参数靠控件选、文件靠拖放。最终也是调 `build_packed_3mf()`,但准备阶段和 CLI 不同: +窗口操作,参数靠控件选、文件靠拖放。最终也是调 `build_packed_3mf()`,准备工作包括: #### GUI 独有的准备阶段 1. **文件管理** — 表格里每行一个源文件 + 份数,可拖放、排序、移除 -2. **元数据预读** — 添加文件时就解析 `slice_info.config`,表格直接显示时间和耗材(CLI 不显示这个,只在构建后输出) +2. **元数据预读** — 添加文件时就解析 `slice_info.config`,表格直接显示时间和耗材 3. **缩略图预览** — 选中行 3MF → 右侧显示 `plate_N.png` 缩略图 -4. **输出路径自动算** — 默认留空,自动写到第一个输入文件旁边。文件名按规则 `{plates} Plates - {sources}.3mf` 生成(CLI 必须手动用 `-o`) +4. **输出路径自动算** — 默认留空,自动写到第一个输入文件旁边。文件名按规则 `{plates} Plates - {sources}.3mf` 生成 5. **设置持久化** — 控件值自动存 `settings.json`,重启恢复 #### 合并模式(默认) -和 CLI 一样,所有行合并调一次 `build_packed_3mf()`: +所有行合并调一次 `build_packed_3mf()`: ``` A.3mf 份数 3 @@ -269,17 +239,3 @@ C.3mf 份数 5 → build_packed_3mf([C]) → 5 Plates - C.3mf - 每行自动算各自的输出路径(文件名规则 + 去重防覆盖) - 弹出进度日志,记录每个文件的构建结果 - 有关闭全局预览标签的选项 - -#### GUI vs CLI 能力速查 - -| 能力 | CLI | GUI | -|------|:---:|:---:| -| 合并模式 | ✅ | ✅ | -| 独立批处理 | ❌ | ✅ | -| 拖放添加文件 | ❌ | ✅ | -| 表格编辑份数 | ❌ | ✅(每行 SpinBox) | -| 元数据预显示 | ❌ | ✅(表格直接看时间/耗材) | -| 缩略图预览 | ❌ | ✅ | -| 输出路径自动生成 | ❌ | ✅(文件名规则) | -| 设置持久化 | ❌ | ✅(settings.json) | -| 脚本/自动化友好 | ✅ | ❌ | diff --git a/run_cli.py b/run_cli.py deleted file mode 100644 index f696bbb..0000000 --- a/run_cli.py +++ /dev/null @@ -1,4 +0,0 @@ -from a1_swap_mod_packer.cli import main - -if __name__ == "__main__": - raise SystemExit(main()) \ No newline at end of file diff --git a/tests/test_version.py b/tests/test_version.py index bf62993..d55144c 100644 --- a/tests/test_version.py +++ b/tests/test_version.py @@ -2,39 +2,21 @@ 验证内容: - APP_TITLE 使用统一的 __version__ 构建 -- CLI 的 --help 和 --version 输出均使用共享的版本号 """ from __future__ import annotations -import contextlib -import io import unittest from a1_swap_mod_packer import APP_NAME, APP_TITLE, __version__ -from a1_swap_mod_packer.cli import create_parser class VersionDisplayTest(unittest.TestCase): - """测试版本信息在应用标题和 CLI 中的一致性。""" + """测试版本信息的一致性。""" def test_app_title_uses_shared_version(self) -> None: """验证 APP_TITLE 由 APP_NAME 与 __version__ 拼接而成。""" self.assertEqual(APP_TITLE, f"{APP_NAME} v{__version__}") - def test_cli_help_and_version_use_shared_version(self) -> None: - """验证 CLI 帮助信息和 --version 输出中均包含统一版本号。""" - parser = create_parser() - # 帮助信息应包含 APP_TITLE - self.assertIn(APP_TITLE, parser.format_help()) - - stdout = io.StringIO() - # --version 参数触发 SystemExit(0) 并打印版本信息 - with self.assertRaises(SystemExit) as caught, contextlib.redirect_stdout(stdout): - parser.parse_args(["--version"]) - - self.assertEqual(caught.exception.code, 0) # 正常退出码 - self.assertEqual(stdout.getvalue().strip(), f"{APP_NAME} {__version__}") # 版本信息匹配 - if __name__ == "__main__": unittest.main()