213 lines
9.2 KiB
Python
213 lines
9.2 KiB
Python
"""命令行接口模块:定义参数解析器和命令处理函数。
|
||
|
||
提供 `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())
|