From f6b118293c59c45bb1614a7c8231ec77e5c3c7d2 Mon Sep 17 00:00:00 2001 From: yw1573 Date: Tue, 28 Jul 2026 14:45:11 +0800 Subject: [PATCH] =?UTF-8?q?feat(printer):=20=E6=96=B0=E5=A2=9E=E5=B1=80?= =?UTF-8?q?=E5=9F=9F=E7=BD=91=E5=8F=91=E9=80=813MF=E8=87=B3A1=E6=89=93?= =?UTF-8?q?=E5=8D=B0=E6=9C=BA=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- a1_swap_mod_packer/cli.py | 21 ++++++++++++ a1_swap_mod_packer/gui.py | 61 +++++++++++++++++++++++++++++++++++ a1_swap_mod_packer/printer.py | 42 ++++++++++++++++++++++++ code/bambulabs_api | 1 + code/ha-bambulab | 1 + 5 files changed, 126 insertions(+) create mode 100644 a1_swap_mod_packer/printer.py create mode 160000 code/bambulabs_api create mode 160000 code/ha-bambulab diff --git a/a1_swap_mod_packer/cli.py b/a1_swap_mod_packer/cli.py index 91a3f99..6d18267 100644 --- a/a1_swap_mod_packer/cli.py +++ b/a1_swap_mod_packer/cli.py @@ -19,6 +19,7 @@ from .core import ( 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: @@ -90,6 +91,21 @@ def build_command(args: argparse.Namespace) -> int: 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 @@ -156,6 +172,11 @@ def create_parser() -> argparse.ArgumentParser: 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) diff --git a/a1_swap_mod_packer/gui.py b/a1_swap_mod_packer/gui.py index fdb03f0..fed3e04 100644 --- a/a1_swap_mod_packer/gui.py +++ b/a1_swap_mod_packer/gui.py @@ -58,6 +58,7 @@ except ImportError as exc: # pragma: no cover raise SystemExit("PySide6 is required to run the GUI. Install it with: pip install PySide6") from exc from . import APP_NAME, APP_TITLE +from .printer import PrinterConfig, send_to_printer from .batch import IndividualBuildTask, individual_batch_worker_count, run_individual_batch_builds from .core import ( BuildOptions, @@ -663,6 +664,35 @@ class MainWindow(QMainWindow): # 输出预览 self.output_preview_label = QLabel("-") output_layout.addRow("预览", self.output_preview_label) + + # ---- 发送至打印机 ---- + self.send_to_printer_check = QCheckBox("构建后发送至打印机") + self.send_to_printer_check.setChecked(False) + self.send_to_printer_check.setToolTip("通过局域网将生成的 3MF 发送到 A1 打印机并开始打印") + output_layout.addRow("打印机", self.send_to_printer_check) + + # 打印机配置行(IP、访问码、序列号) + printer_config_widget = QWidget() + printer_config_layout = QHBoxLayout(printer_config_widget) + printer_config_layout.setContentsMargins(0, 0, 0, 0) + printer_config_layout.setSpacing(6) + + self.printer_ip_edit = QLineEdit() + self.printer_ip_edit.setPlaceholderText("IP 地址") + self.printer_ip_edit.setMaximumWidth(140) + self.printer_access_code_edit = QLineEdit() + self.printer_access_code_edit.setPlaceholderText("访问码") + self.printer_access_code_edit.setMaximumWidth(120) + self.printer_serial_edit = QLineEdit() + self.printer_serial_edit.setPlaceholderText("序列号") + self.printer_serial_edit.setMaximumWidth(180) + + printer_config_layout.addWidget(self.printer_ip_edit) + printer_config_layout.addWidget(self.printer_access_code_edit) + printer_config_layout.addWidget(self.printer_serial_edit) + printer_config_layout.addStretch(1) + output_layout.addRow("", printer_config_widget) + root.addWidget(output_group) # ===================================================================== @@ -723,6 +753,10 @@ class MainWindow(QMainWindow): self.individual_batch_check.stateChanged.connect(self.on_individual_batch_toggled) self.clear_after_build_check.stateChanged.connect(self.save_current_settings) self.skip_duplicates_check.stateChanged.connect(self.save_current_settings) + self.send_to_printer_check.stateChanged.connect(self.save_current_settings) + self.printer_ip_edit.textChanged.connect(self.save_current_settings) + self.printer_access_code_edit.textChanged.connect(self.save_current_settings) + self.printer_serial_edit.textChanged.connect(self.save_current_settings) self.output_dir_edit.textChanged.connect(self.on_output_rule_changed) self.output_name_edit.textChanged.connect(self.on_output_rule_changed) @@ -742,6 +776,10 @@ class MainWindow(QMainWindow): self.individual_batch_check.setChecked(bool(options.get("individual_batch_mode", False))) self.clear_after_build_check.setChecked(bool(options.get("clear_after_build", False))) self.skip_duplicates_check.setChecked(bool(options.get("skip_duplicates", True))) + self.send_to_printer_check.setChecked(bool(options.get("send_to_printer_enabled", False))) + self.printer_ip_edit.setText(str(options.get("printer_ip", ""))) + self.printer_access_code_edit.setText(str(options.get("printer_access_code", ""))) + self.printer_serial_edit.setText(str(options.get("printer_serial", ""))) self.output_dir_edit.setText(str(options.get("output_directory", ""))) self.output_name_edit.setText(str(options.get("output_filename_rule", DEFAULT_OUTPUT_PATTERN))) metadata_mode = options.get("metadata_mode", "sum") @@ -780,6 +818,10 @@ class MainWindow(QMainWindow): "individual_batch_mode": self.individual_batch_check.isChecked(), "clear_after_build": self.clear_after_build_check.isChecked(), "skip_duplicates": self.skip_duplicates_check.isChecked(), + "send_to_printer_enabled": self.send_to_printer_check.isChecked(), + "printer_ip": self.printer_ip_edit.text().strip(), + "printer_access_code": self.printer_access_code_edit.text().strip(), + "printer_serial": self.printer_serial_edit.text().strip(), "output_directory": self.output_dir_edit.text().strip(), "output_filename_rule": self.output_name_edit.text().strip() or DEFAULT_OUTPUT_PATTERN, } @@ -1343,6 +1385,21 @@ class MainWindow(QMainWindow): """显示构建成功的浮动提示条。""" self.success_toast.show_message(message) + def _send_to_printer(self, file_path: Path) -> None: + """将构建好的 3MF 文件发送至打印机。""" + config = PrinterConfig( + ip=self.printer_ip_edit.text().strip(), + access_code=self.printer_access_code_edit.text().strip(), + serial=self.printer_serial_edit.text().strip(), + ) + try: + send_to_printer(file_path, config) + self.log.append(f"已发送至打印机:{file_path.name}") + except ImportError: + self.log.append("错误:未安装 bambulabs_api,请执行 pip install bambulabs_api") + except Exception as exc: + self.log.append(f"发送至打印机失败:{exc}") + def build_output(self) -> None: """主构建入口:根据批处理模式分发到合并构建或独立构建。""" jobs = self.collect_jobs() @@ -1366,6 +1423,8 @@ class MainWindow(QMainWindow): options = self.build_options_for_output(output_path) result = build_packed_3mf(jobs, options) self.log_build_result(result) + if self.send_to_printer_check.isChecked(): + self._send_to_printer(result.output_3mf) self.show_success_toast("已创建打包的 3MF 文件。") if self.clear_after_build_check.isChecked(): self.remove_all() @@ -1395,6 +1454,8 @@ class MainWindow(QMainWindow): batch_result = run_individual_batch_builds(tasks, max_workers=worker_count) for result in batch_result.results: self.log_build_result(result) + if self.send_to_printer_check.isChecked(): + self._send_to_printer(result.output_3mf) success_count = len(batch_result.results) for failure in batch_result.failures: errors.append(f"{failure.job.source_3mf.name}: {failure.error}") diff --git a/a1_swap_mod_packer/printer.py b/a1_swap_mod_packer/printer.py new file mode 100644 index 0000000..08fc4f7 --- /dev/null +++ b/a1_swap_mod_packer/printer.py @@ -0,0 +1,42 @@ +"""打印机发送模块:通过局域网将 3MF 发送到 Bambu A1 并开始打印。""" + +from __future__ import annotations +from pathlib import Path +from dataclasses import dataclass + +@dataclass +class PrinterConfig: + """打印机连接配置。""" + ip: str = "" # 打印机 IP 地址 + access_code: str = "" # 局域网访问码 + serial: str = "" # 打印机序列号 + +def send_to_printer(file_path: Path, config: PrinterConfig) -> bool: + """将 3MF 文件上传至打印机并开始打印。 + + Args: + file_path: 要发送的 3MF 文件路径 + config: 打印机连接配置 + + Returns: + True 表示成功,False 表示失败 + + Raises: + ImportError: bambulabs_api 未安装 + Exception: 连接或上传失败 + """ + # 延迟导入,避免在没有 bambulabs_api 时报错 + from bambulabs_api import Printer as BambuPrinter + printer = BambuPrinter(config.ip, config.access_code, config.serial) + try: + printer.connect() + file_name = file_path.name + with open(file_path, "rb") as f: + import io + result = printer.upload_file(io.BytesIO(f.read()), file_name) + if "226" not in result: + raise RuntimeError(f"上传文件失败: {result}") + printer.start_print(file_name, plate_number=1) + return True + finally: + printer.disconnect() diff --git a/code/bambulabs_api b/code/bambulabs_api new file mode 160000 index 0000000..5bd1e84 --- /dev/null +++ b/code/bambulabs_api @@ -0,0 +1 @@ +Subproject commit 5bd1e84a9b0c21ab7cdfdccac9dab43994319b0d diff --git a/code/ha-bambulab b/code/ha-bambulab new file mode 160000 index 0000000..3402861 --- /dev/null +++ b/code/ha-bambulab @@ -0,0 +1 @@ +Subproject commit 34028612155456061e4059a1754527a9774d7db8