22 Commits

Author SHA1 Message Date
yw1573 addad17e92 style(gui): 重新打包按钮移至文件操作栏,放在应用默认份数之后 2026-07-30 14:44:33 +08:00
yw1573 c43faeb5a2 feat(gui): 新增重新打包3MF按钮,选择解压目录自动更新MD5并打包 2026-07-30 14:32:53 +08:00
yw1573 d362b2280a feat: 新增repack_3mf脚本,解压目录重新打包为3MF文件 2026-07-30 14:16:05 +08:00
yw1573 53a25544fa refactor(gui): 移除成功浮动提示条,构建结果仅弹窗展示 2026-07-30 09:40:16 +08:00
yw1573 40c339dd74 fix(gui): 添加3MF失败时弹窗提示错误原因 2026-07-30 09:32:18 +08:00
yw1573 7c6a9f65ef refactor(gui): 底部日志区改为弹窗展示构建结果 2026-07-30 09:17:40 +08:00
yw1573 703929eea5 feat: 移除不适配的换盘代码 2026-07-30 09:05:01 +08:00
yw1573 bcff3ae264 feat(gui): 新增查看换盘G-code按钮,弹出只读窗口展示内容 2026-07-30 09:01:48 +08:00
yw1573 a9d2a0cbf1 chore: 版本号调整至 0.6.1 2026-07-29 14:19:36 +08:00
yw1573 5e6be787c7 build: Nuitka改为standalone模式并更新应用图标
onefile自解压易被杀软误报,改为standalone目录分发;
更新x.png应用图标。
2026-07-29 14:17:42 +08:00
yw1573 2438eca047 fix(build): Nuitka打包包含i18n翻译文件 2026-07-29 14:04:11 +08:00
yw1573 f066ca24fa refactor: 移除局域网发送至打印机功能 2026-07-29 12:22:19 +08:00
yw1573 116dcfe45f fix(printer): 修复A1打印发送,绕过bambulabs_api的ftp URL问题,改用sdcard路径 2026-07-28 16:25:21 +08:00
yw1573 db517b69f7 fix(i18n): 语言切换后重新计算固定宽度按钮尺寸 2026-07-28 16:04:00 +08:00
yw1573 ed02b48d80 fix(i18n): 首启根据settings.json的locale信息正确加载语言 2026-07-28 15:55:59 +08:00
yw1573 68aa056359 fix(i18n): 语言切换时全面刷新所有UI文本控件 2026-07-28 15:51:54 +08:00
yw1573 2fed301a6c chore: 版本号升至 0.7.0 2026-07-28 15:41:36 +08:00
yw1573 dd79918940 style(gui): 语言下拉框移至窗口右上角 2026-07-28 15:39:30 +08:00
yw1573 015f2636b3 feat(i18n): 新增中英文国际化支持,语言下拉框即时切换 2026-07-28 15:18:58 +08:00
yw1573 935665e7e8 refactor: 移除CLI模块,项目改为纯GUI桌面应用 2026-07-28 14:56:49 +08:00
yw1573 ca436b577b chore: 排除code目录,仅本地参考 2026-07-28 14:46:29 +08:00
yw1573 f6b118293c feat(printer): 新增局域网发送3MF至A1打印机功能 2026-07-28 14:45:11 +08:00
20 changed files with 764 additions and 1258 deletions
+2 -1
View File
@@ -16,4 +16,5 @@ Thumbs.db
*.bak
/settings.json
/reference
codec.py
codec.py/code/
code
+4 -154
View File
@@ -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` 中的规则 |
+1 -1
View File
@@ -6,6 +6,6 @@
# 应用程序显示名称
APP_NAME = "A1 Swap Mod Packer"
# 语义化版本号
__version__ = "0.6.0"
__version__ = "0.6.1"
# 窗口标题:应用名称 + 版本号
APP_TITLE = f"{APP_NAME} v{__version__}"
-191
View File
@@ -1,191 +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
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")
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.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())
+360 -264
View File
File diff suppressed because it is too large Load Diff
+50
View File
@@ -0,0 +1,50 @@
"""国际化模块:提供 t(key, **kwargs) 翻译函数,支持语言切换。
模块导入时自动加载 zh_CN 作为默认语言
通过 switch(locale) 切换语言t() 返回当前语言的翻译文本
"""
from __future__ import annotations
import json
from pathlib import Path
_I18N_DIR = Path(__file__).parent
_translations: dict[str, str] = {}
_current_locale: str = "zh_CN"
def load(locale: str) -> None:
"""加载指定语言的翻译文件。"""
global _translations, _current_locale
_current_locale = locale
path = _I18N_DIR / f"{locale}.json"
if path.exists():
_translations = json.loads(path.read_text(encoding="utf-8"))
else:
_translations = {}
def switch(locale: str) -> None:
"""切换当前语言并返回新语言代码。"""
load(locale)
def current_locale() -> str:
"""返回当前语言代码。"""
return _current_locale
def t(key: str, **kwargs: object) -> str:
"""获取翻译文本,支持 {name} 插值。
如果 key 不存在返回 key 本身作为回退
"""
text = str(_translations.get(key, key))
if kwargs:
text = text.format(**{k: v for k, v in kwargs.items()})
return text
# 模块导入时自动加载默认中文翻译
load("zh_CN")
+104
View File
@@ -0,0 +1,104 @@
{
"_lang": "English",
"gui.file_group.title": "Input 3MF Files",
"gui.file_group.table.header.order": "Order",
"gui.file_group.table.header.file": "3MF File",
"gui.file_group.table.header.copies": "Copies",
"gui.file_group.table.header.time": "Time",
"gui.file_group.table.header.filament": "Filament",
"gui.file_group.table.tooltip": "Drag .3mf files or folders here. Folders add all top-level .3mf files.",
"gui.file_group.table.move_up": "Move up",
"gui.file_group.table.move_down": "Move down",
"gui.file_group.table.tooltip_base_plates": "Base plates: {count}",
"gui.file_group.table.tooltip_base_time": "Base time: {time}",
"gui.file_group.table.tooltip_base_filament": "Base filament: {filament}",
"gui.file_group.total_summary": "Total: {plates} plate(s) | Time: {time} | Filament: {filament}",
"gui.file_group.total_summary.empty": "Total: 0 plates | Time: 0s | Filament: 0.00 g",
"gui.button.add_3mf": "Add 3MF",
"gui.button.remove": "Remove",
"gui.button.remove_all": "Remove All",
"gui.button.apply_copies": "Apply Default Copies to Selected",
"gui.button.build": "Build 3MF",
"gui.preview.title": "Selected Thumbnail",
"gui.preview.select_hint": "Select an input file",
"gui.preview.no_image": "No preview image",
"gui.preview.unavailable": "Preview unavailable",
"gui.options.group.title": "Packing Options",
"gui.options.swap_gcode.label": "Swap G-code",
"gui.options.refresh": "Refresh",
"gui.options.open_folder": "Open Folder",
"gui.options.default_copies": "Default copies for new inputs",
"gui.options.bed_cooldown": "Wait for bed cooldown",
"gui.options.bed_cooldown.label": "Bed cooldown",
"gui.options.bed_cooldown.suffix": "°C",
"gui.options.eject_wait": "Wait after swap",
"gui.options.eject_wait.label": "Wait after swap",
"gui.options.wait_suffix": "sec",
"gui.options.plate_number": "Show current plate in the hundreds digit of remaining time",
"gui.options.plate_number.label": "Plate number",
"gui.options.final_swap": "Run swap G-code after the last plate",
"gui.options.final_swap.label": "Final swap",
"gui.options.gcode_patches": "Apply editable G-code patches",
"gui.options.gcode_patches.label": "G-code patches",
"gui.options.gcode_patches.tooltip": "Uses gcode_patches.ini",
"gui.options.open_config": "Open Config",
"gui.options.metadata.label": "3MF metadata",
"gui.options.metadata.source": "Keep source prediction and weight",
"gui.options.metadata.sum": "Sum prediction and filament",
"gui.options.zip_compression.label": "ZIP compression",
"gui.options.zip_compression.level_format": "Level {level}",
"gui.options.zip_compression.tooltip": "zlib-ng Deflate compression level for the output 3MF.",
"gui.options.batch_mode": "Individual batch mode",
"gui.options.batch_mode.label": "Batch mode",
"gui.options.batch_mode.tooltip": "Build each input row as a separate output file, using that row's copy count.",
"gui.options.clear_after_build": "Clear input list after successful build",
"gui.options.clear_after_build.label": "After build",
"gui.options.skip_duplicates": "Skip duplicate file paths when adding inputs",
"gui.options.input_handling": "Input handling",
"gui.options.language": "Language",
"gui.output.group.title": "Output",
"gui.output.dir.label": "Output directory",
"gui.output.dir.placeholder": "Leave empty to write next to the input file",
"gui.output.dir.browse": "Browse",
"gui.output.filename_rule": "Output filename rule",
"gui.output.filename_rule.placeholder": "Use tokens such as {source}, {sources}, {plates}, {copies}, {date}, {time}",
"gui.output.filename_rule.help": "?",
"gui.output.filename_rule.help.tooltip": "Show filename token help",
"gui.output.preview": "Preview",
"gui.output.batch_preview": "{first} | {count} output file(s)",
"gui.dialog.individual_batch.title": "Individual Batch Mode",
"gui.dialog.individual_batch.text": "Individual batch mode treats every input row as a separate build.\n\nExample: if you add 20 single-plate 3MF files and set copies to 5, Build 3MF will create 20 separate packed files. Each output contains only that source file repeated 5 times.\n\nThis is useful for quickly batch-converting many independent 3MF jobs into multi-copy SwapMod packs. It does not combine all input files into one 3MF.",
"gui.dialog.filename_help.title": "Output Filename Rule",
"gui.dialog.filename_help.text": "Available filename tokens:\n\n{source} - First input file stem, without .3mf\n{sources} - Source name summary. For multiple different inputs, it becomes first_source_and_N_more\n{plates} - Total plate count in the output\n{copies} - Total copy count used for the output\n{date} - Current date as YYYYMMDD\n{time} - Current time as HHMMSS\n\nDefault rule:\n{plates}-Plates-{sources}.3mf\n\nIn individual batch mode, these tokens are calculated separately for each input row.",
"gui.dialog.warning.title": "A1 Swap Mod Packer",
"gui.dialog.no_files": "Please add at least one 3MF file.",
"gui.dialog.no_swap_gcode": "Please put at least one swap G-code file in the swap_gcode folder and select it.",
"gui.dialog.patch_config_not_found": "Patch config does not exist yet:\n{path}",
"gui.dialog.batch_partial_success": "Built {success} file(s), but {failed} file(s) failed.\n\n",
"gui.dialog.choose_output_dir": "Choose output directory",
"gui.dialog.add_files_title": "Add 3MF files",
"gui.dialog.file_filter.3mf": "3MF files (*.3mf)",
"gui.dialog.file_filter.all": "All files (*)",
"gui.toast.combined_success": "The packed 3MF file was created.",
"gui.toast.batch_success": "Created {count} packed 3MF file(s).",
"gui.log.added_files": "Added {added} input file(s). Skipped {skipped}.",
"gui.log.skipped_file": "Skipped {path}: {reason}",
"gui.log.no_swap_files": "No swap G-code files found in {dir}",
"gui.log.building_individual": "Building {count} individual output(s) with {workers} worker(s).",
"gui.log.output": "Output: {path}",
"gui.log.plates": "Plates: {count}",
"gui.log.time": "Estimated source time: {time}",
"gui.log.filament": "Estimated source filament: {filament}",
"gui.log.md5": "G-code MD5: {md5}",
"gui.log.build_error": "Error building {path}: {error}",
"format.unknown": "Unknown",
"gui.button.close": "Close",
"gui.dialog.view_gcode.title": "View Swap G-code",
"gui.options.view_gcode": "View",
"gui.dialog.build_result.title": "Build Result",
"gui.options.repack_3mf": "Repack 3MF",
"gui.dialog.repack_select_folder": "Select unpacked 3MF folder",
"gui.dialog.repack_success.title": "Repack Complete",
"gui.dialog.repack_success.text": "3MF has been repacked:\n{path}",
"gui.dialog.repack_no_content_types": "[Content_Types].xml not found. Not a valid unpacked 3MF directory."
}
+104
View File
@@ -0,0 +1,104 @@
{
"_lang": "简体中文",
"gui.file_group.title": "输入 3MF 文件",
"gui.file_group.table.header.order": "顺序",
"gui.file_group.table.header.file": "3MF 文件",
"gui.file_group.table.header.copies": "份数",
"gui.file_group.table.header.time": "时间",
"gui.file_group.table.header.filament": "耗材",
"gui.file_group.table.tooltip": "将 .3mf 文件或文件夹拖放到此处。文件夹将添加所有顶层 .3mf 文件。",
"gui.file_group.table.move_up": "上移",
"gui.file_group.table.move_down": "下移",
"gui.file_group.table.tooltip_base_plates": "基础板数:{count}",
"gui.file_group.table.tooltip_base_time": "基础时间:{time}",
"gui.file_group.table.tooltip_base_filament": "基础耗材:{filament}",
"gui.file_group.total_summary": "合计:{plates} 板 | 时间:{time} | 耗材:{filament}",
"gui.file_group.total_summary.empty": "合计:0 板 | 时间:0s | 耗材:0.00 g",
"gui.button.add_3mf": "添加 3MF",
"gui.button.remove": "移除",
"gui.button.remove_all": "全部移除",
"gui.button.apply_copies": "应用默认份数至选中行",
"gui.button.build": "构建 3MF",
"gui.preview.title": "选中文件的缩略图",
"gui.preview.select_hint": "选择一个输入文件",
"gui.preview.no_image": "无预览图",
"gui.preview.unavailable": "预览不可用",
"gui.options.group.title": "打包选项",
"gui.options.swap_gcode.label": "换盘 G-code",
"gui.options.refresh": "刷新",
"gui.options.open_folder": "打开文件夹",
"gui.options.view_gcode": "查看",
"gui.options.default_copies": "新输入的默认份数",
"gui.options.bed_cooldown": "等待热床降温",
"gui.options.bed_cooldown.label": "热床降温",
"gui.options.bed_cooldown.suffix": "°C",
"gui.options.eject_wait": "换盘后等待",
"gui.options.eject_wait.label": "换盘后等待时间",
"gui.options.wait_suffix": "秒",
"gui.options.plate_number": "在剩余时间的百位显示当前板号",
"gui.options.plate_number.label": "剩余时间板号",
"gui.options.final_swap": "最后一块盘后也执行换盘 G-code",
"gui.options.final_swap.label": "最后换盘",
"gui.options.gcode_patches": "应用可编辑的 G-code 补丁",
"gui.options.gcode_patches.label": "G-code 补丁",
"gui.options.gcode_patches.tooltip": "使用 gcode_patches.ini",
"gui.options.open_config": "打开配置文件",
"gui.options.metadata.label": "3MF 元数据",
"gui.options.metadata.source": "保留原始预测和重量",
"gui.options.metadata.sum": "累加预测和耗材用量",
"gui.options.zip_compression.label": "ZIP 压缩",
"gui.options.zip_compression.level_format": "级别 {level}",
"gui.options.zip_compression.tooltip": "输出 3MF 的 zlib-ng Deflate 压缩级别。",
"gui.options.batch_mode": "独立批处理模式",
"gui.options.batch_mode.label": "批处理模式",
"gui.options.batch_mode.tooltip": "将每行输入构建为独立的输出文件,使用该行的份数。",
"gui.options.clear_after_build": "构建成功后清空输入列表",
"gui.options.clear_after_build.label": "构建成功后",
"gui.options.skip_duplicates": "添加输入时跳过重复文件路径",
"gui.options.input_handling": "输入处理",
"gui.options.language": "语言",
"gui.output.group.title": "输出",
"gui.output.dir.label": "输出目录",
"gui.output.dir.placeholder": "留空则写入输入文件所在目录",
"gui.output.dir.browse": "浏览",
"gui.output.filename_rule": "输出文件名规则",
"gui.output.filename_rule.placeholder": "可使用 {source}、{sources}、{plates}、{copies}、{date}、{time} 等标记",
"gui.output.filename_rule.help": "?",
"gui.output.filename_rule.help.tooltip": "显示文件名标记帮助",
"gui.output.preview": "预览",
"gui.output.batch_preview": "{first} | {count} 个输出文件",
"gui.dialog.individual_batch.title": "独立批处理模式",
"gui.dialog.individual_batch.text": "独立批处理模式将每一行输入作为独立的构建任务。\n\n示例:如果你添加了 20 个单板 3MF 文件并设置份数为 5,点击\"构建 3MF\"将创建 20 个独立的打包文件。每个输出文件仅包含该源文件重复 5 次的内容。\n\n这个功能适用于快速将大量独立的 3MF 作业批量转换为多份数的 SwapMod 包。它不会将所有输入文件合并到一个 3MF 中。",
"gui.dialog.filename_help.title": "输出文件名规则",
"gui.dialog.filename_help.text": "可用的文件名标记:\n\n{source} — 第一个输入文件的文件名(不含 .3mf)\n{sources} — 源文件名称摘要。多个不同输入时,显示为 first_source_and_N_more\n{plates} — 输出中的总板数\n{copies} — 输出中使用的总份数\n{date} — 当前日期,格式 YYYYMMDD\n{time} — 当前时间,格式 HHMMSS\n\n默认规则:\n{plates}-Plates-{sources}.3mf\n\n在独立批处理模式下,这些标记会针对每行输入单独计算。",
"gui.dialog.warning.title": "A1 Swap Mod Packer",
"gui.dialog.no_files": "请至少添加一个 3MF 文件。",
"gui.dialog.no_swap_gcode": "请在 swap_gcode 文件夹中放入至少一个换盘 G-code 文件并选择它。",
"gui.dialog.patch_config_not_found": "补丁配置文件不存在:{path}",
"gui.dialog.batch_partial_success": "已构建 {success} 个文件,但有 {failed} 个文件失败。\n\n",
"gui.dialog.choose_output_dir": "选择输出目录",
"gui.dialog.add_files_title": "添加 3MF 文件",
"gui.dialog.file_filter.3mf": "3MF 文件 (*.3mf)",
"gui.dialog.file_filter.all": "所有文件 (*)",
"gui.toast.combined_success": "已创建打包的 3MF 文件。",
"gui.toast.batch_success": "已创建 {count} 个打包的 3MF 文件。",
"gui.log.added_files": "已添加 {added} 个输入文件。跳过 {skipped} 个。",
"gui.log.skipped_file": "已跳过 {path}{reason}",
"gui.log.no_swap_files": "在 {dir} 中未找到换盘 G-code 文件",
"gui.log.building_individual": "正在使用 {workers} 个工作线程构建 {count} 个独立输出。",
"gui.log.output": "输出:{path}",
"gui.log.plates": "板数:{count}",
"gui.log.time": "预估源文件时间:{time}",
"gui.log.filament": "预估源文件耗材:{filament}",
"gui.log.md5": "G-code MD5{md5}",
"gui.log.build_error": "构建 {path} 时出错:{error}",
"format.unknown": "未知",
"gui.button.close": "关闭",
"gui.dialog.view_gcode.title": "查看换盘 G-code",
"gui.dialog.build_result.title": "构建结果",
"gui.options.repack_3mf": "重新打包 3MF",
"gui.dialog.repack_select_folder": "选择解压后的 3MF 文件夹",
"gui.dialog.repack_success.title": "重新打包完成",
"gui.dialog.repack_success.text": "已重新打包为 3MF 文件:\n{path}",
"gui.dialog.repack_no_content_types": "未找到 [Content_Types].xml,不是有效的 3MF 解压目录。"
}
+9 -16
View File
@@ -3,15 +3,16 @@ chcp 65001 >nul
setlocal
set "PY=.venv\Scripts\python.exe"
set "DIST=build\onefile"
set "DIST=build\a1packer"
if not exist "%DIST%" mkdir "%DIST%"
"%PY%" -m nuitka ^
--mode=onefile ^
--mode=standalone ^
--msvc=latest ^
--enable-plugin=pyside6 ^
--include-qt-plugins=platforms,imageformats,styles,iconengines ^
--include-data-dir="a1_swap_mod_packer/i18n=a1_swap_mod_packer/i18n" ^
--windows-console-mode=disable ^
--output-dir="%DIST%" ^
--output-filename=a1packer.exe ^
@@ -21,26 +22,18 @@ 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
set "OUTPUT=%DIST%\a1packer.dist"
robocopy "swap_gcode" "%OUTPUT%\swap_gcode" /E
if %ERRORLEVEL% GEQ 8 exit /b %ERRORLEVEL%
copy /Y "gcode_patches.ini" "%DIST%\gcode_patches.ini" >nul
copy /Y "gcode_patches.ini" "%OUTPUT%\gcode_patches.ini" >nul
if errorlevel 1 exit /b %ERRORLEVEL%
copy /Y "x.png" "%DIST%\x.png" >nul
copy /Y "x.png" "%OUTPUT%\x.png" >nul
if errorlevel 1 exit /b %ERRORLEVEL%
echo.
echo Build done: %DIST%
echo Build done: %OUTPUT%
echo The entire "%OUTPUT%" folder is the portable distribution.
exit /b 0
+8 -52
View File
@@ -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()<br/>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 |
| 脚本/自动化友好 | ✅ | ❌ |
+31
View File
@@ -0,0 +1,31 @@
# i18n 国际化实施计划
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 为 A1 Swap Mod Packer 添加中英文国际化支持,用户通过下拉框切换语言,切换即时生效并持久化。
**Architecture:** 新建 `i18n/` 模块,提供 `t(key, **kwargs)` 翻译函数。GUI 中所有硬编码中文替换为 `t("key")` 调用。在打包选项区添加语言下拉框,切换时重新加载翻译并刷新 UI。
**Tech Stack:** Python 标准库 json + PySide6 QComboBox
## Global Constraints
- 所有翻译键命名遵循 `模块.区域.属性` 层级
- `t()` 在键缺失时返回键名本身作为回退
- 语言选择持久化到 `settings.json``locale` 字段,默认 `zh_CN`
- 数值格式化(时间、耗材)保持不翻译,仅在 None 时返回翻译后的占位符
---
## 文件结构
| 文件 | 操作 | 职责 |
|------|------|------|
| `a1_swap_mod_packer/i18n/__init__.py` | 新建 | 翻译加载器 + `t()` 函数 |
| `a1_swap_mod_packer/i18n/zh_CN.json` | 新建 | 简体中文翻译表 |
| `a1_swap_mod_packer/i18n/en.json` | 新建 | 英文翻译表 |
| `a1_swap_mod_packer/gui.py` | 修改 | 字符串替换为 `t()` + 语言下拉框 |
| `a1_swap_mod_packer/gcode.py` | 修改 | `format_duration`/`format_filament` 的 None 返回翻译 |
| `a1_swap_mod_packer/core.py` | 修改 | 导出 `i18n` 模块的 `t` |
---
+90
View File
@@ -0,0 +1,90 @@
"""将解压后的 3MF 文件夹重新打包为 .3mf 文件的工具脚本。
用法:
python repack_3mf.py "解压目录" "output.3mf"
会自动更新 Metadata/plate_N.gcode.md5 并确保 [Content_Types].xml ZIP 第一条目
如果省略输出路径自动在解压目录同级生成同名 .3mf
"""
from __future__ import annotations
import hashlib
import os
import re
import sys
import zipfile
from pathlib import Path
GCODE_MEMBER_RE = re.compile(r"^Metadata[\\/]plate_(\d+)\.gcode$")
def update_md5(src_dir: Path) -> None:
"""更新所有 plate_N.gcode 对应的 .md5 文件。"""
metadata = src_dir / "Metadata"
if not metadata.is_dir():
return
for gcode_file in sorted(metadata.glob("plate_*.gcode")):
if not GCODE_MEMBER_RE.match(str(gcode_file.relative_to(src_dir)).replace("\\", "/")):
continue
h = hashlib.md5()
with open(gcode_file, "rb") as f:
h.update(f.read())
md5_path = Path(str(gcode_file) + ".md5")
md5_path.write_text(h.hexdigest(), encoding="ascii")
print(f" MD5 已更新: {md5_path.name}{h.hexdigest()}")
def repack(src_dir: Path, output: Path) -> None:
"""将解压目录重新打包为 3MF。"""
ct_path = src_dir / "[Content_Types].xml"
if not ct_path.exists():
raise FileNotFoundError(f"未找到 [Content_Types].xml,请确认 {src_dir} 是有效的 3MF 解压目录")
print(f"源目录: {src_dir}")
update_md5(src_dir)
with zipfile.ZipFile(output, "w", zipfile.ZIP_DEFLATED) as z:
z.write(ct_path, "[Content_Types].xml")
count = 1
for root, _dirs, files in os.walk(str(src_dir)):
for fn in files:
fp = Path(root) / fn
arcname = fp.relative_to(src_dir).as_posix()
if arcname == "[Content_Types].xml":
continue
z.write(str(fp), arcname)
count += 1
size_mb = output.stat().st_size / (1024 * 1024)
print(f"输出文件: {output} ({size_mb:.1f} MB, {count} 条目)")
print("打包完成。")
def main() -> int:
if len(sys.argv) < 2:
print(__doc__)
return 1
src = Path(sys.argv[1])
if not src.is_dir():
print(f"错误: 目录不存在 — {src}")
return 1
if len(sys.argv) >= 3:
output = Path(sys.argv[2])
else:
output = src.parent / (src.name.rstrip("/\\") + ".3mf")
try:
repack(src, output)
except Exception as exc:
print(f"错误: {exc}")
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main())
-4
View File
@@ -1,4 +0,0 @@
from a1_swap_mod_packer.cli import main
if __name__ == "__main__":
raise SystemExit(main())
-132
View File
@@ -1,132 +0,0 @@
;==========================================================
; A1 SwapMod 换板 G-code — 类型 B:双轮抖板
; 特点:G380 S3/S2 交替下压、双轮 Shake 抖板(最大3.5mm偏移)、无 M400/M17
;==========================================================
; 开始换板
; --- 阶段1:Y 轴归零并退到后端 ---
G28 Y ; Y 轴归零(寻找限位开关)
G1 Y262 F1200 ; 推到 Y262(后端位置,靠近 Z 轴立柱)
; --- 阶段2Z 轴交替下压(G380 S3/S2 ×4轮),压紧打印板 ---
G91 ; 切换相对坐标模式
G380 S3 Z-25 F1200 ; 力传感器碰撞退出:Z 轴下降 25mm 直到碰到板
G380 S2 Z85 F1200 ; 力传感器目标位移动:Z 轴上升 85mm 到预设力值
G380 S3 Z-20 F1200 ; 第二轮下压碰撞检测
G380 S2 Z85 F1200 ; 第二轮力控上升
G380 S3 Z-20 F1200 ; 第三轮下压
G380 S2 Z85 F1200 ; 第三轮上升
G380 S3 Z-20 F1200 ; 第四轮下压(确保板完全贴合热床)
G380 S2 Z85 F1200 ; 第四轮上升
G1 Z-5 F1200 ; Z 轴下降 5mm 释放上升张力
G90 ; 切回绝对坐标模式
; --- 阶段3:驻留稳定 ---
G4 P2000 ; 驻留 2 秒,让板稳定贴合
; --- 阶段4:前推弹板 — 利用惯性将板从热床上推离 ---
G1 Y15 F2000 ; 先推到前端附近 Y15
G1 Y150 F1200 ; 后退一段
G1 Y40 F1200 ; 再推向前端
; 以下注释掉的 Y260 是备选方案
; G1 Y260 F3000
; 用触力传感器推到最前端 Y264(力控,不会硬撞)
G380 S2 Y264 F2000 ; A1 专有:以预设力矩正向推进到 Y264
; --- 阶段5:Z 轴上撤退让,为弹射留出空间 ---
G91 ; 切相对坐标
; G380 S3 Z-55 F1200 ; 注释掉:原用触力退出方式
G1 Z-55 F1200 ; Z 轴下降 55mm(使喷嘴远离板面)
G90 ; 切回绝对坐标
; --- 阶段6:Y 轴快速移动 + 微调定位 ---
G1 Y120 F4000 ; 快速移动到 Y120
G28 Y ; Y 轴再次归零校准
; 使用 G1 而非 G380 是为了防止触力传感器干扰定位精度
G1 Y-1 F1200 ; Y 轴后移 1mm(为抖板准备起始位置)
; G380 S3 Y-1 F1200 ; 注释掉:原触力退出方式
G4 P500 ; 驻留 500ms
; ═══════════════════════════════════════════
; 抖板 第1轮:微小 Y 轴往复振动,确保板完全脱离
; 振幅逐步增大:1mm → 3.5mm
; ═══════════════════════════════════════════
; 开始 振动触发换板
G1 Y1 F100 ; 前进 1mm(慢速)
G1 Y0 F20000 ; 快速退回
G4 P500 ; 驻留 500ms
G1 Y1.5 F100 ; 前进 1.5mm
G1 Y0 F20000 ; 快速退回
G4 P500
G1 Y2 F100 ; 前进 2mm
G1 Y0 F20000 ; 快速退回
G4 P500
G1 Y2.5 F100 ; 前进 2.5mm
G1 Y0 F20000 ; 快速退回
G4 P500
G1 Y3 F100 ; 前进 3mm
G1 Y0 F10000 ; 退回(降速)
G4 P500
G1 Y3.5 F100 ; 前进 3.5mm(最大振幅)
G1 Y0 F5000 ; 退回(进一步降速)
; 结束 振动触发换板
; --- 阶段7:推板前移 + 力控校准 ---
G1 Y235 F1200 ; 推到 Y235
G1 Y110 F5000 ; 后退
G1 Y200 F1200 ; 再前推
; G1 Y266 F1500 ; 注释掉:备选位置
G380 S2 Y264 F2000 ; 力控推到最前端 Y264
; 将打印板缩回到最前端后,驻留 1 秒
G4 P1000 ; 驻留 1 秒
; --- 阶段8:再次弹射 ---
G1 Y40 F4000 ; 推到 Y40
G1 Y-2 F5000 ; ⚡ 高速反向弹射 2mm
; ═══════════════════════════════════════════
; 抖板 第2轮:再次振动,确保没有残留的板
; 振幅:1mm → 3.5mm(与第1轮相同)
; ═══════════════════════════════════════════
; 开始 振动触发换板
G1 Y1 F100
G1 Y0 F20000
G4 P500
G1 Y1.5 F100
G1 Y0 F20000
G4 P500
G1 Y2 F100
G1 Y0 F20000
G4 P500
G1 Y2.5 F100
G1 Y0 F20000
G4 P500
G1 Y3 F100
G1 Y0 F10000
G4 P500
G1 Y3.5 F100
G1 Y0 F5000
; 以下为注释掉的备选抖板参数
; G1 Y2.5 F100
; G1 Y0 F8000
; G4 P1000
; G1 Y3.5 F100
; G1 Y0 F5000
; 结束 振动触发换板
; --- 阶段9:归位到待机位置 ---
G1 Y120 F2000 ; 回到 Y120 中间待机位置
; 结束换板
-140
View File
@@ -1,140 +0,0 @@
;==========================================================
; A1 SwapMod 换板 G-code — 类型 C1:安全换板(标准版)
; 特点:M400/M17 保护、G1 Z- + G380 S2 纯力控下压、双轮抖板(2.5mm偏移)
;==========================================================
; 开始换板
; --- 阶段1:Y 轴归零并退到后端 ---
G28 Y ; Y 轴归零(寻找限位开关)
G1 Y262 F2000 ; 推到 Y262(后端位置,速度 F2000)
; --- 阶段2:运动保护 — 等所有运动停止 + 降低 Z 电机电流 ---
M400 ; 等待所有运动队列清空
M17 Z0.4 ; 将 Z 轴电机电流降至 0.4A,降低阻力,防止后续 Y 轴移动时 Z 轴硬怼损伤打印件
; --- 阶段3Z 轴下压(G1 Z- + G380 S2 纯力控 ×4轮),压紧打印板 ---
G91 ; 切换相对坐标模式
G1 Z-35 F1200 ; Z 轴下降 35mm
G380 S2 Z95 ; 力控上升 95mm 到预设力值
G1 Z-25 ; Z 轴再降 25mm
G380 S2 Z90 ; 力控上升 90mm
G1 Z-25 ; 第三轮
G380 S2 Z90
G1 Z-30 ; 第四轮
G380 S2 Z95
G1 Z-5 ; Z 轴微降 5mm 释放张力
G90 ; 切回绝对坐标
; --- 阶段4:驻留稳定 ---
G4 P2000 ; 驻留 2 秒
; --- 阶段5:前推弹板 — 利用惯性将板推离 ---
G1 Y15 F800 ; 缓慢推到前端 Y15
G1 Y150 F1200 ; 后退到 Y150
; 此行控制卷料尾端的悬垂距离。值越小,悬垂越少。
G1 Y40 F1000 ; 推到 Y40
; 平台通过力控确保即使没有板也能正常装入新板
; G1 Y260 F3000 ; 注释掉:备选方案
G380 S2 Y264 F2000 ; 力控推到最前端 Y264
; --- 阶段6Z 轴上撤退让 ---
G91 ; 切相对坐标
G1 Z-70 F1200 ; Z 轴下降 70mm(使喷嘴远离板面,为弹射留空间)
G90 ; 切回绝对坐标
; --- 阶段7:Y 轴高速移动 + 微调到弹射起始位 ---
G1 Y200 F10000 ; 高速推到 Y200
G1 Y25 F4000 ; 减速靠前
; 使用 G1 而非 G380,防止触力传感器干扰定位
G1 Y-1 F20000 ; 高速后退 1mm(为抖板准备起始位)
; G380 S3 Y-1 F1200 ; 注释掉:原触力退出方式
G4 P500 ; 驻留 500ms
; ═══════════════════════════════════════════
; 抖板 第1轮:微小 Y 轴往复振动,确保板完全脱离
; 振幅逐步增大:1mm → 2.5mm(比类型B更保守)
; ═══════════════════════════════════════════
G28 Y ; 先 Y 轴归零校准
; 开始 振动触发换板
G1 Y1 F100 ; 前进 1mm(慢速)
G1 Y0 F20000 ; 快速退回
G4 P500
G1 Y1.5 F100 ; 前进 1.5mm
G1 Y0 F20000 ; 快速退回
G4 P500
G1 Y2 F100 ; 前进 2mm
G1 Y0 F20000 ; 快速退回
G4 P500
G1 Y2.5 F100 ; 前进 2.5mm
G1 Y0 F20000 ; 快速退回
G4 P500
G1 Y2.5 F100 ; 再次 2.5mm(巩固)
G1 Y0 F10000 ; 退回(降速)
G4 P500
; 注释掉的更大振幅
; G1 Y3.5 F100
; G1 Y0 F5000
; 结束 振动触发换板
; --- 阶段8:推板前移 + 力控校准 ---
G1 Y235 F1200 ; 推到 Y235
G1 Y110 F5000 ; 后退
G1 Y200 F1200 ; 再前推
; G1 Y266 F1500 ; 注释掉:备选
G380 S2 Y264 F2000 ; 力控推到最前端 Y264
; 将打印板缩回到最前端后,驻留 1 秒
G4 P1000
; --- 阶段9:第2轮弹射 ---
G1 Y25 F4000 ; 推到 Y25
G1 Y-2 F20000 ; ⚡ 高速反向弹射 2mm(F20000,最快的弹射速度)
; G1 Y-2 F1000 ; 慢速备选
G4 P500 ; 驻留 500ms
; ═══════════════════════════════════════════
; 抖板 第2轮:再次振动确保无残留
; 振幅:1mm → 2.5mm
; ═══════════════════════════════════════════
; 开始 振动触发换板
G1 Y1 F100
G1 Y0 F20000
G4 P500
G1 Y1.5 F100
G1 Y0 F20000
G4 P500
G1 Y2 F100
G1 Y0 F20000
G4 P500
G1 Y2.5 F100
G1 Y0 F20000
G4 P500
G1 Y2.5 F100
G1 Y0 F10000
G4 P500
; 结束 振动触发换板
; --- 阶段10:归位 ---
; 推到前端
G1 Y262 F1500 ; 推到 Y262
G1 Y120 F2000 ; 回到 Y120 待机位置
; 结束换板
-133
View File
@@ -1,133 +0,0 @@
;==========================================================
; A1 SwapMod 换板 G-code — 类型 C2:安全换板(慢速版)
; 特点:与 C1 标准版完全相同,仅第2轮弹射速度从 F20000 降为 F1000
; 更温和的弹射,减少对板和打印机的冲击
;==========================================================
; 开始换板
; --- 阶段1:Y 轴归零并退到后端 ---
G28 Y ; Y 轴归零(寻找限位开关)
G1 Y262 F2000 ; 推到 Y262(后端位置)
; --- 阶段2:运动保护 ---
M400 ; 等待所有运动队列清空
M17 Z0.4 ; 将 Z 轴电机电流降至 0.4A,降低阻力,防止碰撞损伤打印件
; --- 阶段3Z 轴下压(G1 Z- + G380 S2 纯力控 ×4轮)---
G91 ; 切换相对坐标
G1 Z-35 F1200 ; Z 轴下降 35mm
G380 S2 Z95 ; 力控上升 95mm
G1 Z-25 ; 第二轮
G380 S2 Z90
G1 Z-25 ; 第三轮
G380 S2 Z90
G1 Z-30 ; 第四轮
G380 S2 Z95
G1 Z-5 ; Z 轴微降释放张力
G90 ; 切回绝对坐标
; --- 阶段4:驻留稳定 ---
G4 P2000 ; 驻留 2 秒
; --- 阶段5:前推弹板 ---
G1 Y15 F800 ; 缓慢推到前端 Y15
G1 Y150 F1200 ; 后退到 Y150
; 此行控制卷料尾端的悬垂距离。值越小,悬垂越少。
G1 Y40 F1000 ; 推到 Y40
; 平台通过力控确保即使没有板也能正常装入新板
; G1 Y260 F3000
G380 S2 Y264 F2000 ; 力控推到最前端 Y264
; --- 阶段6Z 轴上撤退让 ---
G91 ; 切相对坐标
G1 Z-70 F1200 ; Z 轴下降 70mm(为弹射留空间)
G90 ; 切回绝对坐标
; --- 阶段7:Y 轴高速移动 + 微调定位 ---
G1 Y200 F10000 ; 高速推到 Y200
G1 Y25 F4000 ; 减速靠前
; 使用 G1 而非 G380,防止触力传感器干扰定位
G1 Y-1 F20000 ; 高速后退 1mm
G4 P500 ; 驻留 500ms
; --- 抖板校准归零 ---
G28 Y ; Y 轴归零校准
; ═══════════════════════════════════════════
; 抖板 第1轮:振动确保板完全脱离(1→2.5mm)
; ═══════════════════════════════════════════
; 开始 振动触发换板
G1 Y1 F100
G1 Y0 F20000
G4 P500
G1 Y1.5 F100
G1 Y0 F20000
G4 P500
G1 Y2 F100
G1 Y0 F20000
G4 P500
G1 Y2.5 F100
G1 Y0 F20000
G4 P500
G1 Y2.5 F100
G1 Y0 F10000
G4 P500
; G1 Y3.5 F100 ; 注释掉:更大的振幅(备选)
; G1 Y0 F5000
; 结束 振动触发换板
; --- 阶段8:推板 + 力控校准 ---
G1 Y235 F1200
G1 Y110 F5000
G1 Y200 F1200
; G1 Y266 F1500
G380 S2 Y264 F2000 ; 力控推到最前端
; 驻留 1 秒
G4 P1000
; --- 阶段9:第2轮弹射(🐌 慢速版关键差异)---
G1 Y25 F4000 ; 推到 Y25
; G1 Y-2 F20000 ; 注释掉:原标准版高速弹射
G1 Y-2 F1000 ; 🐌 慢速弹射(F1000 vs 标准版 F20000
; 更温和,减少对板和结构的冲击
G4 P500
; ═══════════════════════════════════════════
; 抖板 第2轮:再次振动(1→2.5mm)
; ═══════════════════════════════════════════
; 开始 振动触发换板
G1 Y1 F100
G1 Y0 F20000
G4 P500
G1 Y1.5 F100
G1 Y0 F20000
G4 P500
G1 Y2 F100
G1 Y0 F20000
G4 P500
G1 Y2.5 F100
G1 Y0 F20000
G4 P500
G1 Y2.5 F100
G1 Y0 F10000
G4 P500
; 结束 振动触发换板
; --- 阶段10:归位 ---
; 推到前端
G1 Y262 F1500
G1 Y120 F2000 ; 回到 Y120 待机位置
; 结束换板
-151
View File
@@ -1,151 +0,0 @@
;==========================================================
; A1 SwapMod 换板 G-code — 类型 C3:安全换板(安全Z版)⭐ 推荐
; 特点:基于 C2 慢速版,尾部追加 Z 轴安全回位(Z30),防止下一板打印时碰撞
; 本变体由 QQ 群用户 风信子号 提供
;==========================================================
;===================================
; 此变体由 风信子号 友情提供
;===================================
; 开始换板
; --- 阶段1:Y 轴归零并退到后端 ---
G28 Y ; Y 轴归零(寻找限位开关)
G1 Y262 F2000 ; 推到 Y262(后端位置)
; --- 阶段2:运动保护 ---
M400 ; 等待所有运动队列清空
M17 Z0.4 ; 将 Z 轴电机电流降至 0.4A,降低阻力,防止碰撞损伤打印件
; --- 阶段3Z 轴下压(G1 Z- + G380 S2 纯力控 ×4轮)---
G91 ; 切换相对坐标
G1 Z-35 F1200 ; Z 轴下降 35mm
G380 S2 Z95 ; 力控上升 95mm
G1 Z-25 ; 第二轮
G380 S2 Z90
G1 Z-25 ; 第三轮
G380 S2 Z90
G1 Z-30 ; 第四轮
G380 S2 Z95
G1 Z-5 ; Z 轴微降释放张力
G90 ; 切回绝对坐标
; --- 阶段4:驻留稳定 ---
G4 P2000 ; 驻留 2 秒
; --- 阶段5:前推弹板 ---
G1 Y15 F800 ; 缓慢推到前端 Y15
G1 Y150 F1200 ; 后退到 Y150
; 此行控制卷料尾端的悬垂距离。值越小,悬垂越少。
G1 Y40 F1000 ; 推到 Y40
; 平台通过力控确保即使没有板也能正常装入新板
; G1 Y260 F3000
G380 S2 Y264 F2000 ; 力控推到最前端 Y264
;=================================================
; !!!!!! Z 轴下降,离开顶部
;=================================================
; --- 阶段6Z 轴上撤退让 ---
G91 ; 切相对坐标
G1 Z-70 F1200 ; Z 轴下降 70mm(为弹射留空间)
G90 ; 切回绝对坐标
; --- 阶段7:Y 轴高速移动 + 微调定位 ---
G1 Y200 F10000 ; 高速推到 Y200
G1 Y25 F4000 ; 减速靠前
; 使用 G1 而非 G380,防止触力传感器干扰定位
G1 Y-1 F20000 ; 高速后退 1mm
G4 P500 ; 驻留 500ms
; --- 抖板校准归零 ---
G28 Y ; Y 轴归零校准
; ═══════════════════════════════════════════
; 抖板 第1轮:振动确保板完全脱离(1→2.5mm)
; ═══════════════════════════════════════════
; 开始 振动触发换板
G1 Y1 F100
G1 Y0 F20000
G4 P500
G1 Y1.5 F100
G1 Y0 F20000
G4 P500
G1 Y2 F100
G1 Y0 F20000
G4 P500
G1 Y2.5 F100
G1 Y0 F20000
G4 P500
G1 Y2.5 F100
G1 Y0 F10000
G4 P500
; G1 Y3.5 F100 ; 注释掉:备选更大振幅
; G1 Y0 F5000
; 结束 振动触发换板
; --- 阶段8:推板 + 力控校准 ---
G1 Y235 F1200
G1 Y110 F5000
G1 Y200 F1200
; G1 Y266 F1500
G380 S2 Y264 F2000 ; 力控推到最前端
; 驻留 1 秒
G4 P1000
; --- 阶段9:第2轮弹射(慢速)---
G1 Y25 F4000 ; 推到 Y25
; G1 Y-2 F20000 ; 注释掉:标准版高速
G1 Y-2 F1000 ; 🐌 慢速弹射
G4 P500
; ═══════════════════════════════════════════
; 抖板 第2轮:再次振动(1→2.5mm)
; ═══════════════════════════════════════════
; 开始 振动触发换板
G1 Y1 F100
G1 Y0 F20000
G4 P500
G1 Y1.5 F100
G1 Y0 F20000
G4 P500
G1 Y2 F100
G1 Y0 F20000
G4 P500
G1 Y2.5 F100
G1 Y0 F20000
G4 P500
G1 Y2.5 F100
G1 Y0 F10000
G4 P500
; 结束 振动触发换板
; --- 阶段10:归位 ---
; 推到前端
G1 Y262 F1500
G1 Y120 F2000 ; 回到 Y120 待机位置
; 结束换板
; ═══════════════════════════════════════════
; 🔐 安全 Z 轴回位(C3 独有)
; 换板完成后 Z 轴在低位(约 Z-70),下一板开始时可能碰撞
; 预先升到 Z30 确保安全间距
; ═══════════════════════════════════════════
G4 P3000 ; 驻留 3 秒,等待板完全稳定
M400 ; 等待所有运动完成
G90 ; 确保绝对坐标
G1 Z30 F600 ; 🔐 Z 轴升至 30mm 安全高度
M400 ; 再次确认到位
+1 -19
View File
@@ -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()
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 169 KiB

After

Width:  |  Height:  |  Size: 2.7 MiB