Files
A1SwapModPacker/a1_swap_mod_packer/cli.py
T

213 lines
9.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""命令行接口模块:定义参数解析器和命令处理函数。
提供 `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())