bambu-printer-mcp 1.1.27 → 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 +25 -8
- package/dist/bambu-native.d.ts +55 -0
- package/dist/bambu-native.js +212 -13
- package/dist/index.js +101 -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 +47 -1
- package/dist/safety/printer-state.d.ts +9 -0
- package/dist/safety/printer-state.js +80 -13
- package/dist/slicer/profile-flatten.js +25 -0
- package/dist/stl/stl-manipulator.d.ts +1 -0
- package/dist/stl/stl-manipulator.js +1 -0
- 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 +107 -25
- package/src/printers/bambu.ts +51 -12
- package/src/safety/hidden-hms.ts +406 -0
- package/src/safety/print-file.ts +37 -1
- package/src/safety/printer-state.ts +85 -13
- package/src/slicer/profile-flatten.ts +32 -0
- package/src/stl/stl-manipulator.ts +2 -0
- 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: {
|
|
@@ -2952,6 +2984,11 @@ class BambuPrinterMCPServer {
|
|
|
2952
2984
|
enum: [...VALID_NOZZLE_TYPES],
|
|
2953
2985
|
description: "Installed nozzle material, used when the 3MF must be auto-sliced (default: BAMBU_NOZZLE_TYPE, else the preset's stock nozzle)."
|
|
2954
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
|
+
},
|
|
2955
2992
|
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
2956
2993
|
bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
|
|
2957
2994
|
bambu_token: { type: "string", description: "Access token (default: value from env)" },
|
|
@@ -3147,9 +3184,43 @@ class BambuPrinterMCPServer {
|
|
|
3147
3184
|
const host = String(args?.host || DEFAULT_HOST);
|
|
3148
3185
|
const bambuSerial = String(args?.bambu_serial || DEFAULT_BAMBU_SERIAL);
|
|
3149
3186
|
const bambuToken = String(args?.bambu_token || DEFAULT_BAMBU_TOKEN);
|
|
3150
|
-
const
|
|
3151
|
-
(
|
|
3152
|
-
|
|
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
|
+
};
|
|
3153
3224
|
const requestedTemplateDir =
|
|
3154
3225
|
typeof args?.template_dir === "string" && args.template_dir.trim().length > 0
|
|
3155
3226
|
? String(args.template_dir)
|
|
@@ -3405,15 +3476,17 @@ class BambuPrinterMCPServer {
|
|
|
3405
3476
|
|
|
3406
3477
|
case "cancel_print":
|
|
3407
3478
|
result = await this.bambu.cancelJob(host, bambuSerial, bambuToken,
|
|
3408
|
-
nativeControls
|
|
3409
|
-
? (
|
|
3410
|
-
|
|
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))
|
|
3411
3484
|
: undefined);
|
|
3412
3485
|
break;
|
|
3413
3486
|
|
|
3414
3487
|
case "pause_print":
|
|
3415
3488
|
result = await this.bambu.pauseJob(host, bambuSerial, bambuToken,
|
|
3416
|
-
nativeControls
|
|
3489
|
+
nativeControls()
|
|
3417
3490
|
? () => sendNativeCommand({ host, serial: bambuSerial, token: bambuToken,
|
|
3418
3491
|
messageJson: JSON.stringify({ print: { command: "pause", param: "", sequence_id: String(Date.now()) } }), qos: 1 })
|
|
3419
3492
|
: undefined);
|
|
@@ -3421,7 +3494,7 @@ class BambuPrinterMCPServer {
|
|
|
3421
3494
|
|
|
3422
3495
|
case "resume_print":
|
|
3423
3496
|
result = await this.bambu.resumeJob(host, bambuSerial, bambuToken,
|
|
3424
|
-
nativeControls
|
|
3497
|
+
nativeControls()
|
|
3425
3498
|
? (assertActive, beforeDispatch) => sendNativeCommand({ host, serial: bambuSerial, token: bambuToken,
|
|
3426
3499
|
messageJson: JSON.stringify({ print: { command: "resume", param: "", sequence_id: String(Date.now()) } }), qos: 1 }, { signal: extra.signal, assertActive, beforeDispatch })
|
|
3427
3500
|
: undefined);
|
|
@@ -3431,8 +3504,9 @@ class BambuPrinterMCPServer {
|
|
|
3431
3504
|
if (!args?.message_json) {
|
|
3432
3505
|
throw new Error("Missing required parameter: message_json");
|
|
3433
3506
|
}
|
|
3434
|
-
if (!nativeControls) {
|
|
3435
|
-
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());
|
|
3436
3510
|
}
|
|
3437
3511
|
const messageJson = validateBambuNativeMetadata(String(args.message_json));
|
|
3438
3512
|
result = await sendNativeCommand({
|
|
@@ -3448,7 +3522,7 @@ class BambuPrinterMCPServer {
|
|
|
3448
3522
|
|
|
3449
3523
|
case "clear_hms_errors":
|
|
3450
3524
|
result = await this.bambu.clearHmsErrors(host, bambuSerial, bambuToken,
|
|
3451
|
-
nativeControls
|
|
3525
|
+
nativeControls()
|
|
3452
3526
|
? (assertActive, beforeDispatch) => sendNativeCommand({ host, serial: bambuSerial, token: bambuToken,
|
|
3453
3527
|
messageJson: JSON.stringify({ print: { command: "clean_print_error", param: "", sequence_id: String(Date.now()) } }), qos: 1 }, { signal: extra.signal, assertActive, beforeDispatch })
|
|
3454
3528
|
: undefined);
|
|
@@ -3458,7 +3532,7 @@ class BambuPrinterMCPServer {
|
|
|
3458
3532
|
if (!args?.mode) {
|
|
3459
3533
|
throw new Error("Missing required parameter: mode");
|
|
3460
3534
|
}
|
|
3461
|
-
if (nativeControls) {
|
|
3535
|
+
if (nativeControls()) {
|
|
3462
3536
|
const normalized = String(args.mode).trim().toLowerCase();
|
|
3463
3537
|
const mode = normalized === "silent" ? 1
|
|
3464
3538
|
: normalized === "standard" ? 2
|
|
@@ -3485,7 +3559,7 @@ class BambuPrinterMCPServer {
|
|
|
3485
3559
|
if (!args?.mode) {
|
|
3486
3560
|
throw new Error("Missing required parameter: mode");
|
|
3487
3561
|
}
|
|
3488
|
-
if (nativeControls) {
|
|
3562
|
+
if (nativeControls()) {
|
|
3489
3563
|
const mode = String(args.mode).trim().toLowerCase();
|
|
3490
3564
|
if (mode !== "cooling" && mode !== "heating") {
|
|
3491
3565
|
throw new Error("Airduct mode must be one of: cooling, heating.");
|
|
@@ -3512,7 +3586,7 @@ class BambuPrinterMCPServer {
|
|
|
3512
3586
|
if (args?.ams_id === undefined || args?.slot_id === undefined) {
|
|
3513
3587
|
throw new Error("Missing required parameters: ams_id and slot_id");
|
|
3514
3588
|
}
|
|
3515
|
-
if (nativeControls) {
|
|
3589
|
+
if (nativeControls()) {
|
|
3516
3590
|
const amsId = Number(args.ams_id);
|
|
3517
3591
|
const slotId = Number(args.slot_id);
|
|
3518
3592
|
if ((!Number.isInteger(amsId) || (amsId < 0 || amsId > 3)) && amsId !== 128) {
|
|
@@ -3561,9 +3635,14 @@ class BambuPrinterMCPServer {
|
|
|
3561
3635
|
heaterModel || undefined,
|
|
3562
3636
|
args?.material !== undefined ? String(args.material) : undefined,
|
|
3563
3637
|
Number(args?.nozzle_diameter ?? DEFAULT_NOZZLE_DIAMETER),
|
|
3564
|
-
(args.temperature === 0 ? nativeControls : heaterModel === "x2d" &&
|
|
3565
|
-
? (heater, target, assertActive, beforeDispatch) =>
|
|
3566
|
-
|
|
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
|
+
}
|
|
3567
3646
|
: undefined
|
|
3568
3647
|
);
|
|
3569
3648
|
break;
|
|
@@ -3573,7 +3652,7 @@ class BambuPrinterMCPServer {
|
|
|
3573
3652
|
if (!args?.fan || args?.speed === undefined) {
|
|
3574
3653
|
throw new Error("Missing required parameters: fan and speed");
|
|
3575
3654
|
}
|
|
3576
|
-
if (nativeControls) {
|
|
3655
|
+
if (nativeControls()) {
|
|
3577
3656
|
const status = await this.bambu.getStatus(host, bambuSerial, bambuToken);
|
|
3578
3657
|
const printerState = String(status?.raw?.gcode_state ?? status?.raw?.print?.gcode_state ?? status?.gcode_state ?? status?.status ?? "").toUpperCase();
|
|
3579
3658
|
if (printerState === "RUNNING" && args?.confirm_during_print !== true) {
|
|
@@ -3607,7 +3686,7 @@ class BambuPrinterMCPServer {
|
|
|
3607
3686
|
if (!args?.light || !args?.mode) {
|
|
3608
3687
|
throw new Error("Missing required parameters: light and mode");
|
|
3609
3688
|
}
|
|
3610
|
-
if (nativeControls) {
|
|
3689
|
+
if (nativeControls()) {
|
|
3611
3690
|
const light = String(args.light).trim();
|
|
3612
3691
|
const mode = String(args.mode).trim().toLowerCase();
|
|
3613
3692
|
if (light !== "chamber_light" && light !== "chamber_light2") {
|
|
@@ -3648,7 +3727,7 @@ class BambuPrinterMCPServer {
|
|
|
3648
3727
|
if (!Array.isArray(args?.object_ids)) {
|
|
3649
3728
|
throw new Error("Missing required parameter: object_ids");
|
|
3650
3729
|
}
|
|
3651
|
-
if (nativeControls) {
|
|
3730
|
+
if (nativeControls()) {
|
|
3652
3731
|
const objectIds = args.object_ids.map((id: unknown) => Number(id));
|
|
3653
3732
|
if (objectIds.length === 0 || objectIds.some((id: number) => !Number.isInteger(id) || id < 0)) {
|
|
3654
3733
|
throw new Error("object_ids must contain one or more non-negative integers.");
|
|
@@ -3676,7 +3755,7 @@ class BambuPrinterMCPServer {
|
|
|
3676
3755
|
if (!args?.action || args?.ams_id === undefined) {
|
|
3677
3756
|
throw new Error("Missing required parameters: action and ams_id");
|
|
3678
3757
|
}
|
|
3679
|
-
if (nativeControls) {
|
|
3758
|
+
if (nativeControls()) {
|
|
3680
3759
|
const action = String(args.action).trim().toLowerCase();
|
|
3681
3760
|
const amsId = Number(args.ams_id);
|
|
3682
3761
|
if (action !== "start" && action !== "stop") {
|
|
@@ -3808,6 +3887,7 @@ class BambuPrinterMCPServer {
|
|
|
3808
3887
|
if (args?.filament_colours !== undefined) sliceBambuOptions.filamentColours = parseFilamentColours(String(args.filament_colours));
|
|
3809
3888
|
sliceBambuOptions.bedType = resolveBambuStudioCliBedType(args?.bed_type as string | undefined);
|
|
3810
3889
|
sliceBambuOptions.nozzleType = resolveNozzleType(args?.nozzle_type);
|
|
3890
|
+
sliceBambuOptions.nozzleVolumeType = resolveNozzleVolumeType(args?.nozzle_volume_type);
|
|
3811
3891
|
if (args?.enable_timelapse !== undefined) sliceBambuOptions.enableTimelapse = Boolean(args.enable_timelapse);
|
|
3812
3892
|
if (args?.allow_mix_temp !== undefined) sliceBambuOptions.allowMixTemp = Boolean(args.allow_mix_temp);
|
|
3813
3893
|
if (args?.scale !== undefined) sliceBambuOptions.scale = Number(args.scale);
|
|
@@ -3895,6 +3975,7 @@ class BambuPrinterMCPServer {
|
|
|
3895
3975
|
if (args?.filament_colours !== undefined) sliceBambuOptions.filamentColours = parseFilamentColours(String(args.filament_colours));
|
|
3896
3976
|
sliceBambuOptions.bedType = resolveBambuStudioCliBedType(args?.bed_type as string | undefined);
|
|
3897
3977
|
sliceBambuOptions.nozzleType = resolveNozzleType(args?.nozzle_type);
|
|
3978
|
+
sliceBambuOptions.nozzleVolumeType = resolveNozzleVolumeType(args?.nozzle_volume_type);
|
|
3898
3979
|
if (args?.enable_timelapse !== undefined) sliceBambuOptions.enableTimelapse = Boolean(args.enable_timelapse);
|
|
3899
3980
|
if (args?.allow_mix_temp !== undefined) sliceBambuOptions.allowMixTemp = Boolean(args.allow_mix_temp);
|
|
3900
3981
|
if (args?.scale !== undefined) sliceBambuOptions.scale = Number(args.scale);
|
|
@@ -4023,6 +4104,7 @@ class BambuPrinterMCPServer {
|
|
|
4023
4104
|
skipModifiedGcodes: true,
|
|
4024
4105
|
bedType: printBedType,
|
|
4025
4106
|
nozzleType: resolveNozzleType(args?.nozzle_type),
|
|
4107
|
+
nozzleVolumeType: resolveNozzleVolumeType(args?.nozzle_volume_type),
|
|
4026
4108
|
};
|
|
4027
4109
|
if (!explicitSlicerProfile) {
|
|
4028
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);
|