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/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: {
|
|
@@ -2377,6 +2405,23 @@ class BambuPrinterMCPServer {
|
|
|
2377
2405
|
required: ["action", "ams_id"]
|
|
2378
2406
|
}
|
|
2379
2407
|
},
|
|
2408
|
+
{
|
|
2409
|
+
name: "inspect_print_file",
|
|
2410
|
+
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.",
|
|
2411
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
2412
|
+
inputSchema: {
|
|
2413
|
+
type: "object",
|
|
2414
|
+
additionalProperties: false,
|
|
2415
|
+
properties: {
|
|
2416
|
+
file_path: { type: "string", minLength: 1, description: "Local prepared .3mf or textual .gcode artifact to inspect; checksum-only and unsliced projects reject." },
|
|
2417
|
+
bambu_model: { type: "string", enum: [...VALID_BAMBU_MODELS], description: "Explicit file-inspection target model; never inferred from environment or live state." },
|
|
2418
|
+
plate_index: { type: "integer", minimum: 0, description: "Zero-based selected plate; defaults to 0. Raw G-code only supports 0." },
|
|
2419
|
+
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." },
|
|
2420
|
+
bed_type: { type: "string", enum: [...VALID_BED_TYPES], description: "Optional declared bed type to compare with file metadata." },
|
|
2421
|
+
},
|
|
2422
|
+
required: ["file_path", "bambu_model"],
|
|
2423
|
+
},
|
|
2424
|
+
},
|
|
2380
2425
|
{
|
|
2381
2426
|
name: "print_3mf",
|
|
2382
2427
|
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.",
|
|
@@ -2412,6 +2457,11 @@ class BambuPrinterMCPServer {
|
|
|
2412
2457
|
enum: [...VALID_NOZZLE_TYPES],
|
|
2413
2458
|
description: "Installed nozzle material, used when the 3MF must be auto-sliced (default: BAMBU_NOZZLE_TYPE, else the preset's stock nozzle)."
|
|
2414
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
|
+
},
|
|
2415
2465
|
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
2416
2466
|
bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
|
|
2417
2467
|
bambu_token: { type: "string", description: "Access token (default: value from env)" },
|
|
@@ -2601,9 +2651,42 @@ class BambuPrinterMCPServer {
|
|
|
2601
2651
|
const host = String(args?.host || DEFAULT_HOST);
|
|
2602
2652
|
const bambuSerial = String(args?.bambu_serial || DEFAULT_BAMBU_SERIAL);
|
|
2603
2653
|
const bambuToken = String(args?.bambu_token || DEFAULT_BAMBU_TOKEN);
|
|
2604
|
-
const
|
|
2605
|
-
(
|
|
2606
|
-
|
|
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
|
+
};
|
|
2607
2690
|
const requestedTemplateDir = typeof args?.template_dir === "string" && args.template_dir.trim().length > 0
|
|
2608
2691
|
? String(args.template_dir)
|
|
2609
2692
|
: undefined;
|
|
@@ -2613,6 +2696,42 @@ class BambuPrinterMCPServer {
|
|
|
2613
2696
|
try {
|
|
2614
2697
|
let result;
|
|
2615
2698
|
switch (name) {
|
|
2699
|
+
case "inspect_print_file": {
|
|
2700
|
+
const allowed = new Set(["file_path", "bambu_model", "plate_index", "nozzle_diameters", "bed_type"]);
|
|
2701
|
+
if (!args || Object.keys(args).some(key => !allowed.has(key)))
|
|
2702
|
+
throw new Error("inspect_print_file accepts only file/model/plate/nozzle/bed arguments.");
|
|
2703
|
+
if (typeof args.file_path !== "string" || !args.file_path.trim() || args.file_path.includes("\0"))
|
|
2704
|
+
throw new Error("file_path must name a local prepared artifact.");
|
|
2705
|
+
if (typeof args.bambu_model !== "string" || !args.bambu_model.trim())
|
|
2706
|
+
throw new Error("An explicit bambu_model is required for file inspection.");
|
|
2707
|
+
const model = validateBambuModel(args.bambu_model);
|
|
2708
|
+
const plateIndex = args.plate_index === undefined ? 0 : args.plate_index;
|
|
2709
|
+
if (typeof plateIndex !== "number" || !Number.isSafeInteger(plateIndex) || plateIndex < 0)
|
|
2710
|
+
throw new Error("plate_index must be a nonnegative safe integer.");
|
|
2711
|
+
if (args.nozzle_diameters !== undefined && (!Array.isArray(args.nozzle_diameters) || args.nozzle_diameters.some(value => typeof value !== "number")))
|
|
2712
|
+
throw new Error("nozzle_diameters must be numeric sizes.");
|
|
2713
|
+
const nozzles = requestedPrintNozzles(args);
|
|
2714
|
+
if (args.bed_type !== undefined && (typeof args.bed_type !== "string" || !args.bed_type.trim()))
|
|
2715
|
+
throw new Error("bed_type must be a supported plate type.");
|
|
2716
|
+
const bedType = args.bed_type === undefined ? undefined : resolveBedType(args.bed_type);
|
|
2717
|
+
if (extra.signal.aborted)
|
|
2718
|
+
throw new Error("File inspection cancelled.");
|
|
2719
|
+
result = await withPrintSnapshot(args.file_path, async (snapshot) => {
|
|
2720
|
+
const inspection = await inspectPrintFile(snapshot, { model, nozzleDiameters: nozzles, plateIndex, bedType });
|
|
2721
|
+
if (extra.signal.aborted)
|
|
2722
|
+
throw new Error("File inspection cancelled.");
|
|
2723
|
+
const byteLength = (await fs.promises.stat(snapshot)).size;
|
|
2724
|
+
return {
|
|
2725
|
+
receipt_version: "1",
|
|
2726
|
+
evidence: "file-only",
|
|
2727
|
+
dispatch_authorized: false,
|
|
2728
|
+
artifact: { sha256: inspection.sha256, byte_length: byteLength, file_format: path.extname(snapshot).toLowerCase() === ".3mf" ? "3mf" : "gcode" },
|
|
2729
|
+
selection: { bambu_model: model, plate_index: plateIndex, ...(nozzles === undefined ? {} : { nozzle_diameters: nozzles }), ...(bedType === undefined ? {} : { bed_type: bedType }) },
|
|
2730
|
+
inspection,
|
|
2731
|
+
};
|
|
2732
|
+
});
|
|
2733
|
+
break;
|
|
2734
|
+
}
|
|
2616
2735
|
case "get_printer_status":
|
|
2617
2736
|
result = await this.bambu.getStatus(host, bambuSerial, bambuToken);
|
|
2618
2737
|
break;
|
|
@@ -2769,19 +2888,19 @@ class BambuPrinterMCPServer {
|
|
|
2769
2888
|
break;
|
|
2770
2889
|
}
|
|
2771
2890
|
case "cancel_print":
|
|
2772
|
-
result = await this.bambu.cancelJob(host, bambuSerial, bambuToken, nativeControls
|
|
2773
|
-
? () => sendNativeCommand({ host, serial: bambuSerial, token: bambuToken,
|
|
2774
|
-
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))
|
|
2775
2894
|
: undefined);
|
|
2776
2895
|
break;
|
|
2777
2896
|
case "pause_print":
|
|
2778
|
-
result = await this.bambu.pauseJob(host, bambuSerial, bambuToken, nativeControls
|
|
2897
|
+
result = await this.bambu.pauseJob(host, bambuSerial, bambuToken, nativeControls()
|
|
2779
2898
|
? () => sendNativeCommand({ host, serial: bambuSerial, token: bambuToken,
|
|
2780
2899
|
messageJson: JSON.stringify({ print: { command: "pause", param: "", sequence_id: String(Date.now()) } }), qos: 1 })
|
|
2781
2900
|
: undefined);
|
|
2782
2901
|
break;
|
|
2783
2902
|
case "resume_print":
|
|
2784
|
-
result = await this.bambu.resumeJob(host, bambuSerial, bambuToken, nativeControls
|
|
2903
|
+
result = await this.bambu.resumeJob(host, bambuSerial, bambuToken, nativeControls()
|
|
2785
2904
|
? (assertActive, beforeDispatch) => sendNativeCommand({ host, serial: bambuSerial, token: bambuToken,
|
|
2786
2905
|
messageJson: JSON.stringify({ print: { command: "resume", param: "", sequence_id: String(Date.now()) } }), qos: 1 }, { signal: extra.signal, assertActive, beforeDispatch })
|
|
2787
2906
|
: undefined);
|
|
@@ -2790,8 +2909,9 @@ class BambuPrinterMCPServer {
|
|
|
2790
2909
|
if (!args?.message_json) {
|
|
2791
2910
|
throw new Error("Missing required parameter: message_json");
|
|
2792
2911
|
}
|
|
2793
|
-
if (!nativeControls) {
|
|
2794
|
-
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());
|
|
2795
2915
|
}
|
|
2796
2916
|
const messageJson = validateBambuNativeMetadata(String(args.message_json));
|
|
2797
2917
|
result = await sendNativeCommand({
|
|
@@ -2805,7 +2925,7 @@ class BambuPrinterMCPServer {
|
|
|
2805
2925
|
break;
|
|
2806
2926
|
}
|
|
2807
2927
|
case "clear_hms_errors":
|
|
2808
|
-
result = await this.bambu.clearHmsErrors(host, bambuSerial, bambuToken, nativeControls
|
|
2928
|
+
result = await this.bambu.clearHmsErrors(host, bambuSerial, bambuToken, nativeControls()
|
|
2809
2929
|
? (assertActive, beforeDispatch) => sendNativeCommand({ host, serial: bambuSerial, token: bambuToken,
|
|
2810
2930
|
messageJson: JSON.stringify({ print: { command: "clean_print_error", param: "", sequence_id: String(Date.now()) } }), qos: 1 }, { signal: extra.signal, assertActive, beforeDispatch })
|
|
2811
2931
|
: undefined);
|
|
@@ -2814,7 +2934,7 @@ class BambuPrinterMCPServer {
|
|
|
2814
2934
|
if (!args?.mode) {
|
|
2815
2935
|
throw new Error("Missing required parameter: mode");
|
|
2816
2936
|
}
|
|
2817
|
-
if (nativeControls) {
|
|
2937
|
+
if (nativeControls()) {
|
|
2818
2938
|
const normalized = String(args.mode).trim().toLowerCase();
|
|
2819
2939
|
const mode = normalized === "silent" ? 1
|
|
2820
2940
|
: normalized === "standard" ? 2
|
|
@@ -2841,7 +2961,7 @@ class BambuPrinterMCPServer {
|
|
|
2841
2961
|
if (!args?.mode) {
|
|
2842
2962
|
throw new Error("Missing required parameter: mode");
|
|
2843
2963
|
}
|
|
2844
|
-
if (nativeControls) {
|
|
2964
|
+
if (nativeControls()) {
|
|
2845
2965
|
const mode = String(args.mode).trim().toLowerCase();
|
|
2846
2966
|
if (mode !== "cooling" && mode !== "heating") {
|
|
2847
2967
|
throw new Error("Airduct mode must be one of: cooling, heating.");
|
|
@@ -2868,7 +2988,7 @@ class BambuPrinterMCPServer {
|
|
|
2868
2988
|
if (args?.ams_id === undefined || args?.slot_id === undefined) {
|
|
2869
2989
|
throw new Error("Missing required parameters: ams_id and slot_id");
|
|
2870
2990
|
}
|
|
2871
|
-
if (nativeControls) {
|
|
2991
|
+
if (nativeControls()) {
|
|
2872
2992
|
const amsId = Number(args.ams_id);
|
|
2873
2993
|
const slotId = Number(args.slot_id);
|
|
2874
2994
|
if ((!Number.isInteger(amsId) || (amsId < 0 || amsId > 3)) && amsId !== 128) {
|
|
@@ -2905,9 +3025,14 @@ class BambuPrinterMCPServer {
|
|
|
2905
3025
|
const heaterModel = args.temperature === 0
|
|
2906
3026
|
? String(args.bambu_model || DEFAULT_BAMBU_MODEL).trim().toLowerCase()
|
|
2907
3027
|
: await this.resolveBambuModel(args.bambu_model);
|
|
2908
|
-
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" &&
|
|
2909
|
-
? (heater, target, assertActive, beforeDispatch) =>
|
|
2910
|
-
|
|
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
|
+
}
|
|
2911
3036
|
: undefined);
|
|
2912
3037
|
break;
|
|
2913
3038
|
}
|
|
@@ -2915,7 +3040,7 @@ class BambuPrinterMCPServer {
|
|
|
2915
3040
|
if (!args?.fan || args?.speed === undefined) {
|
|
2916
3041
|
throw new Error("Missing required parameters: fan and speed");
|
|
2917
3042
|
}
|
|
2918
|
-
if (nativeControls) {
|
|
3043
|
+
if (nativeControls()) {
|
|
2919
3044
|
const status = await this.bambu.getStatus(host, bambuSerial, bambuToken);
|
|
2920
3045
|
const printerState = String(status?.raw?.gcode_state ?? status?.raw?.print?.gcode_state ?? status?.gcode_state ?? status?.status ?? "").toUpperCase();
|
|
2921
3046
|
if (printerState === "RUNNING" && args?.confirm_during_print !== true) {
|
|
@@ -2943,7 +3068,7 @@ class BambuPrinterMCPServer {
|
|
|
2943
3068
|
if (!args?.light || !args?.mode) {
|
|
2944
3069
|
throw new Error("Missing required parameters: light and mode");
|
|
2945
3070
|
}
|
|
2946
|
-
if (nativeControls) {
|
|
3071
|
+
if (nativeControls()) {
|
|
2947
3072
|
const light = String(args.light).trim();
|
|
2948
3073
|
const mode = String(args.mode).trim().toLowerCase();
|
|
2949
3074
|
if (light !== "chamber_light" && light !== "chamber_light2") {
|
|
@@ -2978,7 +3103,7 @@ class BambuPrinterMCPServer {
|
|
|
2978
3103
|
if (!Array.isArray(args?.object_ids)) {
|
|
2979
3104
|
throw new Error("Missing required parameter: object_ids");
|
|
2980
3105
|
}
|
|
2981
|
-
if (nativeControls) {
|
|
3106
|
+
if (nativeControls()) {
|
|
2982
3107
|
const objectIds = args.object_ids.map((id) => Number(id));
|
|
2983
3108
|
if (objectIds.length === 0 || objectIds.some((id) => !Number.isInteger(id) || id < 0)) {
|
|
2984
3109
|
throw new Error("object_ids must contain one or more non-negative integers.");
|
|
@@ -3001,7 +3126,7 @@ class BambuPrinterMCPServer {
|
|
|
3001
3126
|
if (!args?.action || args?.ams_id === undefined) {
|
|
3002
3127
|
throw new Error("Missing required parameters: action and ams_id");
|
|
3003
3128
|
}
|
|
3004
|
-
if (nativeControls) {
|
|
3129
|
+
if (nativeControls()) {
|
|
3005
3130
|
const action = String(args.action).trim().toLowerCase();
|
|
3006
3131
|
const amsId = Number(args.ams_id);
|
|
3007
3132
|
if (action !== "start" && action !== "stop") {
|
|
@@ -3111,6 +3236,7 @@ class BambuPrinterMCPServer {
|
|
|
3111
3236
|
sliceBambuOptions.filamentColours = parseFilamentColours(String(args.filament_colours));
|
|
3112
3237
|
sliceBambuOptions.bedType = resolveBambuStudioCliBedType(args?.bed_type);
|
|
3113
3238
|
sliceBambuOptions.nozzleType = resolveNozzleType(args?.nozzle_type);
|
|
3239
|
+
sliceBambuOptions.nozzleVolumeType = resolveNozzleVolumeType(args?.nozzle_volume_type);
|
|
3114
3240
|
if (args?.enable_timelapse !== undefined)
|
|
3115
3241
|
sliceBambuOptions.enableTimelapse = Boolean(args.enable_timelapse);
|
|
3116
3242
|
if (args?.allow_mix_temp !== undefined)
|
|
@@ -3191,6 +3317,7 @@ class BambuPrinterMCPServer {
|
|
|
3191
3317
|
sliceBambuOptions.filamentColours = parseFilamentColours(String(args.filament_colours));
|
|
3192
3318
|
sliceBambuOptions.bedType = resolveBambuStudioCliBedType(args?.bed_type);
|
|
3193
3319
|
sliceBambuOptions.nozzleType = resolveNozzleType(args?.nozzle_type);
|
|
3320
|
+
sliceBambuOptions.nozzleVolumeType = resolveNozzleVolumeType(args?.nozzle_volume_type);
|
|
3194
3321
|
if (args?.enable_timelapse !== undefined)
|
|
3195
3322
|
sliceBambuOptions.enableTimelapse = Boolean(args.enable_timelapse);
|
|
3196
3323
|
if (args?.allow_mix_temp !== undefined)
|
|
@@ -3294,6 +3421,7 @@ class BambuPrinterMCPServer {
|
|
|
3294
3421
|
skipModifiedGcodes: true,
|
|
3295
3422
|
bedType: printBedType,
|
|
3296
3423
|
nozzleType: resolveNozzleType(args?.nozzle_type),
|
|
3424
|
+
nozzleVolumeType: resolveNozzleVolumeType(args?.nozzle_volume_type),
|
|
3297
3425
|
};
|
|
3298
3426
|
if (!explicitSlicerProfile) {
|
|
3299
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[]>>;
|