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/dist/index.js
CHANGED
|
@@ -11,7 +11,7 @@ import { createServer as createHttpServer } from "node:http";
|
|
|
11
11
|
import { randomUUID } from "node:crypto";
|
|
12
12
|
import { STLManipulator, SLICER_TYPES, normalizeSlicerType, } from "./stl/stl-manipulator.js";
|
|
13
13
|
import { BambuNetworkBridge } from "./bambu-network-bridge.js";
|
|
14
|
-
import { assertBambuNativeAvailable, validateBambuNativeMetadata, buildBambuNativeFanCommand, buildBambuNativeTemperatureCommand, printWithBambuNative, sendCommandWithBambuNative, uploadWithBambuNative, } from "./bambu-native.js";
|
|
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
17
|
import { hasAmsMappingInput, normalizeAmsMappingObject, normalizeBridgeAmsTrayValue } from "./ams-mapping.js";
|
|
@@ -90,6 +90,20 @@ function resolveNozzleType(value) {
|
|
|
90
90
|
}
|
|
91
91
|
return normalized;
|
|
92
92
|
}
|
|
93
|
+
/** BambuStudio's nozzle_volume_type value for each tool argument value. */
|
|
94
|
+
const NOZZLE_VOLUME_TYPES = { standard: "Standard", high_flow: "High Flow" };
|
|
95
|
+
const VALID_NOZZLE_VOLUME_TYPES = Object.keys(NOZZLE_VOLUME_TYPES);
|
|
96
|
+
/** The installed nozzle's flow type. Bambu machine presets assume Standard; the print gate compares this with the printer. */
|
|
97
|
+
function resolveNozzleVolumeType(value) {
|
|
98
|
+
const raw = value === undefined || value === null || value === "" ? process.env.BAMBU_NOZZLE_VOLUME_TYPE?.trim() : value;
|
|
99
|
+
if (raw === undefined || raw === "")
|
|
100
|
+
return undefined;
|
|
101
|
+
const normalized = String(raw).trim().toLowerCase().replace(/[\s-]+/g, "_");
|
|
102
|
+
if (!VALID_NOZZLE_VOLUME_TYPES.includes(normalized)) {
|
|
103
|
+
throw new Error(`Invalid nozzle volume type "${raw}". Valid types: ${VALID_NOZZLE_VOLUME_TYPES.join(", ")}`);
|
|
104
|
+
}
|
|
105
|
+
return NOZZLE_VOLUME_TYPES[normalized];
|
|
106
|
+
}
|
|
93
107
|
const VALID_BAMBUSTUDIO_CLI_BED_TYPES = ["textured_plate", "cool_plate", "engineering_plate", "hot_plate"];
|
|
94
108
|
// Map model IDs to BambuStudio --load-machine preset names
|
|
95
109
|
const BAMBU_MODEL_PRESETS = {
|
|
@@ -1040,6 +1054,7 @@ class BambuPrinterMCPServer {
|
|
|
1040
1054
|
skipModifiedGcodes: true,
|
|
1041
1055
|
bedType: bedType ? resolveBambuStudioCliBedType(bedType) : undefined,
|
|
1042
1056
|
nozzleType: resolveNozzleType(args?.nozzle_type),
|
|
1057
|
+
nozzleVolumeType: resolveNozzleVolumeType(args?.nozzle_volume_type),
|
|
1043
1058
|
};
|
|
1044
1059
|
threeMFPath = await this.stlManipulator.sliceSTL(threeMFPath, slicerType, slicerPath, slicerProfile || undefined, undefined, printPreset, autoSliceOptions);
|
|
1045
1060
|
console.error("Auto-sliced to: " + threeMFPath);
|
|
@@ -1326,12 +1341,14 @@ class BambuPrinterMCPServer {
|
|
|
1326
1341
|
return this.print3mfViaBambuNetwork({ ...args, bambu_model: model, connection_type: "lan", bambu_network_method: "start_local_print" }, host, bambuSerial, bambuToken, { onUpdate, signal });
|
|
1327
1342
|
}
|
|
1328
1343
|
async uploadFileViaBambuNative(args, host, bambuSerial, bambuToken, onUpdate, signal) {
|
|
1329
|
-
assertBambuNativeAvailable();
|
|
1330
1344
|
if (args.print)
|
|
1331
1345
|
throw new Error("Native upload never starts printing; use print_3mf.");
|
|
1346
|
+
// Resolve and enforce the model first, as the print path does, so a
|
|
1347
|
+
// non-X2D request is never told to install the native helper.
|
|
1332
1348
|
const model = await this.resolveBambuModel(args.bambu_model);
|
|
1333
1349
|
if (model !== "x2d")
|
|
1334
1350
|
throw new Error("connection_mode=bambu_native is restricted to X2D.");
|
|
1351
|
+
assertBambuNativeAvailable();
|
|
1335
1352
|
const unsupported = ["use_ams", "ams_mapping", "ams_mapping2", "ams_mapping_info", "nozzle_mapping", "nozzles_info",
|
|
1336
1353
|
"bed_leveling", "flow_calibration", "vibration_calibration", "layer_inspect", "timelapse"];
|
|
1337
1354
|
for (const key of unsupported) {
|
|
@@ -1841,6 +1858,11 @@ class BambuPrinterMCPServer {
|
|
|
1841
1858
|
enum: [...VALID_NOZZLE_TYPES],
|
|
1842
1859
|
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."
|
|
1843
1860
|
},
|
|
1861
|
+
nozzle_volume_type: {
|
|
1862
|
+
type: "string",
|
|
1863
|
+
enum: [...VALID_NOZZLE_VOLUME_TYPES],
|
|
1864
|
+
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."
|
|
1865
|
+
},
|
|
1844
1866
|
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." },
|
|
1845
1867
|
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
1846
1868
|
bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
|
|
@@ -1896,6 +1918,11 @@ class BambuPrinterMCPServer {
|
|
|
1896
1918
|
enum: [...VALID_NOZZLE_TYPES],
|
|
1897
1919
|
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."
|
|
1898
1920
|
},
|
|
1921
|
+
nozzle_volume_type: {
|
|
1922
|
+
type: "string",
|
|
1923
|
+
enum: [...VALID_NOZZLE_VOLUME_TYPES],
|
|
1924
|
+
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."
|
|
1925
|
+
},
|
|
1899
1926
|
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." },
|
|
1900
1927
|
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." },
|
|
1901
1928
|
repetitions: { type: "number", description: "Print N identical copies of the model. Each copy gets its own plate placement. Example: 3 prints three copies." },
|
|
@@ -2002,6 +2029,7 @@ class BambuPrinterMCPServer {
|
|
|
2002
2029
|
password: { type: "string", description: "Printer password/access code override for LAN/local bridge methods." },
|
|
2003
2030
|
bed_type: { type: "string", enum: ["textured_plate", "cool_plate", "engineering_plate", "hot_plate", "supertack_plate"], description: "Bed plate type currently installed (default: textured_plate)." },
|
|
2004
2031
|
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)." },
|
|
2032
|
+
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)." },
|
|
2005
2033
|
plate_index: { type: "number", description: "Zero-based plate index to print from the sliced 3MF; converted to FULU's one-based PrintParams plate_index." },
|
|
2006
2034
|
project_name: { type: "string", description: "Optional project name sent in FULU PrintParams; defaults to the 3MF filename without extension." },
|
|
2007
2035
|
preset_name: { type: "string", description: "Optional preset name sent in FULU PrintParams; defaults to project plus one-based plate index." },
|
|
@@ -2265,7 +2293,7 @@ class BambuPrinterMCPServer {
|
|
|
2265
2293
|
},
|
|
2266
2294
|
{
|
|
2267
2295
|
name: "reread_ams_rfid",
|
|
2268
|
-
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.",
|
|
2296
|
+
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.",
|
|
2269
2297
|
inputSchema: {
|
|
2270
2298
|
type: "object",
|
|
2271
2299
|
properties: {
|
|
@@ -2298,7 +2326,7 @@ class BambuPrinterMCPServer {
|
|
|
2298
2326
|
},
|
|
2299
2327
|
{
|
|
2300
2328
|
name: "set_fan_speed",
|
|
2301
|
-
description: "Set a Bambu printer fan speed percentage. X2D on macOS
|
|
2329
|
+
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.",
|
|
2302
2330
|
inputSchema: {
|
|
2303
2331
|
type: "object",
|
|
2304
2332
|
properties: {
|
|
@@ -2357,7 +2385,7 @@ class BambuPrinterMCPServer {
|
|
|
2357
2385
|
},
|
|
2358
2386
|
{
|
|
2359
2387
|
name: "set_ams_drying",
|
|
2360
|
-
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.",
|
|
2388
|
+
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.",
|
|
2361
2389
|
inputSchema: {
|
|
2362
2390
|
type: "object",
|
|
2363
2391
|
properties: {
|
|
@@ -2429,6 +2457,11 @@ class BambuPrinterMCPServer {
|
|
|
2429
2457
|
enum: [...VALID_NOZZLE_TYPES],
|
|
2430
2458
|
description: "Installed nozzle material, used when the 3MF must be auto-sliced (default: BAMBU_NOZZLE_TYPE, else the preset's stock nozzle)."
|
|
2431
2459
|
},
|
|
2460
|
+
nozzle_volume_type: {
|
|
2461
|
+
type: "string",
|
|
2462
|
+
enum: [...VALID_NOZZLE_VOLUME_TYPES],
|
|
2463
|
+
description: "Installed nozzle flow type, used when the 3MF must be auto-sliced (default: BAMBU_NOZZLE_VOLUME_TYPE, else standard)."
|
|
2464
|
+
},
|
|
2432
2465
|
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
2433
2466
|
bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
|
|
2434
2467
|
bambu_token: { type: "string", description: "Access token (default: value from env)" },
|
|
@@ -2618,9 +2651,42 @@ class BambuPrinterMCPServer {
|
|
|
2618
2651
|
const host = String(args?.host || DEFAULT_HOST);
|
|
2619
2652
|
const bambuSerial = String(args?.bambu_serial || DEFAULT_BAMBU_SERIAL);
|
|
2620
2653
|
const bambuToken = String(args?.bambu_token || DEFAULT_BAMBU_TOKEN);
|
|
2621
|
-
const
|
|
2622
|
-
(
|
|
2623
|
-
|
|
2654
|
+
const x2dTarget = bambuSerial.trim().toUpperCase().startsWith("20P") ||
|
|
2655
|
+
(DEFAULT_BAMBU_MODEL === "x2d" && bambuSerial === DEFAULT_BAMBU_SERIAL);
|
|
2656
|
+
// Resolved lazily: the Linux availability check reads files, and most
|
|
2657
|
+
// tools (status, slicing, STL) never need it.
|
|
2658
|
+
// One resolution per request; null means native X2D controls are usable.
|
|
2659
|
+
let nativeUnavailable;
|
|
2660
|
+
const nativeUnavailableReason = () => (nativeUnavailable ?? (nativeUnavailable = x2dTarget ? bambuNativeUnavailableReason() ?? null : "The selected printer is not an X2D."));
|
|
2661
|
+
const nativeControls = () => nativeUnavailableReason() === null;
|
|
2662
|
+
// Linux sent X2D stop and heater-off over MQTT before the native route
|
|
2663
|
+
// existed. Send them over both routes there: a stuck helper must not
|
|
2664
|
+
// delay an emergency control, and a repeated stop or heater-off is
|
|
2665
|
+
// harmless. The immediate MQTT send can precede the exit of an older
|
|
2666
|
+
// print or heating helper that then still dispatches, so MQTT is sent
|
|
2667
|
+
// again once every older helper for this printer has exited, and only
|
|
2668
|
+
// that send counts. Either counted route succeeding is success. macOS
|
|
2669
|
+
// stays native-only.
|
|
2670
|
+
const linuxEmergency = (native, mqtt) => async () => {
|
|
2671
|
+
if (process.platform !== "linux")
|
|
2672
|
+
return native();
|
|
2673
|
+
const [nativeResult, immediateResult, mqttResult] = await Promise.allSettled([
|
|
2674
|
+
native(),
|
|
2675
|
+
mqtt(),
|
|
2676
|
+
interruptNativeOperations(bambuSerial).then(() => mqtt()),
|
|
2677
|
+
]);
|
|
2678
|
+
const reason = (result) => result.status === "rejected" ? (result.reason instanceof Error ? result.reason.message : String(result.reason)) : undefined;
|
|
2679
|
+
if (nativeResult.status === "rejected" && mqttResult.status === "rejected") {
|
|
2680
|
+
throw new Error(`native route: ${reason(nativeResult)}; MQTT route: ${reason(mqttResult)}`);
|
|
2681
|
+
}
|
|
2682
|
+
const state = (result) => result.status === "fulfilled" ? "sent" : "failed";
|
|
2683
|
+
return {
|
|
2684
|
+
status: "success",
|
|
2685
|
+
routes: { native: state(nativeResult), mqtt: state(mqttResult), mqtt_immediate: state(immediateResult) },
|
|
2686
|
+
...(reason(nativeResult) ? { native_error: reason(nativeResult) } : {}),
|
|
2687
|
+
...(reason(mqttResult) ? { mqtt_error: reason(mqttResult) } : {}),
|
|
2688
|
+
};
|
|
2689
|
+
};
|
|
2624
2690
|
const requestedTemplateDir = typeof args?.template_dir === "string" && args.template_dir.trim().length > 0
|
|
2625
2691
|
? String(args.template_dir)
|
|
2626
2692
|
: undefined;
|
|
@@ -2822,19 +2888,19 @@ class BambuPrinterMCPServer {
|
|
|
2822
2888
|
break;
|
|
2823
2889
|
}
|
|
2824
2890
|
case "cancel_print":
|
|
2825
|
-
result = await this.bambu.cancelJob(host, bambuSerial, bambuToken, nativeControls
|
|
2826
|
-
? () => sendNativeCommand({ host, serial: bambuSerial, token: bambuToken,
|
|
2827
|
-
messageJson: JSON.stringify({ print: { command: "stop", param: "", sequence_id: String(Date.now()) } }), qos: 1 })
|
|
2891
|
+
result = await this.bambu.cancelJob(host, bambuSerial, bambuToken, nativeControls()
|
|
2892
|
+
? linuxEmergency(() => sendNativeCommand({ host, serial: bambuSerial, token: bambuToken,
|
|
2893
|
+
messageJson: JSON.stringify({ print: { command: "stop", param: "", sequence_id: String(Date.now()) } }), qos: 1 }), () => this.bambu.sendStopOverMqtt(host, bambuSerial, bambuToken))
|
|
2828
2894
|
: undefined);
|
|
2829
2895
|
break;
|
|
2830
2896
|
case "pause_print":
|
|
2831
|
-
result = await this.bambu.pauseJob(host, bambuSerial, bambuToken, nativeControls
|
|
2897
|
+
result = await this.bambu.pauseJob(host, bambuSerial, bambuToken, nativeControls()
|
|
2832
2898
|
? () => sendNativeCommand({ host, serial: bambuSerial, token: bambuToken,
|
|
2833
2899
|
messageJson: JSON.stringify({ print: { command: "pause", param: "", sequence_id: String(Date.now()) } }), qos: 1 })
|
|
2834
2900
|
: undefined);
|
|
2835
2901
|
break;
|
|
2836
2902
|
case "resume_print":
|
|
2837
|
-
result = await this.bambu.resumeJob(host, bambuSerial, bambuToken, nativeControls
|
|
2903
|
+
result = await this.bambu.resumeJob(host, bambuSerial, bambuToken, nativeControls()
|
|
2838
2904
|
? (assertActive, beforeDispatch) => sendNativeCommand({ host, serial: bambuSerial, token: bambuToken,
|
|
2839
2905
|
messageJson: JSON.stringify({ print: { command: "resume", param: "", sequence_id: String(Date.now()) } }), qos: 1 }, { signal: extra.signal, assertActive, beforeDispatch })
|
|
2840
2906
|
: undefined);
|
|
@@ -2843,8 +2909,9 @@ class BambuPrinterMCPServer {
|
|
|
2843
2909
|
if (!args?.message_json) {
|
|
2844
2910
|
throw new Error("Missing required parameter: message_json");
|
|
2845
2911
|
}
|
|
2846
|
-
if (!nativeControls) {
|
|
2847
|
-
throw new Error("x2d_native_control is restricted to a selected X2D printer on macOS."
|
|
2912
|
+
if (!nativeControls()) {
|
|
2913
|
+
throw new Error("x2d_native_control is restricted to a selected X2D printer on macOS, or on Linux with the native helper installed. " +
|
|
2914
|
+
nativeUnavailableReason());
|
|
2848
2915
|
}
|
|
2849
2916
|
const messageJson = validateBambuNativeMetadata(String(args.message_json));
|
|
2850
2917
|
result = await sendNativeCommand({
|
|
@@ -2858,7 +2925,7 @@ class BambuPrinterMCPServer {
|
|
|
2858
2925
|
break;
|
|
2859
2926
|
}
|
|
2860
2927
|
case "clear_hms_errors":
|
|
2861
|
-
result = await this.bambu.clearHmsErrors(host, bambuSerial, bambuToken, nativeControls
|
|
2928
|
+
result = await this.bambu.clearHmsErrors(host, bambuSerial, bambuToken, nativeControls()
|
|
2862
2929
|
? (assertActive, beforeDispatch) => sendNativeCommand({ host, serial: bambuSerial, token: bambuToken,
|
|
2863
2930
|
messageJson: JSON.stringify({ print: { command: "clean_print_error", param: "", sequence_id: String(Date.now()) } }), qos: 1 }, { signal: extra.signal, assertActive, beforeDispatch })
|
|
2864
2931
|
: undefined);
|
|
@@ -2867,7 +2934,7 @@ class BambuPrinterMCPServer {
|
|
|
2867
2934
|
if (!args?.mode) {
|
|
2868
2935
|
throw new Error("Missing required parameter: mode");
|
|
2869
2936
|
}
|
|
2870
|
-
if (nativeControls) {
|
|
2937
|
+
if (nativeControls()) {
|
|
2871
2938
|
const normalized = String(args.mode).trim().toLowerCase();
|
|
2872
2939
|
const mode = normalized === "silent" ? 1
|
|
2873
2940
|
: normalized === "standard" ? 2
|
|
@@ -2894,7 +2961,7 @@ class BambuPrinterMCPServer {
|
|
|
2894
2961
|
if (!args?.mode) {
|
|
2895
2962
|
throw new Error("Missing required parameter: mode");
|
|
2896
2963
|
}
|
|
2897
|
-
if (nativeControls) {
|
|
2964
|
+
if (nativeControls()) {
|
|
2898
2965
|
const mode = String(args.mode).trim().toLowerCase();
|
|
2899
2966
|
if (mode !== "cooling" && mode !== "heating") {
|
|
2900
2967
|
throw new Error("Airduct mode must be one of: cooling, heating.");
|
|
@@ -2921,7 +2988,7 @@ class BambuPrinterMCPServer {
|
|
|
2921
2988
|
if (args?.ams_id === undefined || args?.slot_id === undefined) {
|
|
2922
2989
|
throw new Error("Missing required parameters: ams_id and slot_id");
|
|
2923
2990
|
}
|
|
2924
|
-
if (nativeControls) {
|
|
2991
|
+
if (nativeControls()) {
|
|
2925
2992
|
const amsId = Number(args.ams_id);
|
|
2926
2993
|
const slotId = Number(args.slot_id);
|
|
2927
2994
|
if ((!Number.isInteger(amsId) || (amsId < 0 || amsId > 3)) && amsId !== 128) {
|
|
@@ -2958,9 +3025,14 @@ class BambuPrinterMCPServer {
|
|
|
2958
3025
|
const heaterModel = args.temperature === 0
|
|
2959
3026
|
? String(args.bambu_model || DEFAULT_BAMBU_MODEL).trim().toLowerCase()
|
|
2960
3027
|
: await this.resolveBambuModel(args.bambu_model);
|
|
2961
|
-
result = await this.bambu.setTemperature(host, bambuSerial, bambuToken, String(args.component), args.temperature, heaterModel || undefined, args?.material !== undefined ? String(args.material) : undefined, Number(args?.nozzle_diameter ?? DEFAULT_NOZZLE_DIAMETER), (args.temperature === 0 ? nativeControls : heaterModel === "x2d" &&
|
|
2962
|
-
? (heater, target, assertActive, beforeDispatch) =>
|
|
2963
|
-
|
|
3028
|
+
result = await this.bambu.setTemperature(host, bambuSerial, bambuToken, String(args.component), args.temperature, heaterModel || undefined, args?.material !== undefined ? String(args.material) : undefined, Number(args?.nozzle_diameter ?? DEFAULT_NOZZLE_DIAMETER), (args.temperature === 0 ? nativeControls() : heaterModel === "x2d" && bambuNativeControlsAvailable())
|
|
3029
|
+
? (heater, target, assertActive, beforeDispatch) => {
|
|
3030
|
+
const native = () => sendNativeCommand({ host, serial: bambuSerial, token: bambuToken,
|
|
3031
|
+
messageJson: buildBambuNativeTemperatureCommand(heater, target).messageJson }, { signal: extra.signal, assertActive, beforeDispatch });
|
|
3032
|
+
return target === 0
|
|
3033
|
+
? linuxEmergency(native, () => this.bambu.sendHeaterOffOverMqtt(host, bambuSerial, bambuToken, heater))()
|
|
3034
|
+
: native();
|
|
3035
|
+
}
|
|
2964
3036
|
: undefined);
|
|
2965
3037
|
break;
|
|
2966
3038
|
}
|
|
@@ -2968,7 +3040,7 @@ class BambuPrinterMCPServer {
|
|
|
2968
3040
|
if (!args?.fan || args?.speed === undefined) {
|
|
2969
3041
|
throw new Error("Missing required parameters: fan and speed");
|
|
2970
3042
|
}
|
|
2971
|
-
if (nativeControls) {
|
|
3043
|
+
if (nativeControls()) {
|
|
2972
3044
|
const status = await this.bambu.getStatus(host, bambuSerial, bambuToken);
|
|
2973
3045
|
const printerState = String(status?.raw?.gcode_state ?? status?.raw?.print?.gcode_state ?? status?.gcode_state ?? status?.status ?? "").toUpperCase();
|
|
2974
3046
|
if (printerState === "RUNNING" && args?.confirm_during_print !== true) {
|
|
@@ -2996,7 +3068,7 @@ class BambuPrinterMCPServer {
|
|
|
2996
3068
|
if (!args?.light || !args?.mode) {
|
|
2997
3069
|
throw new Error("Missing required parameters: light and mode");
|
|
2998
3070
|
}
|
|
2999
|
-
if (nativeControls) {
|
|
3071
|
+
if (nativeControls()) {
|
|
3000
3072
|
const light = String(args.light).trim();
|
|
3001
3073
|
const mode = String(args.mode).trim().toLowerCase();
|
|
3002
3074
|
if (light !== "chamber_light" && light !== "chamber_light2") {
|
|
@@ -3031,7 +3103,7 @@ class BambuPrinterMCPServer {
|
|
|
3031
3103
|
if (!Array.isArray(args?.object_ids)) {
|
|
3032
3104
|
throw new Error("Missing required parameter: object_ids");
|
|
3033
3105
|
}
|
|
3034
|
-
if (nativeControls) {
|
|
3106
|
+
if (nativeControls()) {
|
|
3035
3107
|
const objectIds = args.object_ids.map((id) => Number(id));
|
|
3036
3108
|
if (objectIds.length === 0 || objectIds.some((id) => !Number.isInteger(id) || id < 0)) {
|
|
3037
3109
|
throw new Error("object_ids must contain one or more non-negative integers.");
|
|
@@ -3054,7 +3126,7 @@ class BambuPrinterMCPServer {
|
|
|
3054
3126
|
if (!args?.action || args?.ams_id === undefined) {
|
|
3055
3127
|
throw new Error("Missing required parameters: action and ams_id");
|
|
3056
3128
|
}
|
|
3057
|
-
if (nativeControls) {
|
|
3129
|
+
if (nativeControls()) {
|
|
3058
3130
|
const action = String(args.action).trim().toLowerCase();
|
|
3059
3131
|
const amsId = Number(args.ams_id);
|
|
3060
3132
|
if (action !== "start" && action !== "stop") {
|
|
@@ -3164,6 +3236,7 @@ class BambuPrinterMCPServer {
|
|
|
3164
3236
|
sliceBambuOptions.filamentColours = parseFilamentColours(String(args.filament_colours));
|
|
3165
3237
|
sliceBambuOptions.bedType = resolveBambuStudioCliBedType(args?.bed_type);
|
|
3166
3238
|
sliceBambuOptions.nozzleType = resolveNozzleType(args?.nozzle_type);
|
|
3239
|
+
sliceBambuOptions.nozzleVolumeType = resolveNozzleVolumeType(args?.nozzle_volume_type);
|
|
3167
3240
|
if (args?.enable_timelapse !== undefined)
|
|
3168
3241
|
sliceBambuOptions.enableTimelapse = Boolean(args.enable_timelapse);
|
|
3169
3242
|
if (args?.allow_mix_temp !== undefined)
|
|
@@ -3244,6 +3317,7 @@ class BambuPrinterMCPServer {
|
|
|
3244
3317
|
sliceBambuOptions.filamentColours = parseFilamentColours(String(args.filament_colours));
|
|
3245
3318
|
sliceBambuOptions.bedType = resolveBambuStudioCliBedType(args?.bed_type);
|
|
3246
3319
|
sliceBambuOptions.nozzleType = resolveNozzleType(args?.nozzle_type);
|
|
3320
|
+
sliceBambuOptions.nozzleVolumeType = resolveNozzleVolumeType(args?.nozzle_volume_type);
|
|
3247
3321
|
if (args?.enable_timelapse !== undefined)
|
|
3248
3322
|
sliceBambuOptions.enableTimelapse = Boolean(args.enable_timelapse);
|
|
3249
3323
|
if (args?.allow_mix_temp !== undefined)
|
|
@@ -3347,6 +3421,7 @@ class BambuPrinterMCPServer {
|
|
|
3347
3421
|
skipModifiedGcodes: true,
|
|
3348
3422
|
bedType: printBedType,
|
|
3349
3423
|
nozzleType: resolveNozzleType(args?.nozzle_type),
|
|
3424
|
+
nozzleVolumeType: resolveNozzleVolumeType(args?.nozzle_volume_type),
|
|
3350
3425
|
};
|
|
3351
3426
|
if (!explicitSlicerProfile) {
|
|
3352
3427
|
try {
|
package/dist/printers/bambu.d.ts
CHANGED
|
@@ -26,6 +26,13 @@ interface BambuPrintOptionsInternal {
|
|
|
26
26
|
amsSlots?: number[];
|
|
27
27
|
md5?: string;
|
|
28
28
|
}
|
|
29
|
+
/** What the human confirmed in confirmPrintPreflight, checked again before dispatch. */
|
|
30
|
+
export type PrintPreflight = {
|
|
31
|
+
finishedJob?: string;
|
|
32
|
+
serial: string;
|
|
33
|
+
model: string;
|
|
34
|
+
notices: string[];
|
|
35
|
+
};
|
|
29
36
|
export declare class BambuImplementation {
|
|
30
37
|
private readonly confirm?;
|
|
31
38
|
private printerStore;
|
|
@@ -34,8 +41,14 @@ export declare class BambuImplementation {
|
|
|
34
41
|
constructor(confirm?: ((message: string) => Promise<boolean>) | undefined);
|
|
35
42
|
confirmHardwareAction(message: string, physicalCheck?: boolean): Promise<void>;
|
|
36
43
|
private finishedJobIdentity;
|
|
37
|
-
|
|
38
|
-
|
|
44
|
+
/**
|
|
45
|
+
* The human print confirmation. Its result goes to assertBedClearance at
|
|
46
|
+
* every later state check of the same print, which stops the print if the
|
|
47
|
+
* situation the human confirmed has changed.
|
|
48
|
+
*/
|
|
49
|
+
confirmPrintPreflight(serial: string, status: any, inspection: Awaited<ReturnType<typeof inspectPrintFile>>): Promise<PrintPreflight>;
|
|
50
|
+
/** Stop a print whose confirmed situation changed: a newly finished job, or a hidden HMS notice the preflight did not name. */
|
|
51
|
+
assertBedClearance(status: any, preflight: PrintPreflight): void;
|
|
39
52
|
private getPrinter;
|
|
40
53
|
/** Internal handoff from a successful inspected transport; never exposed as an MCP tool. */
|
|
41
54
|
recordCheckedJob(host: string, serial: string, remotePath: string, requirements: PrinterStateRequirements): void;
|
|
@@ -47,6 +60,10 @@ export declare class BambuImplementation {
|
|
|
47
60
|
print3mf(host: string, serial: string, token: string, options: BambuPrintOptionsInternal): Promise<any>;
|
|
48
61
|
private print3mfPrepared;
|
|
49
62
|
cancelJob(host: string, serial: string, token: string, dispatch?: () => Promise<any>): Promise<any>;
|
|
63
|
+
/** Raw MQTT stop for routes that already ran the shared cancellation steps. */
|
|
64
|
+
sendStopOverMqtt(host: string, serial: string, token: string): Promise<any>;
|
|
65
|
+
/** Raw MQTT heater-off; switching off needs no material or model declaration. */
|
|
66
|
+
sendHeaterOffOverMqtt(host: string, serial: string, token: string, heater: "bed" | "nozzle"): Promise<any>;
|
|
50
67
|
pauseJob(host: string, serial: string, token: string, dispatch?: () => Promise<any>): Promise<any>;
|
|
51
68
|
resumeJob(host: string, serial: string, token: string, dispatch?: (assertActive: () => void, beforeDispatch: () => Promise<void>) => Promise<any>): Promise<any>;
|
|
52
69
|
clearHmsErrors(host: string, serial: string, token: string, dispatch?: (assertActive: () => void, beforeDispatch: () => Promise<void>) => Promise<any>): Promise<any>;
|
|
@@ -54,6 +71,16 @@ export declare class BambuImplementation {
|
|
|
54
71
|
setAirductMode(host: string, serial: string, token: string, mode: string): Promise<any>;
|
|
55
72
|
rereadAmsRfid(host: string, serial: string, token: string, amsId: number, slotId: number): Promise<any>;
|
|
56
73
|
setTemperature(host: string, serial: string, token: string, component: string, temperature: unknown, bambuModel?: string, material?: string, nozzleDiameter?: number, dispatch?: (heater: "bed" | "nozzle", target: number, assertActive: () => void, beforeDispatch?: () => Promise<void>) => Promise<any>): Promise<{
|
|
74
|
+
routes: any;
|
|
75
|
+
native_error: any;
|
|
76
|
+
mqtt_error: any;
|
|
77
|
+
status: string;
|
|
78
|
+
message: string;
|
|
79
|
+
command: string;
|
|
80
|
+
} | {
|
|
81
|
+
routes?: undefined;
|
|
82
|
+
native_error?: undefined;
|
|
83
|
+
mqtt_error?: undefined;
|
|
57
84
|
status: string;
|
|
58
85
|
message: string;
|
|
59
86
|
command: string;
|
package/dist/printers/bambu.js
CHANGED
|
@@ -8,7 +8,7 @@ import { Readable } from "node:stream";
|
|
|
8
8
|
import { readSafe3mfArchive } from "../safety/archive.js";
|
|
9
9
|
import { inspectPrintFile } from "../safety/print-file.js";
|
|
10
10
|
import { normalizeModel, validateTemperature } from "../safety/limits.js";
|
|
11
|
-
import { validatePrinterState, manualHeatingRequirements } from "../safety/printer-state.js";
|
|
11
|
+
import { describeHmsKey, hiddenHmsKeys, hiddenHmsNotices, reportedHmsKey, validatePrinterState, manualHeatingRequirements } 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";
|
|
@@ -69,7 +69,7 @@ const MODEL_ID_TO_NAME = {
|
|
|
69
69
|
const H2_MODEL_NAMES = new Set(["h2", "h2c", "h2d", "h2dpro", "h2d pro", "h2s", "x2d"]);
|
|
70
70
|
export function assertDirectPrintSupported(model, serial) {
|
|
71
71
|
if (model?.trim().toLowerCase() === "x2d" || serial?.trim().toUpperCase().startsWith("20P")) {
|
|
72
|
-
throw new Error("X2D direct printing is not supported through legacy MQTT/FTPS. Use the
|
|
72
|
+
throw new Error("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). " +
|
|
73
73
|
"X2D status and slicing remain available on all supported platforms.");
|
|
74
74
|
}
|
|
75
75
|
}
|
|
@@ -392,21 +392,37 @@ export class BambuImplementation {
|
|
|
392
392
|
finishedJobIdentity(status) {
|
|
393
393
|
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;
|
|
394
394
|
}
|
|
395
|
+
/**
|
|
396
|
+
* The human print confirmation. Its result goes to assertBedClearance at
|
|
397
|
+
* every later state check of the same print, which stops the print if the
|
|
398
|
+
* situation the human confirmed has changed.
|
|
399
|
+
*/
|
|
395
400
|
async confirmPrintPreflight(serial, status, inspection) {
|
|
396
401
|
const finishedJob = this.finishedJobIdentity(status);
|
|
397
402
|
const cleared = this.clearedErrors.get(serial);
|
|
403
|
+
const notices = hiddenHmsNotices(status?.raw, serial, inspection.model);
|
|
398
404
|
await this.confirmHardwareAction(`Start a checked print on ${inspection.model.toUpperCase()} (${serial})? Nozzles: ${inspection.nozzleDiameters.join(", ")} mm. ` +
|
|
399
405
|
`Materials: ${inspection.materials.join(", ")}. Peak targets: nozzle ${inspection.maxNozzleTemperature}°C, bed ${inspection.maxBedTemperature}°C, chamber ${inspection.maxChamberTemperature}°C. ` +
|
|
400
406
|
`File SHA-256: ${inspection.sha256}. Confirm the physical spool labels and that the build plate is clear.` +
|
|
401
407
|
(finishedJob !== undefined ? " The printer reports FINISH: remove the previous part and debris before confirming." : "") +
|
|
402
|
-
(cleared ? ` Previously cleared hardware codes: ${cleared.join(", ")}. Confirm their physical causes have been resolved.` : "")
|
|
403
|
-
|
|
408
|
+
(cleared ? ` Previously cleared hardware codes: ${cleared.map(describeHmsKey).join(", ")}. Confirm their physical causes have been resolved.` : "") +
|
|
409
|
+
(notices.length ? ` The printer also reports ${notices.join(", ")}, which Bambu lists for this model without text and never shows as a fault; ` +
|
|
410
|
+
`${notices.length === 1 ? "it does" : "they do"} not block printing.` : ""), finishedJob !== undefined || !!cleared);
|
|
411
|
+
return { finishedJob, serial, model: inspection.model, notices };
|
|
404
412
|
}
|
|
405
|
-
|
|
413
|
+
/** Stop a print whose confirmed situation changed: a newly finished job, or a hidden HMS notice the preflight did not name. */
|
|
414
|
+
assertBedClearance(status, preflight) {
|
|
406
415
|
const current = this.finishedJobIdentity(status);
|
|
407
|
-
if (current !== undefined && current !==
|
|
416
|
+
if (current !== undefined && current !== preflight.finishedJob) {
|
|
408
417
|
throw new Error("Printer reports a newly finished job. Confirm that its part and debris have been removed before retrying the print.");
|
|
409
418
|
}
|
|
419
|
+
// An X2D raises its hidden camera notice about 15 seconds after boot, so it
|
|
420
|
+
// can appear after the human confirmed. Never pass a notice nobody was told about.
|
|
421
|
+
const unnamed = hiddenHmsNotices(status?.raw, preflight.serial, preflight.model).filter(notice => !preflight.notices.includes(notice));
|
|
422
|
+
if (unnamed.length) {
|
|
423
|
+
throw new Error(`Printer reported ${unnamed.join(", ")} after the print confirmation. ` +
|
|
424
|
+
"Bambu hides it and it does not block printing, but it was not named when you confirmed; start the print again to review it.");
|
|
425
|
+
}
|
|
410
426
|
}
|
|
411
427
|
async getPrinter(host, serial, token) {
|
|
412
428
|
return this.printerStore.getPrinter(host, serial, token);
|
|
@@ -822,6 +838,19 @@ export class BambuImplementation {
|
|
|
822
838
|
throw new Error(`Failed to cancel print: ${error.message}`);
|
|
823
839
|
}
|
|
824
840
|
}
|
|
841
|
+
/** Raw MQTT stop for routes that already ran the shared cancellation steps. */
|
|
842
|
+
async sendStopOverMqtt(host, serial, token) {
|
|
843
|
+
const printer = await this.getPrinter(host, serial, token);
|
|
844
|
+
await invokeWithoutAck(printer, new UpdateStateCommand({ state: "stop" }));
|
|
845
|
+
return { status: "success", message: "Cancel command sent over MQTT." };
|
|
846
|
+
}
|
|
847
|
+
/** Raw MQTT heater-off; switching off needs no material or model declaration. */
|
|
848
|
+
async sendHeaterOffOverMqtt(host, serial, token, heater) {
|
|
849
|
+
const printer = await this.getPrinter(host, serial, token);
|
|
850
|
+
const gcode = `${heater === "bed" ? "M140" : "M104"} S0`;
|
|
851
|
+
await invokeWithoutAck(printer, new GCodeLineCommand({ gcodes: [gcode] }));
|
|
852
|
+
return { status: "success", message: `Heater-off sent over MQTT for ${heater}.`, command: gcode };
|
|
853
|
+
}
|
|
825
854
|
async pauseJob(host, serial, token, dispatch) {
|
|
826
855
|
const printer = dispatch ? undefined : await this.getPrinter(host, serial, token);
|
|
827
856
|
try {
|
|
@@ -867,10 +896,12 @@ export class BambuImplementation {
|
|
|
867
896
|
const codesFor = (status) => {
|
|
868
897
|
if (!Array.isArray(status.raw?.hms) || status.raw?.print_error === undefined)
|
|
869
898
|
throw new Error("Fresh error codes are required before clearing hardware errors.");
|
|
870
|
-
|
|
899
|
+
// Hidden notices never block and survive clean_print_error, so they are not errors to clear or re-acknowledge.
|
|
900
|
+
const hidden = new Set(hiddenHmsKeys(status.raw, serial, String(status.model ?? "")));
|
|
901
|
+
return [`print_error:${status.raw.print_error}`, ...status.raw.hms.map(reportedHmsKey).filter((key) => !hidden.has(key))].sort();
|
|
871
902
|
};
|
|
872
903
|
const codes = codesFor(await this.getSafetyStatus(host, serial, token));
|
|
873
|
-
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);
|
|
904
|
+
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);
|
|
874
905
|
const beforeDispatch = async () => {
|
|
875
906
|
assertActive();
|
|
876
907
|
const current = codesFor(await this.getSafetyStatus(host, serial, token));
|
|
@@ -1001,11 +1032,13 @@ export class BambuImplementation {
|
|
|
1001
1032
|
}
|
|
1002
1033
|
const printer = dispatch ? undefined : await this.getPrinter(host, serial, token);
|
|
1003
1034
|
assertActive();
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
else
|
|
1035
|
+
const dispatched = dispatch ? await dispatch(heater, targetTemperature, assertActive, beforeDispatch) : undefined;
|
|
1036
|
+
if (!dispatch)
|
|
1007
1037
|
await invokeWithoutAck(printer, new GCodeLineCommand({ gcodes: [gcode] }));
|
|
1008
|
-
|
|
1038
|
+
// Keep route details from a dual-route Linux heater-off.
|
|
1039
|
+
const routeDetails = dispatched && typeof dispatched === "object" && "routes" in dispatched
|
|
1040
|
+
? { routes: dispatched.routes, native_error: dispatched.native_error, mqtt_error: dispatched.mqtt_error } : {};
|
|
1041
|
+
return { status: "success", message: `Temperature command sent for ${normalizedComponent}.`, command: gcode, ...routeDetails };
|
|
1009
1042
|
};
|
|
1010
1043
|
if (targetTemperature === 0) {
|
|
1011
1044
|
cancelPendingPrinterOperations(host, serial);
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export declare const HIDDEN_HMS_SOURCE: {
|
|
2
|
+
readonly studioTag: "v02.08.02.61";
|
|
3
|
+
readonly studioCommit: "926a7192574bcb9b3a732e1ec59a46d79cb45466";
|
|
4
|
+
/** Per table: Bambu's device_hms version and the SHA-256 of the English file used. */
|
|
5
|
+
readonly tables: {
|
|
6
|
+
readonly "093": {
|
|
7
|
+
readonly version: 202508191204;
|
|
8
|
+
readonly sha256: "7801fd6c355854b62ecb62ee32727b4e5dcca908841dbcfd5e5de45b3dfd1c25";
|
|
9
|
+
};
|
|
10
|
+
readonly "094": {
|
|
11
|
+
readonly version: 202503231214;
|
|
12
|
+
readonly sha256: "047c15e2140738ec08258b74536e6cb996cddf0c586c5c39e2318e54125a9e76";
|
|
13
|
+
};
|
|
14
|
+
readonly "20P": {
|
|
15
|
+
readonly version: 202511162200;
|
|
16
|
+
readonly sha256: "e4f067d027b9bcbb9bbf6cb90f41f11130cf4d83dffd3dee101db7826dc808c3";
|
|
17
|
+
};
|
|
18
|
+
readonly "22E": {
|
|
19
|
+
readonly version: 202510142200;
|
|
20
|
+
readonly sha256: "87426021aea2bd4e8d721e46e1c86808fd7c6fc06f52baf6d952ead9e8d14d1e";
|
|
21
|
+
};
|
|
22
|
+
readonly "239": {
|
|
23
|
+
readonly version: 202506171403;
|
|
24
|
+
readonly sha256: "c1d23d5302756d57df5cb9bc795d95fc54ef1db7565dca1f04b69a024886426a";
|
|
25
|
+
};
|
|
26
|
+
readonly "31B": {
|
|
27
|
+
readonly version: 202511162200;
|
|
28
|
+
readonly sha256: "dcf63657bff5451f0715ea3c032efe482279bc8cbc28cb9b8ae51c7354826574";
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
export declare const HIDDEN_HMS: Readonly<Record<string, readonly string[]>>;
|