bambu-printer-mcp 1.1.28 → 1.1.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTRIBUTORS.md +3 -1
- package/README.md +4 -2
- package/dist/ams-mapping.d.ts +18 -2
- package/dist/ams-mapping.js +36 -5
- package/dist/index.js +53 -45
- package/dist/printers/bambu.js +14 -16
- package/dist/safety/limits.d.ts +6 -0
- package/dist/safety/limits.js +9 -0
- package/dist/safety/print-file.d.ts +2 -0
- package/dist/safety/print-file.js +36 -6
- package/dist/safety/printer-state.d.ts +2 -0
- package/dist/safety/printer-state.js +35 -9
- package/dist/slicer/remote-slicer.d.ts +12 -0
- package/dist/slicer/remote-slicer.js +240 -0
- package/dist/stl/stl-manipulator.d.ts +1 -1
- package/dist/stl/stl-manipulator.js +70 -42
- package/package.json +1 -1
- package/src/ams-mapping.ts +38 -5
- package/src/index.ts +49 -42
- package/src/printers/bambu.ts +14 -13
- package/src/safety/limits.ts +9 -0
- package/src/safety/print-file.ts +34 -6
- package/src/safety/printer-state.ts +32 -9
- package/src/slicer/remote-slicer.ts +216 -0
- package/src/stl/stl-manipulator.ts +68 -43
package/CONTRIBUTORS.md
CHANGED
|
@@ -11,7 +11,9 @@ Thank you to everyone who builds, tests, reports problems, and shares real print
|
|
|
11
11
|
| [Patrick Juchli (@patrickjuchli)](https://github.com/patrickjuchli) | Maintains basic-ftp and supplies the 6.2.3 fixes for rejected-command recovery, asynchronous credential-error crashes, and password redaction used by direct FTPS transfers. |
|
|
12
12
|
| [Aleksei Popovich (alexpapay)](https://github.com/alexpapay) | Fixes cross-platform AMS profile resolution and packed P2S chamber status in [#43](https://github.com/DMontgomery40/bambu-printer-mcp/pull/43), contributes regressions, and supplies read-only Windows P2S/AMS 2 Pro status and local slicing evidence. No physical print is claimed for this contribution. |
|
|
13
13
|
| [Adam (@adamkaplan)](https://github.com/adamkaplan) | Identifies the reversed H2C/H2D Pro serial prefixes in [#47](https://github.com/DMontgomery40/bambu-printer-mcp/issues/47), verifies corrected H2C status locally, and isolates the official H2C startup commands and config-block marker failure in [#48](https://github.com/DMontgomery40/bambu-printer-mcp/issues/48). Provides exact Bambu Studio version/template hash and unsafe-file reproductions; maintainer validation is offline and mocked. |
|
|
14
|
-
| [
|
|
14
|
+
| [Pavel Bukhatkin (welderpb)](https://github.com/welderpb) | Requests remote API slicing for containerized deployments and specifies authentication, timeout and async needs in [#50](https://github.com/DMontgomery40/bambu-printer-mcp/issues/50). This informs the optional single-slot Orca API backend; validation uses HTTP fixtures and the genuine API application with a fake CLI, without a physical print. |
|
|
15
|
+
| [AFKFelix](https://github.com/AFKFelix) and [orca-slicer-api contributors](https://github.com/AFKFelix/orca-slicer-api) | Provide the upstream HTTP slicing application and published multipart/job contracts used by the optional remote backend. |
|
|
16
|
+
| [travismcashan](https://github.com/travismcashan) | Reports stock X2D material-switch and runout-purge rejection, misleading unchecked confirmation, and stdout diagnostics in [#44](https://github.com/DMontgomery40/bambu-printer-mcp/issues/44). Supplies exact preset syntax and firmware evidence, then reports a clean physical X2D cube print with a local workaround; maintainer validation remains isolated and mocked. Reports the accepted native job and subsequent helper teardown crash in [#46](https://github.com/DMontgomery40/bambu-printer-mcp/issues/46), with completed-print and macOS crash evidence informing result preservation and duplicate-print warnings. Contributes the X2D dual-nozzle slicer-output and dual-external-spool fix in [#59](https://github.com/DMontgomery40/bambu-printer-mcp/pull/59), with a real five-filament X2D job, live `vir_slot` evidence and the Bambu Studio virtual-tray conventions; the change is validated offline and mocked, and no physical print is claimed for it. |
|
|
15
17
|
| [nitpreet22](https://github.com/nitpreet22) | Fixes rejection of the stock A1/A1 mini `M109 H` startup waits in [#41](https://github.com/DMontgomery40/bambu-printer-mcp/pull/41), contributes temperature and model regressions, and supplies Windows Bambu Studio profile and real sliced A1 job inspection evidence. Their physical print used Bambu Studio, not this MCP change. |
|
|
16
18
|
| [Steavie (steavie)](https://github.com/steavie) | Reports the reversed A1/A1 mini serial prefixes in [#39](https://github.com/DMontgomery40/bambu-printer-mcp/issues/39), with A1 status and configuration evidence that identifies the status and fresh-identity regression. |
|
|
17
19
|
| [JaviOFC](https://github.com/JaviOFC) | Reports the X2D identity rejection in [#36](https://github.com/DMontgomery40/bambu-printer-mcp/issues/36) and supplies current firmware, connection-mode, status-field, serial-prefix, and cloud-job evidence showing that `model_id` can contain a print-job identifier while `model` correctly identifies the printer. |
|
package/README.md
CHANGED
|
@@ -210,7 +210,7 @@ The optional FULU **BambuNetwork bridge** exposes `bambu_network_bridge_status`,
|
|
|
210
210
|
- Start G-code files already stored on the printer
|
|
211
211
|
- **Collar charm print wrapper** (`print_collar_charm`) — specialized two-color workflow with fixed tray policy for inner (black, AMS 1 slot 1) and outer (white, AMS 2 slot 1) charm parts
|
|
212
212
|
- STL manipulation: scale, rotate, extend base, merge vertices, center at origin, lay flat, and inspect model info
|
|
213
|
-
- Slice STL or 3MF files using an external CLI. BambuStudio, FULU, and Orca require the exact machine preset and resolve BBL dependencies before slicing; see [FULU/Orca CLI setup and validation limits](https://github.com/DMontgomery40/bambu-printer-mcp/blob/main/docs/FULU.md#fulu-and-orca-cli-safety-limit).
|
|
213
|
+
- Slice STL or 3MF files using an external CLI, or use the optional [`orca-slicer-api` backend](https://github.com/DMontgomery40/bambu-printer-mcp/blob/main/docs/SLICING.md) in a separate container without a local slicer executable. Remote slicing requires a matching read-only Orca profile tree and currently supports one filament slot and one selected plate. BambuStudio, FULU, and Orca require the exact machine preset and resolve BBL dependencies before slicing; see [FULU/Orca CLI setup and validation limits](https://github.com/DMontgomery40/bambu-printer-mcp/blob/main/docs/FULU.md#fulu-and-orca-cli-safety-limit).
|
|
214
214
|
- Inspect slicer settings from a saved 3MF template or extracted profile via `get_slice_settings`
|
|
215
215
|
- Enumerate saved slicing templates from the local registry via `list_templates`
|
|
216
216
|
- Save templates into the local registry via `save_template`
|
|
@@ -279,6 +279,8 @@ If you are using the direct-feed spool holder (no AMS attached) or want to bypas
|
|
|
279
279
|
|
|
280
280
|
For H2 projects with declared filaments, also provide the required mapping; `use_ams: false` alone does not remove the firmware's mapping requirement. See [`print_3mf`](#print_3mf).
|
|
281
281
|
|
|
282
|
+
Dual-nozzle printers (H2D, H2D Pro, H2C, X2D) have two external spool holders, and the printer reports one virtual slot per nozzle. Use Bambu Studio's ids in `ams_slots`/`ams_mapping`: `254` is the left/deputy nozzle's external spool and `255` the right/main nozzle's. The server composes the selected plate's `filament_map` with `physical_extruder_map` to verify each filament's physical nozzle (`0` = right, `1` = left), and requires the matching fresh `vir_slot` report. An explicit external-spool mapping is preserved with `use_ams: false`; a missing mapping is derived only from these inspected headers. Single-nozzle printers keep `254` as their only external tray.
|
|
283
|
+
|
|
282
284
|
### Auto-match AMS by RFID
|
|
283
285
|
|
|
284
286
|
For pre-sliced 3MFs that declare filament types, the `auto_match_ams` flag on `print_3mf` (or the standalone `resolve_3mf_ams_slots` dry-run tool) automatically resolves the required filaments against your live AMS inventory. The matcher works as follows:
|
|
@@ -948,7 +950,7 @@ Install Bambu Studio and its networking plug-in, plus the Apple command-line dev
|
|
|
948
950
|
|
|
949
951
|
The npm package and desktop bundle include `native/bambu-native-print.cpp`, `scripts/build-bambu-native.sh` and the Linux setup script, never a developer's compiled binary. The server finds the built helper relative to its installed package, regardless of the launch directory. A trusted server setting `BAMBU_NATIVE_HELPER` can select an alternate executable.
|
|
950
952
|
|
|
951
|
-
For X2D, `print_3mf` defaults to `connection_mode: "bambu_native"`; legacy `lan_mqtt_ftps` requests are redirected to it. Supply `ams_slots`, a complete project-level `ams_mapping`, or `auto_match_ams: true`.
|
|
953
|
+
For X2D, `print_3mf` defaults to `connection_mode: "bambu_native"`; legacy `lan_mqtt_ftps` requests are redirected to it. Supply `ams_slots`, a complete project-level `ams_mapping`, or `auto_match_ams: true`. A job that uses only one external spool requires explicit `use_ams: false`; a job mixing AMS trays with the external spools maps them as `254` (left/deputy nozzle) and `255` (right/main nozzle). Raw `ams_mapping2` and nozzle/extra-option overrides are rejected before dispatch; the server derives both AMS representations from the checked structured mapping. Model/nozzle/material inspection, fresh printer-state checks, and human preflight apply before the helper receives a private snapshot. The native helper requests fresh shared printer-state authorization immediately before each print submission, including a certificate retry, and before checked heating, resume, or error-clearing commands; custom helpers must support this handshake. Stop and heater-off remain available without preflight. Native upload-only requests use the same all-plate inspection as FTPS uploads. They accept `project_name`, `preset_name`, `bed_type`, and an existing `plate_index`; AMS, nozzle, and calibration print options are rejected. Use `print_3mf` for checked print settings.
|
|
952
954
|
|
|
953
955
|
The helper waits for connection/certificate exchange and retries only the initial `-4030` send once. Rebuild the helper after upgrading to receive the teardown fix. A successful dispatch receipt ends the helper before plug-in cleanup can turn an accepted job into an error. If an older/custom helper crashes after dispatch, the server reports uncertainty: check the printer state and job name before retrying, because resending can duplicate a print. Previous hardware testing reached `RUNNING` on X2D; this revision is validated with mocked transport regressions and clean installs, not a new physical print. `bambu_connect` is an optional macOS handoff for user review in Bambu Connect and does not start a print. Native task, heater, and error controls retain the common safety checks; raw controls cannot bypass them. Request cancellation, stop, and heater-off interrupt pending native helpers; the server waits for process exit before releasing a checked file. A command already sent to the printer cannot be recalled by cancelling the request, so verify printer state before retrying.
|
|
954
956
|
|
package/dist/ams-mapping.d.ts
CHANGED
|
@@ -1,3 +1,19 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
/** Tray-value options. `dualNozzle` admits 255, the right/main nozzle external spool that
|
|
2
|
+
* dual-nozzle printers report beside the left/deputy external spool 254 (Bambu Studio
|
|
3
|
+
* VIRTUAL_TRAY_MAIN_ID / VIRTUAL_TRAY_DEPUTY_ID). Single-nozzle printers keep 254 only. */
|
|
4
|
+
export interface AmsTrayValueOptions {
|
|
5
|
+
dualNozzle?: boolean;
|
|
6
|
+
}
|
|
7
|
+
export declare function describeAmsTrayRange(options?: AmsTrayValueOptions): string;
|
|
8
|
+
export declare function normalizeBridgeAmsTrayValue(value: unknown, label: string, options?: AmsTrayValueOptions): number;
|
|
9
|
+
export declare function normalizeAmsMappingObject(mapping: Record<string, unknown>, label?: string, options?: AmsTrayValueOptions): number[];
|
|
3
10
|
export declare function hasAmsMappingInput(value: unknown): boolean;
|
|
11
|
+
/** Flat wire entries use -1 for dual-nozzle virtual trays; ams_mapping2 identifies their spool. */
|
|
12
|
+
export declare function bridgeAmsMappingValue(value: number, dualNozzle?: boolean): number;
|
|
13
|
+
/** Select an external spool only from a verified physical nozzle on dual-nozzle printers. */
|
|
14
|
+
export declare function externalTrayForNozzle(nozzle: number | undefined, dualNozzle?: boolean): number;
|
|
15
|
+
/** Virtual trays use slot zero on dual-nozzle printers; retain the historical single-nozzle form. */
|
|
16
|
+
export declare function bridgeAmsMapping2Entry(value: number, dualNozzle?: boolean): {
|
|
17
|
+
ams_id: number;
|
|
18
|
+
slot_id: number;
|
|
19
|
+
};
|
package/dist/ams-mapping.js
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
export function
|
|
1
|
+
export function describeAmsTrayRange(options = {}) {
|
|
2
|
+
return options.dualNozzle
|
|
3
|
+
? "[-1, 15] (absolute tray), 128-253 (AMS-HT), 254 (left/deputy external) or 255 (right/main external)"
|
|
4
|
+
: "[-1, 15] (absolute tray) or 128-254 (HT/external)";
|
|
5
|
+
}
|
|
6
|
+
export function normalizeBridgeAmsTrayValue(value, label, options = {}) {
|
|
2
7
|
let numeric;
|
|
3
8
|
if (typeof value === "number") {
|
|
4
9
|
numeric = value;
|
|
@@ -12,8 +17,8 @@ export function normalizeBridgeAmsTrayValue(value, label) {
|
|
|
12
17
|
if (!Number.isInteger(numeric) ||
|
|
13
18
|
numeric < -1 ||
|
|
14
19
|
(numeric > 15 && numeric < 128) ||
|
|
15
|
-
numeric > 254) {
|
|
16
|
-
throw new Error(`${label} must be an integer in
|
|
20
|
+
numeric > (options.dualNozzle ? 255 : 254)) {
|
|
21
|
+
throw new Error(`${label} must be an integer in ${describeAmsTrayRange(options)}; got ${JSON.stringify(value)}`);
|
|
17
22
|
}
|
|
18
23
|
return numeric;
|
|
19
24
|
}
|
|
@@ -21,7 +26,7 @@ function isNumericMappingKey(key) {
|
|
|
21
26
|
const numeric = Number(key);
|
|
22
27
|
return Number.isInteger(numeric) && numeric >= 0;
|
|
23
28
|
}
|
|
24
|
-
export function normalizeAmsMappingObject(mapping, label = "ams_mapping") {
|
|
29
|
+
export function normalizeAmsMappingObject(mapping, label = "ams_mapping", options = {}) {
|
|
25
30
|
return Object.entries(mapping)
|
|
26
31
|
.sort(([leftKey], [rightKey]) => {
|
|
27
32
|
const leftNumeric = isNumericMappingKey(leftKey);
|
|
@@ -34,7 +39,7 @@ export function normalizeAmsMappingObject(mapping, label = "ams_mapping") {
|
|
|
34
39
|
return 1;
|
|
35
40
|
return 0;
|
|
36
41
|
})
|
|
37
|
-
.map(([key, value]) => normalizeBridgeAmsTrayValue(value, `${label}[${key}]
|
|
42
|
+
.map(([key, value]) => normalizeBridgeAmsTrayValue(value, `${label}[${key}]`, options));
|
|
38
43
|
}
|
|
39
44
|
export function hasAmsMappingInput(value) {
|
|
40
45
|
if (Array.isArray(value))
|
|
@@ -43,3 +48,29 @@ export function hasAmsMappingInput(value) {
|
|
|
43
48
|
return Object.keys(value).length > 0;
|
|
44
49
|
return value !== undefined && value !== null;
|
|
45
50
|
}
|
|
51
|
+
/** Flat wire entries use -1 for dual-nozzle virtual trays; ams_mapping2 identifies their spool. */
|
|
52
|
+
export function bridgeAmsMappingValue(value, dualNozzle = false) {
|
|
53
|
+
return dualNozzle && (value === 254 || value === 255) ? -1 : value;
|
|
54
|
+
}
|
|
55
|
+
/** Select an external spool only from a verified physical nozzle on dual-nozzle printers. */
|
|
56
|
+
export function externalTrayForNozzle(nozzle, dualNozzle = false) {
|
|
57
|
+
if (!dualNozzle)
|
|
58
|
+
return 254;
|
|
59
|
+
if (nozzle === 0)
|
|
60
|
+
return 255;
|
|
61
|
+
if (nozzle === 1)
|
|
62
|
+
return 254;
|
|
63
|
+
throw new Error("External-spool printing requires inspected filament_map and physical_extruder_map nozzle bindings.");
|
|
64
|
+
}
|
|
65
|
+
/** Virtual trays use slot zero on dual-nozzle printers; retain the historical single-nozzle form. */
|
|
66
|
+
export function bridgeAmsMapping2Entry(value, dualNozzle = false) {
|
|
67
|
+
if (value < 0)
|
|
68
|
+
return { ams_id: 255, slot_id: 255 };
|
|
69
|
+
if (value === 255)
|
|
70
|
+
return dualNozzle ? { ams_id: 255, slot_id: 0 } : { ams_id: 255, slot_id: 255 };
|
|
71
|
+
if (value === 254)
|
|
72
|
+
return { ams_id: 254, slot_id: dualNozzle ? 0 : 254 };
|
|
73
|
+
if (value >= 128)
|
|
74
|
+
return { ams_id: 128, slot_id: value - 128 };
|
|
75
|
+
return { ams_id: Math.floor(value / 4), slot_id: value % 4 };
|
|
76
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -14,11 +14,12 @@ import { BambuNetworkBridge } from "./bambu-network-bridge.js";
|
|
|
14
14
|
import { assertBambuNativeAvailable, bambuNativeControlsAvailable, bambuNativeUnavailableReason, interruptNativeOperations, validateBambuNativeMetadata, buildBambuNativeFanCommand, buildBambuNativeTemperatureCommand, printWithBambuNative, sendCommandWithBambuNative, uploadWithBambuNative, } from "./bambu-native.js";
|
|
15
15
|
import { importFileViaBambuConnect } from "./bambu-connect.js";
|
|
16
16
|
import { BlenderMcpBridge } from "./blender-mcp-bridge.js";
|
|
17
|
-
import { hasAmsMappingInput, normalizeAmsMappingObject, normalizeBridgeAmsTrayValue } from "./ams-mapping.js";
|
|
17
|
+
import { bridgeAmsMapping2Entry, bridgeAmsMappingValue, externalTrayForNozzle, hasAmsMappingInput, normalizeAmsMappingObject, normalizeBridgeAmsTrayValue } from "./ams-mapping.js";
|
|
18
18
|
import { analyze3MFAmsRequirements, analyze3MFPlateObjects, analyzeCollarCharm3MF, extractBambuTemplateSettings, getCollarCharmRolePolicy, parse3MF } from './3mf_parser.js';
|
|
19
19
|
import { BambuImplementation, assertDirectPrintSupported } from "./printers/bambu.js";
|
|
20
20
|
import { inspectPrintFile } from "./safety/print-file.js";
|
|
21
21
|
import { validatePrinterState } from "./safety/printer-state.js";
|
|
22
|
+
import { isDualNozzleModel } from "./safety/limits.js";
|
|
22
23
|
import { withPrinterOperation, withPrintSnapshot, uniquePrintName } from "./safety/artifact.js";
|
|
23
24
|
import { detectProfilesRoot } from "./slicer/profile-flatten.js";
|
|
24
25
|
dotenv.config();
|
|
@@ -523,7 +524,7 @@ function defaultSlicerPathFor(slicerType) {
|
|
|
523
524
|
}
|
|
524
525
|
function resolveSlicerConfig(args) {
|
|
525
526
|
const slicerType = normalizeSlicerType(String(args?.slicer_type || DEFAULT_SLICER_TYPE));
|
|
526
|
-
const slicerPath = String(args?.slicer_path || process.env.SLICER_PATH || defaultSlicerPathFor(slicerType));
|
|
527
|
+
const slicerPath = slicerType === "orca-slicer-api" ? "" : String(args?.slicer_path || process.env.SLICER_PATH || defaultSlicerPathFor(slicerType));
|
|
527
528
|
const slicerProfile = String(args?.slicer_profile || DEFAULT_SLICER_PROFILE);
|
|
528
529
|
return { slicerType, slicerPath, slicerProfile };
|
|
529
530
|
}
|
|
@@ -735,18 +736,10 @@ function stringifyBridgeJson(value) {
|
|
|
735
736
|
return value;
|
|
736
737
|
return JSON.stringify(value);
|
|
737
738
|
}
|
|
738
|
-
function nativeAmsMapping2(mapping) {
|
|
739
|
+
function nativeAmsMapping2(mapping, dualNozzle = false) {
|
|
739
740
|
if (!mapping || mapping.length === 0)
|
|
740
741
|
return undefined;
|
|
741
|
-
return JSON.stringify(mapping.map((value) =>
|
|
742
|
-
if (value < 0 || value === 255)
|
|
743
|
-
return { ams_id: 255, slot_id: 255 };
|
|
744
|
-
if (value === 254)
|
|
745
|
-
return { ams_id: 254, slot_id: 254 };
|
|
746
|
-
if (value >= 128)
|
|
747
|
-
return { ams_id: 128, slot_id: value - 128 };
|
|
748
|
-
return { ams_id: Math.floor(value / 4), slot_id: value % 4 };
|
|
749
|
-
}));
|
|
742
|
+
return JSON.stringify(mapping.map((value) => bridgeAmsMapping2Entry(value, dualNozzle)));
|
|
750
743
|
}
|
|
751
744
|
function redactPrintParams(params) {
|
|
752
745
|
return {
|
|
@@ -1025,6 +1018,9 @@ class BambuPrinterMCPServer {
|
|
|
1025
1018
|
resolveSlicerConfigFromArgs(args, toolName) {
|
|
1026
1019
|
const config = resolveSlicerConfig(args);
|
|
1027
1020
|
if (args?.slicer_path !== undefined) {
|
|
1021
|
+
if (config.slicerType === "orca-slicer-api") {
|
|
1022
|
+
throw new Error(`${toolName}: remote slicing uses trusted SLICER_API_URL and BAMBU_PROFILES_ROOT configuration; slicer_path is unsupported.`);
|
|
1023
|
+
}
|
|
1028
1024
|
const resolvedSlicerPath = this.resolveExecutableSelectorArg(args.slicer_path, toolName, "slicer_path");
|
|
1029
1025
|
if (resolvedSlicerPath !== undefined) {
|
|
1030
1026
|
config.slicerPath = resolvedSlicerPath;
|
|
@@ -1048,10 +1044,10 @@ class BambuPrinterMCPServer {
|
|
|
1048
1044
|
}
|
|
1049
1045
|
console.error(`3MF has no gcode - auto-slicing with ${slicerType} for ${printModel}`);
|
|
1050
1046
|
const autoSliceOptions = {
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1047
|
+
...(slicerType === "orca-slicer-api" ? {} : {
|
|
1048
|
+
uptodate: true, ensureOnBed: true, minSave: true, skipModifiedGcodes: true,
|
|
1049
|
+
}),
|
|
1050
|
+
...(slicerType === "orca-slicer-api" ? { slicePlate: Number(args.plate_index ?? 0) + 1 } : {}),
|
|
1055
1051
|
bedType: bedType ? resolveBambuStudioCliBedType(bedType) : undefined,
|
|
1056
1052
|
nozzleType: resolveNozzleType(args?.nozzle_type),
|
|
1057
1053
|
nozzleVolumeType: resolveNozzleVolumeType(args?.nozzle_volume_type),
|
|
@@ -1063,8 +1059,9 @@ class BambuPrinterMCPServer {
|
|
|
1063
1059
|
async resolveAmsPrintSettings(threeMFPath, args, host, bambuSerial, bambuToken, printModel, printNozzle) {
|
|
1064
1060
|
const parsed3MFData = await parse3MF(threeMFPath);
|
|
1065
1061
|
let parsedAmsMapping;
|
|
1062
|
+
const trayOptions = { dualNozzle: isDualNozzleModel(printModel) };
|
|
1066
1063
|
if (parsed3MFData.slicerConfig?.ams_mapping) {
|
|
1067
|
-
const slots = normalizeAmsMappingObject(parsed3MFData.slicerConfig.ams_mapping);
|
|
1064
|
+
const slots = normalizeAmsMappingObject(parsed3MFData.slicerConfig.ams_mapping, "ams_mapping", trayOptions);
|
|
1068
1065
|
if (slots.length > 0) {
|
|
1069
1066
|
parsedAmsMapping = slots;
|
|
1070
1067
|
}
|
|
@@ -1077,10 +1074,10 @@ class BambuPrinterMCPServer {
|
|
|
1077
1074
|
if (hasUserAmsMapping) {
|
|
1078
1075
|
let userMappingOverride;
|
|
1079
1076
|
if (Array.isArray(args.ams_mapping)) {
|
|
1080
|
-
userMappingOverride = args.ams_mapping.map((v, i) => normalizeBridgeAmsTrayValue(v, `ams_mapping[${i}]
|
|
1077
|
+
userMappingOverride = args.ams_mapping.map((v, i) => normalizeBridgeAmsTrayValue(v, `ams_mapping[${i}]`, trayOptions));
|
|
1081
1078
|
}
|
|
1082
1079
|
else if (args.ams_mapping && typeof args.ams_mapping === 'object') {
|
|
1083
|
-
userMappingOverride = normalizeAmsMappingObject(args.ams_mapping);
|
|
1080
|
+
userMappingOverride = normalizeAmsMappingObject(args.ams_mapping, "ams_mapping", trayOptions);
|
|
1084
1081
|
}
|
|
1085
1082
|
if (userMappingOverride && userMappingOverride.length > 0) {
|
|
1086
1083
|
finalAmsMapping = userMappingOverride;
|
|
@@ -1089,7 +1086,7 @@ class BambuPrinterMCPServer {
|
|
|
1089
1086
|
}
|
|
1090
1087
|
}
|
|
1091
1088
|
if (!hasUserAmsMapping && hasUserAmsSlots) {
|
|
1092
|
-
const userSlots = args.ams_slots.map((slot, i) => normalizeBridgeAmsTrayValue(slot, `ams_slots[${i}]
|
|
1089
|
+
const userSlots = args.ams_slots.map((slot, i) => normalizeBridgeAmsTrayValue(slot, `ams_slots[${i}]`, trayOptions));
|
|
1093
1090
|
finalAmsSlots = userSlots;
|
|
1094
1091
|
finalAmsMapping = undefined;
|
|
1095
1092
|
useAMS = userSlots.length > 0;
|
|
@@ -1106,8 +1103,10 @@ class BambuPrinterMCPServer {
|
|
|
1106
1103
|
useAMS = true;
|
|
1107
1104
|
}
|
|
1108
1105
|
if (args?.use_ams === false) {
|
|
1109
|
-
|
|
1110
|
-
|
|
1106
|
+
if (!hasUserAmsMapping)
|
|
1107
|
+
finalAmsMapping = undefined;
|
|
1108
|
+
if (!hasUserAmsSlots)
|
|
1109
|
+
finalAmsSlots = undefined;
|
|
1111
1110
|
useAMS = false;
|
|
1112
1111
|
}
|
|
1113
1112
|
if ((!finalAmsMapping || finalAmsMapping.length === 0) && (!finalAmsSlots || finalAmsSlots.length === 0)) {
|
|
@@ -1177,8 +1176,16 @@ class BambuPrinterMCPServer {
|
|
|
1177
1176
|
inspection.usedFilamentPositions.forEach((position, index) => { mapping[position] = finalAmsSlots[index]; });
|
|
1178
1177
|
}
|
|
1179
1178
|
if (!useAMS) {
|
|
1180
|
-
mapping = Array(inspection.materials.length).fill(-1);
|
|
1181
|
-
|
|
1179
|
+
mapping ?? (mapping = Array(inspection.materials.length).fill(-1));
|
|
1180
|
+
const dualNozzle = isDualNozzleModel(printModel);
|
|
1181
|
+
for (const position of inspection.usedFilamentPositions) {
|
|
1182
|
+
if (mapping[position] === undefined || mapping[position] < 0) {
|
|
1183
|
+
mapping[position] = externalTrayForNozzle(inspection.filamentNozzleIndices?.[position], dualNozzle);
|
|
1184
|
+
}
|
|
1185
|
+
if (mapping[position] !== 254 && !(dualNozzle && mapping[position] === 255)) {
|
|
1186
|
+
throw new Error("use_ams:false requires an external-spool mapping for every used filament.");
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1182
1189
|
}
|
|
1183
1190
|
const requirements = { ...inspection, amsMapping: mapping, useAMS };
|
|
1184
1191
|
const initialStatus = await this.bambu.getSafetyStatus(devIp, devId, password || bambuToken);
|
|
@@ -1187,7 +1194,7 @@ class BambuPrinterMCPServer {
|
|
|
1187
1194
|
const confirmedStatus = await this.bambu.getSafetyStatus(devIp, devId, password || bambuToken);
|
|
1188
1195
|
validatePrinterState(confirmedStatus, requirements);
|
|
1189
1196
|
this.bambu.assertBedClearance(confirmedStatus, bedClearance);
|
|
1190
|
-
const amsMapping = stringifyBridgeJson(mapping);
|
|
1197
|
+
const amsMapping = stringifyBridgeJson(mapping?.map(value => bridgeAmsMappingValue(value, isDualNozzleModel(printModel))));
|
|
1191
1198
|
const remoteFileName = uniquePrintName(threeMFPath);
|
|
1192
1199
|
const params = {
|
|
1193
1200
|
dev_id: devId,
|
|
@@ -1204,7 +1211,7 @@ class BambuPrinterMCPServer {
|
|
|
1204
1211
|
ftp_file_md5: String(args?.ftp_file_md5 || ""),
|
|
1205
1212
|
nozzle_mapping: stringifyBridgeJson(args?.nozzle_mapping) || "",
|
|
1206
1213
|
ams_mapping: amsMapping || "",
|
|
1207
|
-
ams_mapping2:
|
|
1214
|
+
ams_mapping2: nativeAmsMapping2(mapping, isDualNozzleModel(printModel)) || "",
|
|
1208
1215
|
ams_mapping_info: stringifyBridgeJson(args?.ams_mapping_info) || "",
|
|
1209
1216
|
nozzles_info: stringifyBridgeJson(args?.nozzles_info) || "",
|
|
1210
1217
|
connection_type: connectionType,
|
|
@@ -1244,7 +1251,7 @@ class BambuPrinterMCPServer {
|
|
|
1244
1251
|
host: devIp, serial: devId, token: password || bambuToken, filePath: snapshot,
|
|
1245
1252
|
projectName: String(params.task_name), presetName, remoteName: remoteFileName,
|
|
1246
1253
|
plateIndex, bedType: printBedType, useAMS,
|
|
1247
|
-
amsMapping, amsMapping2: nativeAmsMapping2(mapping),
|
|
1254
|
+
amsMapping, amsMapping2: nativeAmsMapping2(mapping, isDualNozzleModel(printModel)),
|
|
1248
1255
|
bedLeveling: params.task_bed_leveling,
|
|
1249
1256
|
flowCalibration: params.task_flow_cali,
|
|
1250
1257
|
vibrationCalibration: params.task_vibration_cali,
|
|
@@ -1447,10 +1454,9 @@ class BambuPrinterMCPServer {
|
|
|
1447
1454
|
throw new Error('BambuStudio CLI SuperTack bed type is not verified; use a pre-sliced 3MF for SuperTack or choose textured_plate, cool_plate, engineering_plate, or hot_plate.');
|
|
1448
1455
|
}
|
|
1449
1456
|
const autoSliceOptions = {
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
skipModifiedGcodes: true,
|
|
1457
|
+
...(slicerType === "orca-slicer-api" ? {} : {
|
|
1458
|
+
uptodate: true, ensureOnBed: true, minSave: true, skipModifiedGcodes: true,
|
|
1459
|
+
}),
|
|
1454
1460
|
bedType,
|
|
1455
1461
|
};
|
|
1456
1462
|
try {
|
|
@@ -1943,7 +1949,7 @@ class BambuPrinterMCPServer {
|
|
|
1943
1949
|
rotate_y: { type: "number", description: "Rotate the model around the Y-axis by this many degrees before slicing. Useful for reorienting prints for better layer adhesion." },
|
|
1944
1950
|
min_save: { type: "boolean", description: "Write a smaller output 3MF by omitting non-essential metadata. Reduces file size for faster FTP upload to the printer." },
|
|
1945
1951
|
skip_modified_gcodes: { type: "boolean", description: "Strip custom start/end gcodes embedded in the 3MF. Recommended for downloaded 3MFs since custom gcodes from other users' profiles may be unsafe for your printer." },
|
|
1946
|
-
slice_plate: { type: "number", description: "Which plate index to slice. 0 = all plates
|
|
1952
|
+
slice_plate: { type: "number", description: "Which plate index to slice. 0 = all plates for local slicers; remote slicing defaults to plate 1 and rejects 0. Use 1, 2, etc. to slice only a specific plate in multi-plate 3MF projects." }
|
|
1947
1953
|
},
|
|
1948
1954
|
required: ["stl_path", "bambu_model"]
|
|
1949
1955
|
}
|
|
@@ -2046,8 +2052,8 @@ class BambuPrinterMCPServer {
|
|
|
2046
2052
|
nozzle_diameter: { type: "string", description: "Nozzle diameter in mm for auto-slicing (default: 0.4)." },
|
|
2047
2053
|
nozzle_diameters: { type: "array", minItems: 1, maxItems: 2, items: { type: "number", enum: [0.2, 0.4, 0.6, 0.8] }, description: "Complete per-nozzle diameters for pre-sliced jobs, e.g. [0.4, 0.6]. Omit to verify file metadata against every reported nozzle; cannot combine with nozzle_diameter." },
|
|
2048
2054
|
use_ams: { type: "boolean", description: "Whether to use the AMS; defaults to auto-detect from the 3MF mapping." },
|
|
2049
|
-
ams_mapping: { type: "array", description: "AMS slot mapping array used by both local MCP printing and FULU PrintParams.", items: { type: "number" } },
|
|
2050
|
-
ams_slots: { type: "array", description: "Per-used-filament AMS slot list, matching the local LAN print path.", items: { type: "number" } },
|
|
2055
|
+
ams_mapping: { type: "array", description: "AMS slot mapping array used by both local MCP printing and FULU PrintParams. Same tray values as print_3mf: 254 = external spool; on dual-nozzle printers 254 = left/deputy nozzle external and 255 = right/main nozzle external.", items: { type: "number" } },
|
|
2056
|
+
ams_slots: { type: "array", description: "Per-used-filament AMS slot list, matching the local LAN print path (254/255 external spool rules as in print_3mf).", items: { type: "number" } },
|
|
2051
2057
|
ams_mapping_bridge: { type: "string", description: "Raw JSON string override for FULU PrintParams ams_mapping when the automatic array is not enough." },
|
|
2052
2058
|
ams_mapping2: { type: "string", description: "Raw JSON string for FULU PrintParams ams_mapping2, matching OrcaSlicer-bambulab's v1 AMS mapping field." },
|
|
2053
2059
|
ams_mapping_info: { type: "string", description: "Raw JSON string for FULU PrintParams ams_mapping_info, matching OrcaSlicer-bambulab's detailed AMS mapping field." },
|
|
@@ -2475,12 +2481,12 @@ class BambuPrinterMCPServer {
|
|
|
2475
2481
|
use_ams: { type: "boolean", description: "Whether to use the AMS (default: auto-detect from 3MF)" },
|
|
2476
2482
|
ams_mapping: {
|
|
2477
2483
|
type: "array",
|
|
2478
|
-
description: "Project-level AMS mapping array. Position = project filament index, value = absolute AMS tray (0-3=AMS 0, 4-7=AMS 1, 8-11=AMS 2, 128+=AMS-HT, 254=external, -1=unused). Prefer ams_slots unless you know the project-level layout.",
|
|
2484
|
+
description: "Project-level AMS mapping array. Position = project filament index, value = absolute AMS tray (0-3=AMS 0, 4-7=AMS 1, 8-11=AMS 2, 128+=AMS-HT, 254=external, -1=unused). On dual-nozzle printers (H2D, H2D Pro, H2C, X2D) 254 is the LEFT/deputy nozzle external spool and 255 the RIGHT/main nozzle external spool, matching Bambu Studio's virtual tray ids; the gcode header's filament_map (1=left, 2=right) says which nozzle each filament was sliced for. Prefer ams_slots unless you know the project-level layout.",
|
|
2479
2485
|
items: { type: "number" }
|
|
2480
2486
|
},
|
|
2481
2487
|
ams_slots: {
|
|
2482
2488
|
type: "array",
|
|
2483
|
-
description: "Preferred AMS input: one absolute tray index per USED filament in plate order, e.g. [1] for a single-filament print pulling from AMS 0 slot 1. Expanded to project-level ams_mapping automatically from the 3MF's plate_N.json and gcode header.",
|
|
2489
|
+
description: "Preferred AMS input: one absolute tray index per USED filament in plate order, e.g. [1] for a single-filament print pulling from AMS 0 slot 1. Use 254 for the external spool (on dual-nozzle printers: 254 = left/deputy nozzle external, 255 = right/main nozzle external). Expanded to project-level ams_mapping automatically from the 3MF's plate_N.json and gcode header.",
|
|
2484
2490
|
items: { type: "number" }
|
|
2485
2491
|
},
|
|
2486
2492
|
auto_match_ams: {
|
|
@@ -3415,10 +3421,9 @@ class BambuPrinterMCPServer {
|
|
|
3415
3421
|
}
|
|
3416
3422
|
console.error(`3MF has no gcode — auto-slicing with ${slicerType} for ${printModel}`);
|
|
3417
3423
|
const autoSliceOptions = {
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
skipModifiedGcodes: true,
|
|
3424
|
+
...(slicerType === "orca-slicer-api" ? { slicePlate: plateIndex + 1 } : {
|
|
3425
|
+
uptodate: true, ensureOnBed: true, minSave: true, skipModifiedGcodes: true,
|
|
3426
|
+
}),
|
|
3422
3427
|
bedType: printBedType,
|
|
3423
3428
|
nozzleType: resolveNozzleType(args?.nozzle_type),
|
|
3424
3429
|
nozzleVolumeType: resolveNozzleVolumeType(args?.nozzle_volume_type),
|
|
@@ -3440,9 +3445,10 @@ class BambuPrinterMCPServer {
|
|
|
3440
3445
|
}
|
|
3441
3446
|
const parsed3MFData = await parse3MF(threeMFPath);
|
|
3442
3447
|
const isH2Print = H2_BAMBU_MODELS.has(printModel);
|
|
3448
|
+
const trayOptions = { dualNozzle: isDualNozzleModel(printModel) };
|
|
3443
3449
|
let parsedAmsMapping;
|
|
3444
3450
|
if (!isH2Print && parsed3MFData.slicerConfig?.ams_mapping) {
|
|
3445
|
-
const slots = normalizeAmsMappingObject(parsed3MFData.slicerConfig.ams_mapping);
|
|
3451
|
+
const slots = normalizeAmsMappingObject(parsed3MFData.slicerConfig.ams_mapping, "ams_mapping", trayOptions);
|
|
3446
3452
|
if (slots.length > 0) {
|
|
3447
3453
|
parsedAmsMapping = slots;
|
|
3448
3454
|
}
|
|
@@ -3455,10 +3461,10 @@ class BambuPrinterMCPServer {
|
|
|
3455
3461
|
if (hasUserAmsMapping) {
|
|
3456
3462
|
let userMappingOverride;
|
|
3457
3463
|
if (Array.isArray(args.ams_mapping)) {
|
|
3458
|
-
userMappingOverride = args.ams_mapping.map((v, i) => normalizeBridgeAmsTrayValue(v, `ams_mapping[${i}]
|
|
3464
|
+
userMappingOverride = args.ams_mapping.map((v, i) => normalizeBridgeAmsTrayValue(v, `ams_mapping[${i}]`, trayOptions));
|
|
3459
3465
|
}
|
|
3460
3466
|
else if (args.ams_mapping && typeof args.ams_mapping === 'object') {
|
|
3461
|
-
userMappingOverride = normalizeAmsMappingObject(args.ams_mapping);
|
|
3467
|
+
userMappingOverride = normalizeAmsMappingObject(args.ams_mapping, "ams_mapping", trayOptions);
|
|
3462
3468
|
}
|
|
3463
3469
|
if (userMappingOverride && userMappingOverride.length > 0) {
|
|
3464
3470
|
finalAmsMapping = userMappingOverride;
|
|
@@ -3499,8 +3505,10 @@ class BambuPrinterMCPServer {
|
|
|
3499
3505
|
}
|
|
3500
3506
|
}
|
|
3501
3507
|
if (args?.use_ams === false) {
|
|
3502
|
-
|
|
3503
|
-
|
|
3508
|
+
if (!hasUserAmsMapping)
|
|
3509
|
+
finalAmsMapping = undefined;
|
|
3510
|
+
if (!hasUserAmsSlots)
|
|
3511
|
+
finalAmsSlots = undefined;
|
|
3504
3512
|
useAMS = false;
|
|
3505
3513
|
}
|
|
3506
3514
|
if ((!finalAmsMapping || finalAmsMapping.length === 0) && (!finalAmsSlots || finalAmsSlots.length === 0)) {
|
package/dist/printers/bambu.js
CHANGED
|
@@ -7,7 +7,8 @@ import os from "node:os";
|
|
|
7
7
|
import { Readable } from "node:stream";
|
|
8
8
|
import { readSafe3mfArchive } from "../safety/archive.js";
|
|
9
9
|
import { inspectPrintFile } from "../safety/print-file.js";
|
|
10
|
-
import { normalizeModel, validateTemperature } from "../safety/limits.js";
|
|
10
|
+
import { isDualNozzleModel, normalizeModel, validateTemperature } from "../safety/limits.js";
|
|
11
|
+
import { bridgeAmsMapping2Entry, bridgeAmsMappingValue, externalTrayForNozzle, describeAmsTrayRange } from "../ams-mapping.js";
|
|
11
12
|
import { describeHmsKey, hiddenHmsKeys, hiddenHmsNotices, reportedHmsKey, validatePrinterState, manualHeatingRequirements } from "../safety/printer-state.js";
|
|
12
13
|
import { withPrinterOperation, withPrintSnapshot, uniquePrintName, normalizedRemotePath, cancelPendingPrinterOperations } from "../safety/artifact.js";
|
|
13
14
|
import { Client as FTPClient } from "basic-ftp";
|
|
@@ -620,12 +621,17 @@ export class BambuImplementation {
|
|
|
620
621
|
// `plate_N.json.filament_ids` -- and expand to a full project-level
|
|
621
622
|
// array here. `amsMapping` is the raw escape hatch (takes precedence
|
|
622
623
|
// when both are supplied).
|
|
624
|
+
// Dual-nozzle printers address two external spools: 254 feeds the left/deputy
|
|
625
|
+
// nozzle and 255 the right/main nozzle (Bambu Studio VIRTUAL_TRAY_DEPUTY_ID /
|
|
626
|
+
// VIRTUAL_TRAY_MAIN_ID). Single-nozzle printers keep 254 as the only external tray.
|
|
627
|
+
const dualNozzle = isDualNozzleModel(model);
|
|
628
|
+
const externalTrays = dualNozzle ? [254, 255] : [254];
|
|
623
629
|
const validateTrayValue = (v, label) => {
|
|
624
630
|
if (!Number.isInteger(v) ||
|
|
625
631
|
v < -1 ||
|
|
626
632
|
(v > 15 && v < 128) ||
|
|
627
|
-
v > 254) {
|
|
628
|
-
throw new Error(`${label} values must be integers in
|
|
633
|
+
v > (dualNozzle ? 255 : 254)) {
|
|
634
|
+
throw new Error(`${label} values must be integers in ${describeAmsTrayRange({ dualNozzle })}; got ${v}`);
|
|
629
635
|
}
|
|
630
636
|
};
|
|
631
637
|
let baseMapping;
|
|
@@ -655,7 +661,7 @@ export class BambuImplementation {
|
|
|
655
661
|
const projectLen = Math.max(projectMetadata.projectFilamentCount, ...positions.map((p) => p + 1), 1);
|
|
656
662
|
baseMapping = Array(projectLen).fill(-1);
|
|
657
663
|
positions.forEach((pos, i) => {
|
|
658
|
-
baseMapping[pos] = options.useAMS === false ?
|
|
664
|
+
baseMapping[pos] = options.useAMS === false ? externalTrayForNozzle(inspection.filamentNozzleIndices?.[pos], dualNozzle) : i;
|
|
659
665
|
});
|
|
660
666
|
}
|
|
661
667
|
let amsMapping;
|
|
@@ -663,15 +669,7 @@ export class BambuImplementation {
|
|
|
663
669
|
if (usesH2ProjectFile) {
|
|
664
670
|
const projLen = Math.max(projectMetadata.projectFilamentCount, baseMapping.length, 1);
|
|
665
671
|
amsMapping = Array.from({ length: projLen }, (_, i) => i < baseMapping.length ? baseMapping[i] : -1);
|
|
666
|
-
amsMapping2 = amsMapping.map((v) =>
|
|
667
|
-
if (v < 0 || v === 255)
|
|
668
|
-
return { ams_id: 255, slot_id: 255 };
|
|
669
|
-
if (v === 254)
|
|
670
|
-
return { ams_id: 254, slot_id: 254 };
|
|
671
|
-
if (v >= 128)
|
|
672
|
-
return { ams_id: 128, slot_id: v - 128 };
|
|
673
|
-
return { ams_id: Math.floor(v / 4), slot_id: v % 4 };
|
|
674
|
-
});
|
|
672
|
+
amsMapping2 = amsMapping.map((v) => bridgeAmsMapping2Entry(v, dualNozzle));
|
|
675
673
|
}
|
|
676
674
|
else {
|
|
677
675
|
amsMapping = Array.from({ length: Math.max(5, baseMapping.length, projectMetadata.projectFilamentCount) }, (_, i) => i < baseMapping.length ? baseMapping[i] : -1);
|
|
@@ -681,8 +679,8 @@ export class BambuImplementation {
|
|
|
681
679
|
if (baseMapping[position] === undefined || baseMapping[position] < 0) {
|
|
682
680
|
throw new Error(`Missing physical filament mapping for project filament ${position}.`);
|
|
683
681
|
}
|
|
684
|
-
if (options.useAMS === false && baseMapping[position]
|
|
685
|
-
throw new Error(
|
|
682
|
+
if (options.useAMS === false && !externalTrays.includes(baseMapping[position])) {
|
|
683
|
+
throw new Error(`External-spool printing requires external spool mapping (${externalTrays.join(" or ")}).`);
|
|
686
684
|
}
|
|
687
685
|
}
|
|
688
686
|
const legacyContainer = options.filePath.toLowerCase().endsWith(".gcode.3mf") && !usesH2ProjectFile && !isA1;
|
|
@@ -768,7 +766,7 @@ export class BambuImplementation {
|
|
|
768
766
|
project_id: submissionId,
|
|
769
767
|
subtask_id: submissionId,
|
|
770
768
|
task_id: submissionId,
|
|
771
|
-
ams_mapping: amsMapping,
|
|
769
|
+
ams_mapping: amsMapping.map(value => bridgeAmsMappingValue(value, dualNozzle)),
|
|
772
770
|
ams_mapping2: amsMapping2,
|
|
773
771
|
},
|
|
774
772
|
};
|
package/dist/safety/limits.d.ts
CHANGED
|
@@ -21,6 +21,12 @@ export declare const MACHINE_LIMITS: Readonly<Record<string, {
|
|
|
21
21
|
chamber: number;
|
|
22
22
|
volume: readonly number[];
|
|
23
23
|
}>>;
|
|
24
|
+
/** Printers with two physical nozzles. Bambu Studio numbers the right/main nozzle 0
|
|
25
|
+
* (MAIN_EXTRUDER_ID) and the left/deputy nozzle 1 (DEPUTY_EXTRUDER_ID), and reports one
|
|
26
|
+
* virtual external-spool slot per nozzle: 255 (VIRTUAL_TRAY_MAIN_ID) and 254
|
|
27
|
+
* (VIRTUAL_TRAY_DEPUTY_ID). The single-nozzle H2S is deliberately absent. */
|
|
28
|
+
export declare const DUAL_NOZZLE_MODELS: ReadonlySet<string>;
|
|
29
|
+
export declare function isDualNozzleModel(model: unknown): boolean;
|
|
24
30
|
export declare function normalizeModel(value: unknown): string | undefined;
|
|
25
31
|
export declare function normalizeMaterial(value: unknown): string | undefined;
|
|
26
32
|
export declare function validateTemperature(component: 'nozzle' | 'bed' | 'chamber', value: unknown, model: string, materials?: string[]): number;
|
package/dist/safety/limits.js
CHANGED
|
@@ -30,6 +30,15 @@ export const MACHINE_LIMITS = {
|
|
|
30
30
|
h2s: { nozzle: 350, bed: 120, chamber: 65, volume: [340, 320, 340] },
|
|
31
31
|
x2d: { nozzle: 300, bed: 120, chamber: 65, volume: [256, 256, 260] },
|
|
32
32
|
};
|
|
33
|
+
/** Printers with two physical nozzles. Bambu Studio numbers the right/main nozzle 0
|
|
34
|
+
* (MAIN_EXTRUDER_ID) and the left/deputy nozzle 1 (DEPUTY_EXTRUDER_ID), and reports one
|
|
35
|
+
* virtual external-spool slot per nozzle: 255 (VIRTUAL_TRAY_MAIN_ID) and 254
|
|
36
|
+
* (VIRTUAL_TRAY_DEPUTY_ID). The single-nozzle H2S is deliberately absent. */
|
|
37
|
+
export const DUAL_NOZZLE_MODELS = new Set(['h2d', 'h2dpro', 'h2c', 'x2d']);
|
|
38
|
+
export function isDualNozzleModel(model) {
|
|
39
|
+
const normalized = normalizeModel(model);
|
|
40
|
+
return normalized !== undefined && DUAL_NOZZLE_MODELS.has(normalized);
|
|
41
|
+
}
|
|
33
42
|
export function normalizeModel(value) {
|
|
34
43
|
if (typeof value !== 'string')
|
|
35
44
|
return undefined;
|
|
@@ -12,6 +12,8 @@ export interface PrintFileInspection {
|
|
|
12
12
|
selectsAms: boolean;
|
|
13
13
|
nozzleTypes?: string[];
|
|
14
14
|
nozzleFlows?: string[];
|
|
15
|
+
/** Physical nozzle index for each declared filament, composed from the selected plate headers. */
|
|
16
|
+
filamentNozzleIndices?: number[];
|
|
15
17
|
}
|
|
16
18
|
/** Inspect the exact selected plate. This does not simulate firmware or arbitrary motion:
|
|
17
19
|
* only declared object bounds are checked, never purge/homing/wipe travel coordinates.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { readBoundedPrintFile, loadSafe3mfArchive } from './archive.js';
|
|
3
|
-
import { MACHINE_LIMITS, normalizeMaterial, normalizeModel, validateStartupPurgeTemperature, validateTemperature } from './limits.js';
|
|
3
|
+
import { MACHINE_LIMITS, isDualNozzleModel, normalizeMaterial, normalizeModel, validateStartupPurgeTemperature, validateTemperature } from './limits.js';
|
|
4
4
|
const NUMBER = /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)$/;
|
|
5
5
|
const fail = (message) => { throw new Error(`Print safety: ${message}`); };
|
|
6
6
|
function list(value) {
|
|
@@ -168,6 +168,7 @@ export async function inspectPrintFile(filePath, options) {
|
|
|
168
168
|
const model = normalizeModel(options.model);
|
|
169
169
|
if (!model)
|
|
170
170
|
return fail(`unknown or missing printer model '${options.model}'`);
|
|
171
|
+
const dualNozzle = isDualNozzleModel(model);
|
|
171
172
|
const plateIndex = options.plateIndex ?? 0;
|
|
172
173
|
if (!Number.isInteger(plateIndex) || plateIndex < 0)
|
|
173
174
|
return fail('selected plate index must be a nonnegative integer');
|
|
@@ -308,6 +309,24 @@ export async function inspectPrintFile(filePath, options) {
|
|
|
308
309
|
const values = list(value);
|
|
309
310
|
return values.length && values.every(v => !!normalizeMaterial(v)) ? values.map(v => normalizeMaterial(v)) : undefined;
|
|
310
311
|
}, 'filament material');
|
|
312
|
+
// GCode.cpp uses 1-based filament_map logical extruders and then indexes
|
|
313
|
+
// physical_extruder_map to select the physical nozzle. Only this selected
|
|
314
|
+
// plate's headers establish that binding; unsliced project defaults do not.
|
|
315
|
+
const headerMap = (key, count, low, high) => {
|
|
316
|
+
const values = metadataValues(headers, [key]);
|
|
317
|
+
if (!values.length)
|
|
318
|
+
return undefined;
|
|
319
|
+
return consistent(values, value => {
|
|
320
|
+
const entries = list(value);
|
|
321
|
+
return entries.length === count && entries.every(v => NUMBER.test(v) && Number.isInteger(Number(v)) && Number(v) >= low && Number(v) <= high)
|
|
322
|
+
? entries.map(Number) : undefined;
|
|
323
|
+
}, key.replace(/_/g, ' '));
|
|
324
|
+
};
|
|
325
|
+
const physicalExtruderMap = headerMap('physical_extruder_map', nozzleDiameters.length, 0, dualNozzle ? Math.min(nozzleDiameters.length, 2) - 1 : nozzleDiameters.length - 1);
|
|
326
|
+
if (physicalExtruderMap && new Set(physicalExtruderMap).size !== physicalExtruderMap.length)
|
|
327
|
+
fail('physical extruder map must be a complete permutation');
|
|
328
|
+
const filamentMap = headerMap('filament_map', materials.length, 1, nozzleDiameters.length);
|
|
329
|
+
const filamentNozzleIndices = filamentMap && physicalExtruderMap ? filamentMap.map(logical => physicalExtruderMap[logical - 1]) : undefined;
|
|
311
330
|
const bedValues = metadataValues([...metadata, plate], ['curr_bed_type', 'bed_type']);
|
|
312
331
|
const bedType = bedValues.length ? consistent(bedValues, normalizeBed, 'bed type') : undefined;
|
|
313
332
|
if (options.bedType && (!bedType || normalizeBed(options.bedType) !== bedType))
|
|
@@ -370,7 +389,11 @@ export async function inspectPrintFile(filePath, options) {
|
|
|
370
389
|
let line = stripComments(lines[index]);
|
|
371
390
|
if (!line || line === '%')
|
|
372
391
|
continue;
|
|
373
|
-
//
|
|
392
|
+
// Accept only the exact vendor banners in their documented model/scope.
|
|
393
|
+
// X2D emits its filament-change banner throughout a print; H2C's record
|
|
394
|
+
// separator stays inside its explicitly bounded startup block.
|
|
395
|
+
if (line === '======== X2D filament_change gcode ==========' && model === 'x2d')
|
|
396
|
+
continue;
|
|
374
397
|
if (line === '========== record data ==========' && h2cStartup(index))
|
|
375
398
|
continue;
|
|
376
399
|
line = line.replace(/^N\d+\s*/i, '').replace(/\*\d+\s*$/, '');
|
|
@@ -477,9 +500,16 @@ export async function inspectPrintFile(filePath, options) {
|
|
|
477
500
|
continue;
|
|
478
501
|
}
|
|
479
502
|
const vendorWait = code === 'M109' && ['a1', 'a1mini'].includes(model);
|
|
503
|
+
// Bambu Studio addresses a physical nozzle on dual-nozzle machines with N<index>
|
|
504
|
+
// (X2D `M104 T1 S25 N0` multi-extruder pre-cooling and wipe-tower reheats).
|
|
505
|
+
// N selects a nozzle, never a target, so it is accepted only on those models,
|
|
506
|
+
// only for nozzle heaters, and only within the declared nozzle count.
|
|
507
|
+
const nozzleIndex = ['M104', 'M109'].includes(code) && dualNozzle;
|
|
480
508
|
for (const key of args.keys())
|
|
481
|
-
if (!'SRTA'.includes(key) && !(key === 'H' && vendorWait))
|
|
509
|
+
if (!'SRTA'.includes(key) && !(key === 'H' && vendorWait) && !(key === 'N' && nozzleIndex))
|
|
482
510
|
fail(`unsupported ${code} temperature parameter ${key}`);
|
|
511
|
+
if (args.has('N') && (!Number.isInteger(args.get('N')) || args.get('N') < 0 || args.get('N') >= Math.min(nozzleDiameters.length, 2)))
|
|
512
|
+
fail(`unsupported ${code} nozzle index N`);
|
|
483
513
|
if (args.has('H') && (!Number.isFinite(args.get('H')) || args.get('H') < 0 || args.get('H') > 300))
|
|
484
514
|
fail(`unsupported ${code} wait parameter H`);
|
|
485
515
|
if (!args.has('S') && !args.has('R'))
|
|
@@ -489,13 +519,13 @@ export async function inspectPrintFile(filePath, options) {
|
|
|
489
519
|
// Without a verified per-filament nozzle map, apply all possible materials.
|
|
490
520
|
if (args.has('T') && (!Number.isInteger(args.get('T')) || args.get('T') < 0 || args.get('T') >= nozzleDiameters.length))
|
|
491
521
|
fail('physical heater target has no declared nozzle');
|
|
492
|
-
const position = args.has('T') ? undefined : active;
|
|
522
|
+
const position = args.has('T') || args.has('N') ? undefined : active;
|
|
493
523
|
const commonFlush = code === 'M109' && args.size === 1 && args.get('S') === 290 && model === 'x1e' && !depositionStarted && !commonFlushUsed && boundedX1ePurge(lines, index);
|
|
494
524
|
if (commonFlush)
|
|
495
525
|
commonFlushUsed = true;
|
|
496
526
|
for (const key of ['S', 'R'])
|
|
497
527
|
if (args.has(key))
|
|
498
|
-
heat(component, args.get(key), position, args.has('T') || args.has('A'), commonFlush);
|
|
528
|
+
heat(component, args.get(key), position, args.has('T') || args.has('N') || args.has('A'), commonFlush);
|
|
499
529
|
}
|
|
500
530
|
else if (code === 'M620' || code === 'M621') {
|
|
501
531
|
const args = parameters(argumentsText, model === 'x2d' ? 'MAB' : model === 'h2c' ? 'MAN' : 'MA');
|
|
@@ -656,5 +686,5 @@ export async function inspectPrintFile(filePath, options) {
|
|
|
656
686
|
return fail('selected G-code contains no printable commands');
|
|
657
687
|
if (!used.size)
|
|
658
688
|
materials.forEach((_, index) => used.add(index));
|
|
659
|
-
return { model, nozzleDiameters, materials, usedFilamentPositions: [...used].sort((a, b) => a - b), plateInternalPath, sha256, maxNozzleTemperature, maxBedTemperature, maxChamberTemperature, bedType, selectsAms, nozzleTypes, nozzleFlows };
|
|
689
|
+
return { model, nozzleDiameters, materials, usedFilamentPositions: [...used].sort((a, b) => a - b), plateInternalPath, sha256, maxNozzleTemperature, maxBedTemperature, maxChamberTemperature, bedType, selectsAms, nozzleTypes, nozzleFlows, filamentNozzleIndices };
|
|
660
690
|
}
|