Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| addad17e92 | |||
| c43faeb5a2 | |||
| d362b2280a | |||
| 53a25544fa | |||
| 40c339dd74 | |||
| 7c6a9f65ef | |||
| 703929eea5 | |||
| bcff3ae264 | |||
| a9d2a0cbf1 | |||
| 5e6be787c7 | |||
| 2438eca047 | |||
| f066ca24fa | |||
| 116dcfe45f | |||
| db517b69f7 | |||
| ed02b48d80 | |||
| 68aa056359 | |||
| 2fed301a6c | |||
| dd79918940 | |||
| 015f2636b3 | |||
| 935665e7e8 | |||
| ca436b577b | |||
| f6b118293c |
+2
-1
@@ -16,4 +16,5 @@ Thumbs.db
|
||||
*.bak
|
||||
/settings.json
|
||||
/reference
|
||||
codec.py
|
||||
codec.py/code/
|
||||
code
|
||||
|
||||
@@ -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` 中的规则 |
|
||||
|
||||
@@ -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__}"
|
||||
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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")
|
||||
@@ -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."
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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) |
|
||||
| 脚本/自动化友好 | ✅ | ❌ |
|
||||
|
||||
@@ -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` |
|
||||
|
||||
---
|
||||
@@ -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())
|
||||
@@ -1,4 +0,0 @@
|
||||
from a1_swap_mod_packer.cli import main
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -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 轴立柱)
|
||||
|
||||
; --- 阶段2:Z 轴交替下压(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 中间待机位置
|
||||
|
||||
; 结束换板
|
||||
@@ -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 轴硬怼损伤打印件
|
||||
|
||||
; --- 阶段3:Z 轴下压(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
|
||||
|
||||
; --- 阶段6:Z 轴上撤退让 ---
|
||||
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 待机位置
|
||||
|
||||
; 结束换板
|
||||
@@ -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,降低阻力,防止碰撞损伤打印件
|
||||
|
||||
; --- 阶段3:Z 轴下压(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
|
||||
|
||||
; --- 阶段6:Z 轴上撤退让 ---
|
||||
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 待机位置
|
||||
|
||||
; 结束换板
|
||||
@@ -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,降低阻力,防止碰撞损伤打印件
|
||||
|
||||
; --- 阶段3:Z 轴下压(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 轴下降,离开顶部
|
||||
;=================================================
|
||||
|
||||
; --- 阶段6:Z 轴上撤退让 ---
|
||||
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
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user