bambu-printer-mcp 1.1.26 → 1.1.28
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 +4 -0
- package/README.md +43 -8
- package/dist/bambu-native.d.ts +55 -0
- package/dist/bambu-native.js +212 -13
- package/dist/index.js +154 -26
- package/dist/printers/bambu.d.ts +29 -2
- package/dist/printers/bambu.js +45 -12
- package/dist/safety/hidden-hms.d.ts +32 -0
- package/dist/safety/hidden-hms.js +405 -0
- package/dist/safety/print-file.js +54 -4
- package/dist/safety/printer-state.d.ts +9 -0
- package/dist/safety/printer-state.js +80 -13
- package/dist/slicer/profile-files.d.ts +2 -0
- package/dist/slicer/profile-files.js +24 -0
- package/dist/slicer/profile-flatten.js +27 -13
- package/dist/stl/stl-manipulator.d.ts +1 -0
- package/dist/stl/stl-manipulator.js +6 -8
- package/native/bambu-native-print.cpp +100 -26
- package/package.json +7 -5
- package/scripts/build-bambu-native.sh +53 -0
- package/scripts/setup-bambu-native.mjs +372 -0
- package/src/bambu-native.ts +229 -14
- package/src/index.ts +152 -25
- package/src/printers/bambu.ts +51 -12
- package/src/safety/hidden-hms.ts +406 -0
- package/src/safety/print-file.ts +43 -4
- package/src/safety/printer-state.ts +85 -13
- package/src/slicer/profile-files.ts +21 -0
- package/src/slicer/profile-flatten.ts +34 -11
- package/src/stl/stl-manipulator.ts +7 -10
- package/scripts/build-bambu-native.zsh +0 -33
package/src/index.ts
CHANGED
|
@@ -27,6 +27,9 @@ import {
|
|
|
27
27
|
import { BambuNetworkBridge, type BambuNetworkBridgeOptions } from "./bambu-network-bridge.js";
|
|
28
28
|
import {
|
|
29
29
|
assertBambuNativeAvailable,
|
|
30
|
+
bambuNativeControlsAvailable,
|
|
31
|
+
bambuNativeUnavailableReason,
|
|
32
|
+
interruptNativeOperations,
|
|
30
33
|
validateBambuNativeMetadata,
|
|
31
34
|
buildBambuNativeFanCommand,
|
|
32
35
|
buildBambuNativeTemperatureCommand,
|
|
@@ -124,6 +127,21 @@ function resolveNozzleType(value: unknown): string | undefined {
|
|
|
124
127
|
}
|
|
125
128
|
return normalized;
|
|
126
129
|
}
|
|
130
|
+
|
|
131
|
+
/** BambuStudio's nozzle_volume_type value for each tool argument value. */
|
|
132
|
+
const NOZZLE_VOLUME_TYPES = { standard: "Standard", high_flow: "High Flow" } as const;
|
|
133
|
+
const VALID_NOZZLE_VOLUME_TYPES = Object.keys(NOZZLE_VOLUME_TYPES) as (keyof typeof NOZZLE_VOLUME_TYPES)[];
|
|
134
|
+
|
|
135
|
+
/** The installed nozzle's flow type. Bambu machine presets assume Standard; the print gate compares this with the printer. */
|
|
136
|
+
function resolveNozzleVolumeType(value: unknown): "Standard" | "High Flow" | undefined {
|
|
137
|
+
const raw = value === undefined || value === null || value === "" ? process.env.BAMBU_NOZZLE_VOLUME_TYPE?.trim() : value;
|
|
138
|
+
if (raw === undefined || raw === "") return undefined;
|
|
139
|
+
const normalized = String(raw).trim().toLowerCase().replace(/[\s-]+/g, "_");
|
|
140
|
+
if (!(VALID_NOZZLE_VOLUME_TYPES as readonly string[]).includes(normalized)) {
|
|
141
|
+
throw new Error(`Invalid nozzle volume type "${raw}". Valid types: ${VALID_NOZZLE_VOLUME_TYPES.join(", ")}`);
|
|
142
|
+
}
|
|
143
|
+
return NOZZLE_VOLUME_TYPES[normalized as keyof typeof NOZZLE_VOLUME_TYPES];
|
|
144
|
+
}
|
|
127
145
|
const VALID_BAMBUSTUDIO_CLI_BED_TYPES = ["textured_plate", "cool_plate", "engineering_plate", "hot_plate"] as const;
|
|
128
146
|
|
|
129
147
|
// Map model IDs to BambuStudio --load-machine preset names
|
|
@@ -1385,6 +1403,7 @@ class BambuPrinterMCPServer {
|
|
|
1385
1403
|
skipModifiedGcodes: true,
|
|
1386
1404
|
bedType: bedType ? resolveBambuStudioCliBedType(bedType) : undefined,
|
|
1387
1405
|
nozzleType: resolveNozzleType(args?.nozzle_type),
|
|
1406
|
+
nozzleVolumeType: resolveNozzleVolumeType(args?.nozzle_volume_type),
|
|
1388
1407
|
};
|
|
1389
1408
|
threeMFPath = await this.stlManipulator.sliceSTL(
|
|
1390
1409
|
threeMFPath, slicerType, slicerPath, slicerProfile || undefined,
|
|
@@ -1765,10 +1784,12 @@ class BambuPrinterMCPServer {
|
|
|
1765
1784
|
args: Record<string, any>, host: string, bambuSerial: string, bambuToken: string,
|
|
1766
1785
|
onUpdate?: BambuNativeUpdateCallback, signal?: AbortSignal
|
|
1767
1786
|
): Promise<Record<string, unknown>> {
|
|
1768
|
-
assertBambuNativeAvailable();
|
|
1769
1787
|
if (args.print) throw new Error("Native upload never starts printing; use print_3mf.");
|
|
1788
|
+
// Resolve and enforce the model first, as the print path does, so a
|
|
1789
|
+
// non-X2D request is never told to install the native helper.
|
|
1770
1790
|
const model = await this.resolveBambuModel(args.bambu_model);
|
|
1771
1791
|
if (model !== "x2d") throw new Error("connection_mode=bambu_native is restricted to X2D.");
|
|
1792
|
+
assertBambuNativeAvailable();
|
|
1772
1793
|
const unsupported = ["use_ams", "ams_mapping", "ams_mapping2", "ams_mapping_info", "nozzle_mapping", "nozzles_info",
|
|
1773
1794
|
"bed_leveling", "flow_calibration", "vibration_calibration", "layer_inspect", "timelapse"];
|
|
1774
1795
|
for (const key of unsupported) {
|
|
@@ -2364,6 +2385,11 @@ class BambuPrinterMCPServer {
|
|
|
2364
2385
|
enum: [...VALID_NOZZLE_TYPES],
|
|
2365
2386
|
description: "Bambu-compatible slicing: the hotend nozzle material installed on the printer (default: BAMBU_NOZZLE_TYPE, else the model preset's stock nozzle, usually stainless_steel). Printing compares it with the printer's reported nozzle."
|
|
2366
2387
|
},
|
|
2388
|
+
nozzle_volume_type: {
|
|
2389
|
+
type: "string",
|
|
2390
|
+
enum: [...VALID_NOZZLE_VOLUME_TYPES],
|
|
2391
|
+
description: "Bambu-compatible slicing: the flow type of the nozzle installed on the printer (default: BAMBU_NOZZLE_VOLUME_TYPE, else standard). high_flow uses the presets' High Flow values, such as the filament's High Flow max volumetric speed, and is refused if the machine preset has no High Flow variant. Printing compares it with the printer's reported nozzle flow."
|
|
2392
|
+
},
|
|
2367
2393
|
use_printer_filaments: { type: "boolean", description: "When true, and no explicit slicer profile or load_filaments override is provided, use the printer's current or first loaded AMS filament as the slicer filament profile." },
|
|
2368
2394
|
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
2369
2395
|
bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
|
|
@@ -2419,6 +2445,11 @@ class BambuPrinterMCPServer {
|
|
|
2419
2445
|
enum: [...VALID_NOZZLE_TYPES],
|
|
2420
2446
|
description: "Bambu-compatible slicing: the hotend nozzle material installed on the printer (default: BAMBU_NOZZLE_TYPE, else the model preset's stock nozzle, usually stainless_steel). Printing compares it with the printer's reported nozzle."
|
|
2421
2447
|
},
|
|
2448
|
+
nozzle_volume_type: {
|
|
2449
|
+
type: "string",
|
|
2450
|
+
enum: [...VALID_NOZZLE_VOLUME_TYPES],
|
|
2451
|
+
description: "Bambu-compatible slicing: the flow type of the nozzle installed on the printer (default: BAMBU_NOZZLE_VOLUME_TYPE, else standard). high_flow uses the presets' High Flow values, such as the filament's High Flow max volumetric speed, and is refused if the machine preset has no High Flow variant. Printing compares it with the printer's reported nozzle flow."
|
|
2452
|
+
},
|
|
2422
2453
|
use_printer_filaments: { type: "boolean", description: "When true, and no explicit slicer profile or load_filaments override is provided, use the printer's current or first loaded AMS filament as the slicer filament profile. Template 3MF process settings can still be used at the same time." },
|
|
2423
2454
|
uptodate: { type: "boolean", description: "Refresh 3MF preset configs to match the latest BambuStudio version. Use when slicing downloaded or older 3MF files to prevent stale-config failures." },
|
|
2424
2455
|
repetitions: { type: "number", description: "Print N identical copies of the model. Each copy gets its own plate placement. Example: 3 prints three copies." },
|
|
@@ -2525,6 +2556,7 @@ class BambuPrinterMCPServer {
|
|
|
2525
2556
|
password: { type: "string", description: "Printer password/access code override for LAN/local bridge methods." },
|
|
2526
2557
|
bed_type: { type: "string", enum: ["textured_plate", "cool_plate", "engineering_plate", "hot_plate", "supertack_plate"], description: "Bed plate type currently installed (default: textured_plate)." },
|
|
2527
2558
|
nozzle_type: { type: "string", enum: [...VALID_NOZZLE_TYPES], description: "Installed nozzle material, used when the 3MF must be auto-sliced (default: BAMBU_NOZZLE_TYPE, else the preset's stock nozzle)." },
|
|
2559
|
+
nozzle_volume_type: { type: "string", enum: [...VALID_NOZZLE_VOLUME_TYPES], description: "Installed nozzle flow type, used when the 3MF must be auto-sliced (default: BAMBU_NOZZLE_VOLUME_TYPE, else standard)." },
|
|
2528
2560
|
plate_index: { type: "number", description: "Zero-based plate index to print from the sliced 3MF; converted to FULU's one-based PrintParams plate_index." },
|
|
2529
2561
|
project_name: { type: "string", description: "Optional project name sent in FULU PrintParams; defaults to the 3MF filename without extension." },
|
|
2530
2562
|
preset_name: { type: "string", description: "Optional preset name sent in FULU PrintParams; defaults to project plus one-based plate index." },
|
|
@@ -2788,7 +2820,7 @@ class BambuPrinterMCPServer {
|
|
|
2788
2820
|
},
|
|
2789
2821
|
{
|
|
2790
2822
|
name: "reread_ams_rfid",
|
|
2791
|
-
description: "Trigger a Bambu AMS RFID re-read for one AMS slot. X2D on macOS uses the native official plug-in route. This can move AMS filament; use only when the printer is idle and unloaded.",
|
|
2823
|
+
description: "Trigger a Bambu AMS RFID re-read for one AMS slot. X2D on macOS, or Linux with the native helper installed, uses the native official plug-in route. This can move AMS filament; use only when the printer is idle and unloaded.",
|
|
2792
2824
|
inputSchema: {
|
|
2793
2825
|
type: "object",
|
|
2794
2826
|
properties: {
|
|
@@ -2821,7 +2853,7 @@ class BambuPrinterMCPServer {
|
|
|
2821
2853
|
},
|
|
2822
2854
|
{
|
|
2823
2855
|
name: "set_fan_speed",
|
|
2824
|
-
description: "Set a Bambu printer fan speed percentage. X2D on macOS
|
|
2856
|
+
description: "Set a Bambu printer fan speed percentage. X2D on macOS, or Linux with the native helper installed, uses Bambu's networking plug-in without UI automation; other models use MQTT.",
|
|
2825
2857
|
inputSchema: {
|
|
2826
2858
|
type: "object",
|
|
2827
2859
|
properties: {
|
|
@@ -2880,7 +2912,7 @@ class BambuPrinterMCPServer {
|
|
|
2880
2912
|
},
|
|
2881
2913
|
{
|
|
2882
2914
|
name: "set_ams_drying",
|
|
2883
|
-
description: "Start or stop the AMS filament drying cycle. X2D on macOS uses the native official plug-in route and supports AMS-HT id 128.",
|
|
2915
|
+
description: "Start or stop the AMS filament drying cycle. X2D on macOS, or Linux with the native helper installed, uses the native official plug-in route and supports AMS-HT id 128.",
|
|
2884
2916
|
inputSchema: {
|
|
2885
2917
|
type: "object",
|
|
2886
2918
|
properties: {
|
|
@@ -2900,6 +2932,23 @@ class BambuPrinterMCPServer {
|
|
|
2900
2932
|
required: ["action", "ams_id"]
|
|
2901
2933
|
}
|
|
2902
2934
|
},
|
|
2935
|
+
{
|
|
2936
|
+
name: "inspect_print_file",
|
|
2937
|
+
description: "Inspect an exact private snapshot of a prepared sliced 3MF or textual G-code file and selected plate without contacting a printer. Returns versioned file-only evidence; no printer readiness or permission to dispatch is established. Does not auto-slice.",
|
|
2938
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
2939
|
+
inputSchema: {
|
|
2940
|
+
type: "object",
|
|
2941
|
+
additionalProperties: false,
|
|
2942
|
+
properties: {
|
|
2943
|
+
file_path: { type: "string", minLength: 1, description: "Local prepared .3mf or textual .gcode artifact to inspect; checksum-only and unsliced projects reject." },
|
|
2944
|
+
bambu_model: { type: "string", enum: [...VALID_BAMBU_MODELS], description: "Explicit file-inspection target model; never inferred from environment or live state." },
|
|
2945
|
+
plate_index: { type: "integer", minimum: 0, description: "Zero-based selected plate; defaults to 0. Raw G-code only supports 0." },
|
|
2946
|
+
nozzle_diameters: { type: "array", minItems: 1, maxItems: 2, items: { type: "number", enum: [0.2, 0.4, 0.6, 0.8] }, description: "Optional requested nozzle diameters to compare with file declarations; this does not observe installed hardware." },
|
|
2947
|
+
bed_type: { type: "string", enum: [...VALID_BED_TYPES], description: "Optional declared bed type to compare with file metadata." },
|
|
2948
|
+
},
|
|
2949
|
+
required: ["file_path", "bambu_model"],
|
|
2950
|
+
},
|
|
2951
|
+
},
|
|
2903
2952
|
{
|
|
2904
2953
|
name: "print_3mf",
|
|
2905
2954
|
description: "Print a 3MF file on a Bambu Lab printer. Auto-slices if the 3MF has no gcode. IMPORTANT: bambu_model must be specified to ensure safe printer operation.",
|
|
@@ -2935,6 +2984,11 @@ class BambuPrinterMCPServer {
|
|
|
2935
2984
|
enum: [...VALID_NOZZLE_TYPES],
|
|
2936
2985
|
description: "Installed nozzle material, used when the 3MF must be auto-sliced (default: BAMBU_NOZZLE_TYPE, else the preset's stock nozzle)."
|
|
2937
2986
|
},
|
|
2987
|
+
nozzle_volume_type: {
|
|
2988
|
+
type: "string",
|
|
2989
|
+
enum: [...VALID_NOZZLE_VOLUME_TYPES],
|
|
2990
|
+
description: "Installed nozzle flow type, used when the 3MF must be auto-sliced (default: BAMBU_NOZZLE_VOLUME_TYPE, else standard)."
|
|
2991
|
+
},
|
|
2938
2992
|
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
2939
2993
|
bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
|
|
2940
2994
|
bambu_token: { type: "string", description: "Access token (default: value from env)" },
|
|
@@ -3130,9 +3184,43 @@ class BambuPrinterMCPServer {
|
|
|
3130
3184
|
const host = String(args?.host || DEFAULT_HOST);
|
|
3131
3185
|
const bambuSerial = String(args?.bambu_serial || DEFAULT_BAMBU_SERIAL);
|
|
3132
3186
|
const bambuToken = String(args?.bambu_token || DEFAULT_BAMBU_TOKEN);
|
|
3133
|
-
const
|
|
3134
|
-
(
|
|
3135
|
-
|
|
3187
|
+
const x2dTarget = bambuSerial.trim().toUpperCase().startsWith("20P") ||
|
|
3188
|
+
(DEFAULT_BAMBU_MODEL === "x2d" && bambuSerial === DEFAULT_BAMBU_SERIAL);
|
|
3189
|
+
// Resolved lazily: the Linux availability check reads files, and most
|
|
3190
|
+
// tools (status, slicing, STL) never need it.
|
|
3191
|
+
// One resolution per request; null means native X2D controls are usable.
|
|
3192
|
+
let nativeUnavailable: string | null | undefined;
|
|
3193
|
+
const nativeUnavailableReason = () =>
|
|
3194
|
+
(nativeUnavailable ??= x2dTarget ? bambuNativeUnavailableReason() ?? null : "The selected printer is not an X2D.");
|
|
3195
|
+
const nativeControls = () => nativeUnavailableReason() === null;
|
|
3196
|
+
// Linux sent X2D stop and heater-off over MQTT before the native route
|
|
3197
|
+
// existed. Send them over both routes there: a stuck helper must not
|
|
3198
|
+
// delay an emergency control, and a repeated stop or heater-off is
|
|
3199
|
+
// harmless. The immediate MQTT send can precede the exit of an older
|
|
3200
|
+
// print or heating helper that then still dispatches, so MQTT is sent
|
|
3201
|
+
// again once every older helper for this printer has exited, and only
|
|
3202
|
+
// that send counts. Either counted route succeeding is success. macOS
|
|
3203
|
+
// stays native-only.
|
|
3204
|
+
const linuxEmergency = (native: () => Promise<any>, mqtt: () => Promise<any>) => async () => {
|
|
3205
|
+
if (process.platform !== "linux") return native();
|
|
3206
|
+
const [nativeResult, immediateResult, mqttResult] = await Promise.allSettled([
|
|
3207
|
+
native(),
|
|
3208
|
+
mqtt(),
|
|
3209
|
+
interruptNativeOperations(bambuSerial).then(() => mqtt()),
|
|
3210
|
+
]);
|
|
3211
|
+
const reason = (result: PromiseSettledResult<any>) =>
|
|
3212
|
+
result.status === "rejected" ? (result.reason instanceof Error ? result.reason.message : String(result.reason)) : undefined;
|
|
3213
|
+
if (nativeResult.status === "rejected" && mqttResult.status === "rejected") {
|
|
3214
|
+
throw new Error(`native route: ${reason(nativeResult)}; MQTT route: ${reason(mqttResult)}`);
|
|
3215
|
+
}
|
|
3216
|
+
const state = (result: PromiseSettledResult<any>) => result.status === "fulfilled" ? "sent" : "failed";
|
|
3217
|
+
return {
|
|
3218
|
+
status: "success",
|
|
3219
|
+
routes: { native: state(nativeResult), mqtt: state(mqttResult), mqtt_immediate: state(immediateResult) },
|
|
3220
|
+
...(reason(nativeResult) ? { native_error: reason(nativeResult) } : {}),
|
|
3221
|
+
...(reason(mqttResult) ? { mqtt_error: reason(mqttResult) } : {}),
|
|
3222
|
+
};
|
|
3223
|
+
};
|
|
3136
3224
|
const requestedTemplateDir =
|
|
3137
3225
|
typeof args?.template_dir === "string" && args.template_dir.trim().length > 0
|
|
3138
3226
|
? String(args.template_dir)
|
|
@@ -3147,6 +3235,34 @@ class BambuPrinterMCPServer {
|
|
|
3147
3235
|
let result;
|
|
3148
3236
|
|
|
3149
3237
|
switch (name) {
|
|
3238
|
+
case "inspect_print_file": {
|
|
3239
|
+
const allowed = new Set(["file_path", "bambu_model", "plate_index", "nozzle_diameters", "bed_type"]);
|
|
3240
|
+
if (!args || Object.keys(args).some(key => !allowed.has(key))) throw new Error("inspect_print_file accepts only file/model/plate/nozzle/bed arguments.");
|
|
3241
|
+
if (typeof args.file_path !== "string" || !args.file_path.trim() || args.file_path.includes("\0")) throw new Error("file_path must name a local prepared artifact.");
|
|
3242
|
+
if (typeof args.bambu_model !== "string" || !args.bambu_model.trim()) throw new Error("An explicit bambu_model is required for file inspection.");
|
|
3243
|
+
const model = validateBambuModel(args.bambu_model);
|
|
3244
|
+
const plateIndex = args.plate_index === undefined ? 0 : args.plate_index;
|
|
3245
|
+
if (typeof plateIndex !== "number" || !Number.isSafeInteger(plateIndex) || plateIndex < 0) throw new Error("plate_index must be a nonnegative safe integer.");
|
|
3246
|
+
if (args.nozzle_diameters !== undefined && (!Array.isArray(args.nozzle_diameters) || args.nozzle_diameters.some(value => typeof value !== "number"))) throw new Error("nozzle_diameters must be numeric sizes.");
|
|
3247
|
+
const nozzles = requestedPrintNozzles(args);
|
|
3248
|
+
if (args.bed_type !== undefined && (typeof args.bed_type !== "string" || !args.bed_type.trim())) throw new Error("bed_type must be a supported plate type.");
|
|
3249
|
+
const bedType = args.bed_type === undefined ? undefined : resolveBedType(args.bed_type);
|
|
3250
|
+
if (extra.signal.aborted) throw new Error("File inspection cancelled.");
|
|
3251
|
+
result = await withPrintSnapshot(args.file_path, async snapshot => {
|
|
3252
|
+
const inspection = await inspectPrintFile(snapshot, { model, nozzleDiameters: nozzles, plateIndex, bedType });
|
|
3253
|
+
if (extra.signal.aborted) throw new Error("File inspection cancelled.");
|
|
3254
|
+
const byteLength = (await fs.promises.stat(snapshot)).size;
|
|
3255
|
+
return {
|
|
3256
|
+
receipt_version: "1",
|
|
3257
|
+
evidence: "file-only",
|
|
3258
|
+
dispatch_authorized: false,
|
|
3259
|
+
artifact: { sha256: inspection.sha256, byte_length: byteLength, file_format: path.extname(snapshot).toLowerCase() === ".3mf" ? "3mf" : "gcode" },
|
|
3260
|
+
selection: { bambu_model: model, plate_index: plateIndex, ...(nozzles === undefined ? {} : { nozzle_diameters: nozzles }), ...(bedType === undefined ? {} : { bed_type: bedType }) },
|
|
3261
|
+
inspection,
|
|
3262
|
+
};
|
|
3263
|
+
});
|
|
3264
|
+
break;
|
|
3265
|
+
}
|
|
3150
3266
|
case "get_printer_status":
|
|
3151
3267
|
result = await this.bambu.getStatus(host, bambuSerial, bambuToken);
|
|
3152
3268
|
break;
|
|
@@ -3360,15 +3476,17 @@ class BambuPrinterMCPServer {
|
|
|
3360
3476
|
|
|
3361
3477
|
case "cancel_print":
|
|
3362
3478
|
result = await this.bambu.cancelJob(host, bambuSerial, bambuToken,
|
|
3363
|
-
nativeControls
|
|
3364
|
-
? (
|
|
3365
|
-
|
|
3479
|
+
nativeControls()
|
|
3480
|
+
? linuxEmergency(
|
|
3481
|
+
() => sendNativeCommand({ host, serial: bambuSerial, token: bambuToken,
|
|
3482
|
+
messageJson: JSON.stringify({ print: { command: "stop", param: "", sequence_id: String(Date.now()) } }), qos: 1 }),
|
|
3483
|
+
() => this.bambu.sendStopOverMqtt(host, bambuSerial, bambuToken))
|
|
3366
3484
|
: undefined);
|
|
3367
3485
|
break;
|
|
3368
3486
|
|
|
3369
3487
|
case "pause_print":
|
|
3370
3488
|
result = await this.bambu.pauseJob(host, bambuSerial, bambuToken,
|
|
3371
|
-
nativeControls
|
|
3489
|
+
nativeControls()
|
|
3372
3490
|
? () => sendNativeCommand({ host, serial: bambuSerial, token: bambuToken,
|
|
3373
3491
|
messageJson: JSON.stringify({ print: { command: "pause", param: "", sequence_id: String(Date.now()) } }), qos: 1 })
|
|
3374
3492
|
: undefined);
|
|
@@ -3376,7 +3494,7 @@ class BambuPrinterMCPServer {
|
|
|
3376
3494
|
|
|
3377
3495
|
case "resume_print":
|
|
3378
3496
|
result = await this.bambu.resumeJob(host, bambuSerial, bambuToken,
|
|
3379
|
-
nativeControls
|
|
3497
|
+
nativeControls()
|
|
3380
3498
|
? (assertActive, beforeDispatch) => sendNativeCommand({ host, serial: bambuSerial, token: bambuToken,
|
|
3381
3499
|
messageJson: JSON.stringify({ print: { command: "resume", param: "", sequence_id: String(Date.now()) } }), qos: 1 }, { signal: extra.signal, assertActive, beforeDispatch })
|
|
3382
3500
|
: undefined);
|
|
@@ -3386,8 +3504,9 @@ class BambuPrinterMCPServer {
|
|
|
3386
3504
|
if (!args?.message_json) {
|
|
3387
3505
|
throw new Error("Missing required parameter: message_json");
|
|
3388
3506
|
}
|
|
3389
|
-
if (!nativeControls) {
|
|
3390
|
-
throw new Error("x2d_native_control is restricted to a selected X2D printer on macOS."
|
|
3507
|
+
if (!nativeControls()) {
|
|
3508
|
+
throw new Error("x2d_native_control is restricted to a selected X2D printer on macOS, or on Linux with the native helper installed. " +
|
|
3509
|
+
nativeUnavailableReason());
|
|
3391
3510
|
}
|
|
3392
3511
|
const messageJson = validateBambuNativeMetadata(String(args.message_json));
|
|
3393
3512
|
result = await sendNativeCommand({
|
|
@@ -3403,7 +3522,7 @@ class BambuPrinterMCPServer {
|
|
|
3403
3522
|
|
|
3404
3523
|
case "clear_hms_errors":
|
|
3405
3524
|
result = await this.bambu.clearHmsErrors(host, bambuSerial, bambuToken,
|
|
3406
|
-
nativeControls
|
|
3525
|
+
nativeControls()
|
|
3407
3526
|
? (assertActive, beforeDispatch) => sendNativeCommand({ host, serial: bambuSerial, token: bambuToken,
|
|
3408
3527
|
messageJson: JSON.stringify({ print: { command: "clean_print_error", param: "", sequence_id: String(Date.now()) } }), qos: 1 }, { signal: extra.signal, assertActive, beforeDispatch })
|
|
3409
3528
|
: undefined);
|
|
@@ -3413,7 +3532,7 @@ class BambuPrinterMCPServer {
|
|
|
3413
3532
|
if (!args?.mode) {
|
|
3414
3533
|
throw new Error("Missing required parameter: mode");
|
|
3415
3534
|
}
|
|
3416
|
-
if (nativeControls) {
|
|
3535
|
+
if (nativeControls()) {
|
|
3417
3536
|
const normalized = String(args.mode).trim().toLowerCase();
|
|
3418
3537
|
const mode = normalized === "silent" ? 1
|
|
3419
3538
|
: normalized === "standard" ? 2
|
|
@@ -3440,7 +3559,7 @@ class BambuPrinterMCPServer {
|
|
|
3440
3559
|
if (!args?.mode) {
|
|
3441
3560
|
throw new Error("Missing required parameter: mode");
|
|
3442
3561
|
}
|
|
3443
|
-
if (nativeControls) {
|
|
3562
|
+
if (nativeControls()) {
|
|
3444
3563
|
const mode = String(args.mode).trim().toLowerCase();
|
|
3445
3564
|
if (mode !== "cooling" && mode !== "heating") {
|
|
3446
3565
|
throw new Error("Airduct mode must be one of: cooling, heating.");
|
|
@@ -3467,7 +3586,7 @@ class BambuPrinterMCPServer {
|
|
|
3467
3586
|
if (args?.ams_id === undefined || args?.slot_id === undefined) {
|
|
3468
3587
|
throw new Error("Missing required parameters: ams_id and slot_id");
|
|
3469
3588
|
}
|
|
3470
|
-
if (nativeControls) {
|
|
3589
|
+
if (nativeControls()) {
|
|
3471
3590
|
const amsId = Number(args.ams_id);
|
|
3472
3591
|
const slotId = Number(args.slot_id);
|
|
3473
3592
|
if ((!Number.isInteger(amsId) || (amsId < 0 || amsId > 3)) && amsId !== 128) {
|
|
@@ -3516,9 +3635,14 @@ class BambuPrinterMCPServer {
|
|
|
3516
3635
|
heaterModel || undefined,
|
|
3517
3636
|
args?.material !== undefined ? String(args.material) : undefined,
|
|
3518
3637
|
Number(args?.nozzle_diameter ?? DEFAULT_NOZZLE_DIAMETER),
|
|
3519
|
-
(args.temperature === 0 ? nativeControls : heaterModel === "x2d" &&
|
|
3520
|
-
? (heater, target, assertActive, beforeDispatch) =>
|
|
3521
|
-
|
|
3638
|
+
(args.temperature === 0 ? nativeControls() : heaterModel === "x2d" && bambuNativeControlsAvailable())
|
|
3639
|
+
? (heater, target, assertActive, beforeDispatch) => {
|
|
3640
|
+
const native = () => sendNativeCommand({ host, serial: bambuSerial, token: bambuToken,
|
|
3641
|
+
messageJson: buildBambuNativeTemperatureCommand(heater, target).messageJson }, { signal: extra.signal, assertActive, beforeDispatch });
|
|
3642
|
+
return target === 0
|
|
3643
|
+
? linuxEmergency(native, () => this.bambu.sendHeaterOffOverMqtt(host, bambuSerial, bambuToken, heater))()
|
|
3644
|
+
: native();
|
|
3645
|
+
}
|
|
3522
3646
|
: undefined
|
|
3523
3647
|
);
|
|
3524
3648
|
break;
|
|
@@ -3528,7 +3652,7 @@ class BambuPrinterMCPServer {
|
|
|
3528
3652
|
if (!args?.fan || args?.speed === undefined) {
|
|
3529
3653
|
throw new Error("Missing required parameters: fan and speed");
|
|
3530
3654
|
}
|
|
3531
|
-
if (nativeControls) {
|
|
3655
|
+
if (nativeControls()) {
|
|
3532
3656
|
const status = await this.bambu.getStatus(host, bambuSerial, bambuToken);
|
|
3533
3657
|
const printerState = String(status?.raw?.gcode_state ?? status?.raw?.print?.gcode_state ?? status?.gcode_state ?? status?.status ?? "").toUpperCase();
|
|
3534
3658
|
if (printerState === "RUNNING" && args?.confirm_during_print !== true) {
|
|
@@ -3562,7 +3686,7 @@ class BambuPrinterMCPServer {
|
|
|
3562
3686
|
if (!args?.light || !args?.mode) {
|
|
3563
3687
|
throw new Error("Missing required parameters: light and mode");
|
|
3564
3688
|
}
|
|
3565
|
-
if (nativeControls) {
|
|
3689
|
+
if (nativeControls()) {
|
|
3566
3690
|
const light = String(args.light).trim();
|
|
3567
3691
|
const mode = String(args.mode).trim().toLowerCase();
|
|
3568
3692
|
if (light !== "chamber_light" && light !== "chamber_light2") {
|
|
@@ -3603,7 +3727,7 @@ class BambuPrinterMCPServer {
|
|
|
3603
3727
|
if (!Array.isArray(args?.object_ids)) {
|
|
3604
3728
|
throw new Error("Missing required parameter: object_ids");
|
|
3605
3729
|
}
|
|
3606
|
-
if (nativeControls) {
|
|
3730
|
+
if (nativeControls()) {
|
|
3607
3731
|
const objectIds = args.object_ids.map((id: unknown) => Number(id));
|
|
3608
3732
|
if (objectIds.length === 0 || objectIds.some((id: number) => !Number.isInteger(id) || id < 0)) {
|
|
3609
3733
|
throw new Error("object_ids must contain one or more non-negative integers.");
|
|
@@ -3631,7 +3755,7 @@ class BambuPrinterMCPServer {
|
|
|
3631
3755
|
if (!args?.action || args?.ams_id === undefined) {
|
|
3632
3756
|
throw new Error("Missing required parameters: action and ams_id");
|
|
3633
3757
|
}
|
|
3634
|
-
if (nativeControls) {
|
|
3758
|
+
if (nativeControls()) {
|
|
3635
3759
|
const action = String(args.action).trim().toLowerCase();
|
|
3636
3760
|
const amsId = Number(args.ams_id);
|
|
3637
3761
|
if (action !== "start" && action !== "stop") {
|
|
@@ -3763,6 +3887,7 @@ class BambuPrinterMCPServer {
|
|
|
3763
3887
|
if (args?.filament_colours !== undefined) sliceBambuOptions.filamentColours = parseFilamentColours(String(args.filament_colours));
|
|
3764
3888
|
sliceBambuOptions.bedType = resolveBambuStudioCliBedType(args?.bed_type as string | undefined);
|
|
3765
3889
|
sliceBambuOptions.nozzleType = resolveNozzleType(args?.nozzle_type);
|
|
3890
|
+
sliceBambuOptions.nozzleVolumeType = resolveNozzleVolumeType(args?.nozzle_volume_type);
|
|
3766
3891
|
if (args?.enable_timelapse !== undefined) sliceBambuOptions.enableTimelapse = Boolean(args.enable_timelapse);
|
|
3767
3892
|
if (args?.allow_mix_temp !== undefined) sliceBambuOptions.allowMixTemp = Boolean(args.allow_mix_temp);
|
|
3768
3893
|
if (args?.scale !== undefined) sliceBambuOptions.scale = Number(args.scale);
|
|
@@ -3850,6 +3975,7 @@ class BambuPrinterMCPServer {
|
|
|
3850
3975
|
if (args?.filament_colours !== undefined) sliceBambuOptions.filamentColours = parseFilamentColours(String(args.filament_colours));
|
|
3851
3976
|
sliceBambuOptions.bedType = resolveBambuStudioCliBedType(args?.bed_type as string | undefined);
|
|
3852
3977
|
sliceBambuOptions.nozzleType = resolveNozzleType(args?.nozzle_type);
|
|
3978
|
+
sliceBambuOptions.nozzleVolumeType = resolveNozzleVolumeType(args?.nozzle_volume_type);
|
|
3853
3979
|
if (args?.enable_timelapse !== undefined) sliceBambuOptions.enableTimelapse = Boolean(args.enable_timelapse);
|
|
3854
3980
|
if (args?.allow_mix_temp !== undefined) sliceBambuOptions.allowMixTemp = Boolean(args.allow_mix_temp);
|
|
3855
3981
|
if (args?.scale !== undefined) sliceBambuOptions.scale = Number(args.scale);
|
|
@@ -3978,6 +4104,7 @@ class BambuPrinterMCPServer {
|
|
|
3978
4104
|
skipModifiedGcodes: true,
|
|
3979
4105
|
bedType: printBedType,
|
|
3980
4106
|
nozzleType: resolveNozzleType(args?.nozzle_type),
|
|
4107
|
+
nozzleVolumeType: resolveNozzleVolumeType(args?.nozzle_volume_type),
|
|
3981
4108
|
};
|
|
3982
4109
|
if (!explicitSlicerProfile) {
|
|
3983
4110
|
try {
|
package/src/printers/bambu.ts
CHANGED
|
@@ -8,7 +8,7 @@ import { Readable } from "node:stream";
|
|
|
8
8
|
import { readSafe3mfArchive } from "../safety/archive.js";
|
|
9
9
|
import { inspectPrintFile, type PrintFileInspection } from "../safety/print-file.js";
|
|
10
10
|
import { normalizeModel, validateTemperature } from "../safety/limits.js";
|
|
11
|
-
import { validatePrinterState, manualHeatingRequirements, type PrinterStateRequirements } from "../safety/printer-state.js";
|
|
11
|
+
import { describeHmsKey, hiddenHmsKeys, hiddenHmsNotices, reportedHmsKey, validatePrinterState, manualHeatingRequirements, type PrinterStateRequirements } from "../safety/printer-state.js";
|
|
12
12
|
import { withPrinterOperation, withPrintSnapshot, uniquePrintName, normalizedRemotePath, cancelPendingPrinterOperations } from "../safety/artifact.js";
|
|
13
13
|
import { Client as FTPClient } from "basic-ftp";
|
|
14
14
|
import { BambuPrinter } from "bambu-js";
|
|
@@ -85,7 +85,7 @@ const H2_MODEL_NAMES = new Set(["h2", "h2c", "h2d", "h2dpro", "h2d pro", "h2s",
|
|
|
85
85
|
export function assertDirectPrintSupported(model: string | undefined, serial?: string): void {
|
|
86
86
|
if (model?.trim().toLowerCase() === "x2d" || serial?.trim().toUpperCase().startsWith("20P")) {
|
|
87
87
|
throw new Error(
|
|
88
|
-
"X2D direct printing is not supported through legacy MQTT/FTPS. Use the
|
|
88
|
+
"X2D direct printing is not supported through legacy MQTT/FTPS. Use the native eMMC route via print_3mf (macOS, or Linux with the native helper installed). " +
|
|
89
89
|
"X2D status and slicing remain available on all supported platforms."
|
|
90
90
|
);
|
|
91
91
|
}
|
|
@@ -461,6 +461,9 @@ class BambuClientStore {
|
|
|
461
461
|
}
|
|
462
462
|
}
|
|
463
463
|
|
|
464
|
+
/** What the human confirmed in confirmPrintPreflight, checked again before dispatch. */
|
|
465
|
+
export type PrintPreflight = { finishedJob?: string; serial: string; model: string; notices: string[] };
|
|
466
|
+
|
|
464
467
|
export class BambuImplementation {
|
|
465
468
|
private printerStore: BambuClientStore;
|
|
466
469
|
private checkedJobs = new Map<string, { remotePath: string; requirements: PrinterStateRequirements }>();
|
|
@@ -481,25 +484,41 @@ export class BambuImplementation {
|
|
|
481
484
|
return status.raw.gcode_state === "FINISH" ? JSON.stringify([status.raw.gcode_file, status.raw.subtask_name, status.raw.task_id, status.raw.subtask_id]) : undefined;
|
|
482
485
|
}
|
|
483
486
|
|
|
484
|
-
|
|
487
|
+
/**
|
|
488
|
+
* The human print confirmation. Its result goes to assertBedClearance at
|
|
489
|
+
* every later state check of the same print, which stops the print if the
|
|
490
|
+
* situation the human confirmed has changed.
|
|
491
|
+
*/
|
|
492
|
+
async confirmPrintPreflight(serial: string, status: any, inspection: Awaited<ReturnType<typeof inspectPrintFile>>): Promise<PrintPreflight> {
|
|
485
493
|
const finishedJob = this.finishedJobIdentity(status);
|
|
486
494
|
const cleared = this.clearedErrors.get(serial);
|
|
495
|
+
const notices = hiddenHmsNotices(status?.raw, serial, inspection.model);
|
|
487
496
|
await this.confirmHardwareAction(
|
|
488
497
|
`Start a checked print on ${inspection.model.toUpperCase()} (${serial})? Nozzles: ${inspection.nozzleDiameters.join(", ")} mm. ` +
|
|
489
498
|
`Materials: ${inspection.materials.join(", ")}. Peak targets: nozzle ${inspection.maxNozzleTemperature}°C, bed ${inspection.maxBedTemperature}°C, chamber ${inspection.maxChamberTemperature}°C. ` +
|
|
490
499
|
`File SHA-256: ${inspection.sha256}. Confirm the physical spool labels and that the build plate is clear.` +
|
|
491
500
|
(finishedJob !== undefined ? " The printer reports FINISH: remove the previous part and debris before confirming." : "") +
|
|
492
|
-
(cleared ? ` Previously cleared hardware codes: ${cleared.join(", ")}. Confirm their physical causes have been resolved.` : "")
|
|
501
|
+
(cleared ? ` Previously cleared hardware codes: ${cleared.map(describeHmsKey).join(", ")}. Confirm their physical causes have been resolved.` : "") +
|
|
502
|
+
(notices.length ? ` The printer also reports ${notices.join(", ")}, which Bambu lists for this model without text and never shows as a fault; ` +
|
|
503
|
+
`${notices.length === 1 ? "it does" : "they do"} not block printing.` : ""),
|
|
493
504
|
finishedJob !== undefined || !!cleared
|
|
494
505
|
);
|
|
495
|
-
return finishedJob;
|
|
506
|
+
return { finishedJob, serial, model: inspection.model, notices };
|
|
496
507
|
}
|
|
497
508
|
|
|
498
|
-
|
|
509
|
+
/** Stop a print whose confirmed situation changed: a newly finished job, or a hidden HMS notice the preflight did not name. */
|
|
510
|
+
assertBedClearance(status: any, preflight: PrintPreflight): void {
|
|
499
511
|
const current = this.finishedJobIdentity(status);
|
|
500
|
-
if (current !== undefined && current !==
|
|
512
|
+
if (current !== undefined && current !== preflight.finishedJob) {
|
|
501
513
|
throw new Error("Printer reports a newly finished job. Confirm that its part and debris have been removed before retrying the print.");
|
|
502
514
|
}
|
|
515
|
+
// An X2D raises its hidden camera notice about 15 seconds after boot, so it
|
|
516
|
+
// can appear after the human confirmed. Never pass a notice nobody was told about.
|
|
517
|
+
const unnamed = hiddenHmsNotices(status?.raw, preflight.serial, preflight.model).filter(notice => !preflight.notices.includes(notice));
|
|
518
|
+
if (unnamed.length) {
|
|
519
|
+
throw new Error(`Printer reported ${unnamed.join(", ")} after the print confirmation. ` +
|
|
520
|
+
"Bambu hides it and it does not block printing, but it was not named when you confirmed; start the print again to review it.");
|
|
521
|
+
}
|
|
503
522
|
}
|
|
504
523
|
|
|
505
524
|
private async getPrinter(host: string, serial: string, token: string): Promise<BambuClient> {
|
|
@@ -944,6 +963,21 @@ export class BambuImplementation {
|
|
|
944
963
|
}
|
|
945
964
|
}
|
|
946
965
|
|
|
966
|
+
/** Raw MQTT stop for routes that already ran the shared cancellation steps. */
|
|
967
|
+
async sendStopOverMqtt(host: string, serial: string, token: string): Promise<any> {
|
|
968
|
+
const printer = await this.getPrinter(host, serial, token);
|
|
969
|
+
await invokeWithoutAck(printer, new UpdateStateCommand({ state: "stop" }));
|
|
970
|
+
return { status: "success", message: "Cancel command sent over MQTT." };
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
/** Raw MQTT heater-off; switching off needs no material or model declaration. */
|
|
974
|
+
async sendHeaterOffOverMqtt(host: string, serial: string, token: string, heater: "bed" | "nozzle"): Promise<any> {
|
|
975
|
+
const printer = await this.getPrinter(host, serial, token);
|
|
976
|
+
const gcode = `${heater === "bed" ? "M140" : "M104"} S0`;
|
|
977
|
+
await invokeWithoutAck(printer, new GCodeLineCommand({ gcodes: [gcode] }));
|
|
978
|
+
return { status: "success", message: `Heater-off sent over MQTT for ${heater}.`, command: gcode };
|
|
979
|
+
}
|
|
980
|
+
|
|
947
981
|
async pauseJob(host: string, serial: string, token: string, dispatch?: () => Promise<any>): Promise<any> {
|
|
948
982
|
const printer = dispatch ? undefined : await this.getPrinter(host, serial, token);
|
|
949
983
|
try {
|
|
@@ -982,10 +1016,12 @@ export class BambuImplementation {
|
|
|
982
1016
|
return withPrinterOperation(host, serial, async assertActive => {
|
|
983
1017
|
const codesFor = (status: any): string[] => {
|
|
984
1018
|
if (!Array.isArray(status.raw?.hms) || status.raw?.print_error === undefined) throw new Error("Fresh error codes are required before clearing hardware errors.");
|
|
985
|
-
|
|
1019
|
+
// Hidden notices never block and survive clean_print_error, so they are not errors to clear or re-acknowledge.
|
|
1020
|
+
const hidden = new Set(hiddenHmsKeys(status.raw, serial, String(status.model ?? "")));
|
|
1021
|
+
return [`print_error:${status.raw.print_error}`, ...status.raw.hms.map(reportedHmsKey).filter((key: string) => !hidden.has(key))].sort();
|
|
986
1022
|
};
|
|
987
1023
|
const codes = codesFor(await this.getSafetyStatus(host, serial, token));
|
|
988
|
-
await this.confirmHardwareAction(`Clear reported hardware errors on ${serial}: ${codes.join(", ")}? Inspect the printer and resolve the physical cause first. Confirm only after removing obstructions and correcting the fault.`, true);
|
|
1024
|
+
await this.confirmHardwareAction(`Clear reported hardware errors on ${serial}: ${codes.map(describeHmsKey).join(", ")}? Inspect the printer and resolve the physical cause first. Confirm only after removing obstructions and correcting the fault.`, true);
|
|
989
1025
|
const beforeDispatch = async () => {
|
|
990
1026
|
assertActive();
|
|
991
1027
|
const current = codesFor(await this.getSafetyStatus(host, serial, token));
|
|
@@ -1141,9 +1177,12 @@ export class BambuImplementation {
|
|
|
1141
1177
|
}
|
|
1142
1178
|
const printer = dispatch ? undefined : await this.getPrinter(host, serial, token);
|
|
1143
1179
|
assertActive();
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1180
|
+
const dispatched = dispatch ? await dispatch(heater, targetTemperature, assertActive, beforeDispatch) : undefined;
|
|
1181
|
+
if (!dispatch) await invokeWithoutAck(printer!, new GCodeLineCommand({ gcodes: [gcode] }));
|
|
1182
|
+
// Keep route details from a dual-route Linux heater-off.
|
|
1183
|
+
const routeDetails = dispatched && typeof dispatched === "object" && "routes" in dispatched
|
|
1184
|
+
? { routes: dispatched.routes, native_error: dispatched.native_error, mqtt_error: dispatched.mqtt_error } : {};
|
|
1185
|
+
return { status: "success", message: `Temperature command sent for ${normalizedComponent}.`, command: gcode, ...routeDetails };
|
|
1147
1186
|
};
|
|
1148
1187
|
if (targetTemperature === 0) {
|
|
1149
1188
|
cancelPendingPrinterOperations(host, serial);
|