bambu-printer-mcp 1.1.17 → 1.1.19
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/README.md +1 -1
- package/dist/index.js +38 -2
- package/dist/printers/bambu.d.ts +9 -0
- package/dist/printers/bambu.js +143 -6
- package/dist/safety/printer-state.js +4 -1
- package/dist/stl/stl-manipulator.d.ts +1 -0
- package/dist/stl/stl-manipulator.js +13 -0
- package/package.json +1 -1
- package/src/index.ts +38 -2
- package/src/printers/bambu.ts +105 -6
- package/src/safety/printer-state.ts +4 -1
- package/src/stl/stl-manipulator.ts +14 -0
package/README.md
CHANGED
|
@@ -906,7 +906,7 @@ The primary tool for starting a Bambu print. **Recommended input: a pre-sliced `
|
|
|
906
906
|
|
|
907
907
|
`bambu_model` is **required** for model-specific routing and preset selection. It does not by itself validate pre-sliced G-code. BambuStudio, FULU, and Orca CLI preparation additionally require the exact model/nozzle machine preset and reject incomplete profiles. Using the wrong model can damage hardware. If `bambu_model` is not provided in the tool call and `BAMBU_MODEL` is not set in the environment, the server will ask you interactively via MCP elicitation (if your client supports it) or return a clear error.
|
|
908
908
|
|
|
909
|
-
`bed_type` defaults to `textured_plate` if omitted. `ams_slots` is the preferred override input; `ams_mapping` remains the raw escape hatch. On AMS-equipped H2 printers, `use_ams: false` does not suppress mapping lookup if the sliced file declares filaments. If no mapping is provided for an H2 pre-sliced job with declared filaments, the server fails before sending; pass explicit `ams_slots`, raw `ams_mapping`, or `auto_match_ams: true`.
|
|
909
|
+
`bed_type` defaults to `textured_plate` if omitted. `nozzle_type` (`stainless_steel`, `hardened_steel`, `tungsten_carbide`, `brass`; default `BAMBU_NOZZLE_TYPE`) sets the installed nozzle when the project must be auto-sliced; the job's nozzle type must match the printer's report, and stock P1S/P1P/A1 presets assume stainless steel. After an MQTT print command the server listens to the printer's pushed reports for up to 15 seconds (`BAMBU_DISPATCH_CHECK_MS`) and returns `dispatch: "started"` or `"unconfirmed"`; if firmware 01.08.05+ refuses the command (HMS 0500-0500-0001-0007, needs LAN Only Mode and Developer Mode), the call fails and says so; clear that fatal HMS entry with `clear_hms_errors` before printing again. `ams_slots` is the preferred override input; `ams_mapping` remains the raw escape hatch. On AMS-equipped H2 printers, `use_ams: false` does not suppress mapping lookup if the sliced file declares filaments. If no mapping is provided for an H2 pre-sliced job with declared filaments, the server fails before sending; pass explicit `ams_slots`, raw `ams_mapping`, or `auto_match_ams: true`.
|
|
910
910
|
|
|
911
911
|
Set `auto_match_ams: true` to match the sliced 3MF's `tray_info_idx` values against the live AMS inventory and use the matching `ams_slots`. The matcher joins on `(tray_info_idx, tray_color)` and tracks already-claimed slots, so prints with two filaments of the same SKU but different colors (e.g. two GFG02 PETG HF in black and white) resolve correctly. Falls back to SKU-only when the 3MF's filament has no color set or only one tray of that SKU is loaded. Returns a structured `missing` report (`reason: "no_loaded_match" | "color_mismatch" | "exhausted" | "no_sku"`) when a filament can't be resolved. Ignored when you provide `ams_slots` or `ams_mapping` explicitly.
|
|
912
912
|
|
package/dist/index.js
CHANGED
|
@@ -77,6 +77,18 @@ const VALID_BAMBU_MODELS = ["p1s", "p1p", "p2s", "x1c", "x1e", "a1", "a1mini", "
|
|
|
77
77
|
// jobs take the correct branch instead of the legacy 5-slot mapping.
|
|
78
78
|
const H2_BAMBU_MODELS = new Set(["h2d", "h2s", "h2c", "x2d"]);
|
|
79
79
|
const VALID_BED_TYPES = ["textured_plate", "cool_plate", "engineering_plate", "hot_plate", "supertack_plate"];
|
|
80
|
+
const VALID_NOZZLE_TYPES = ["stainless_steel", "hardened_steel", "tungsten_carbide", "brass"];
|
|
81
|
+
/** The installed hotend material. Bambu machine presets assume the stock nozzle; the print gate compares this with the printer. */
|
|
82
|
+
function resolveNozzleType(value) {
|
|
83
|
+
const raw = value === undefined || value === null || value === "" ? process.env.BAMBU_NOZZLE_TYPE?.trim() : value;
|
|
84
|
+
if (raw === undefined || raw === "")
|
|
85
|
+
return undefined;
|
|
86
|
+
const normalized = String(raw).trim().toLowerCase().replace(/[\s-]+/g, "_");
|
|
87
|
+
if (!VALID_NOZZLE_TYPES.includes(normalized)) {
|
|
88
|
+
throw new Error(`Invalid nozzle type "${raw}". Valid types: ${VALID_NOZZLE_TYPES.join(", ")}`);
|
|
89
|
+
}
|
|
90
|
+
return normalized;
|
|
91
|
+
}
|
|
80
92
|
const VALID_BAMBUSTUDIO_CLI_BED_TYPES = ["textured_plate", "cool_plate", "engineering_plate", "hot_plate"];
|
|
81
93
|
// Map model IDs to BambuStudio --load-machine preset names
|
|
82
94
|
const BAMBU_MODEL_PRESETS = {
|
|
@@ -1010,6 +1022,7 @@ class BambuPrinterMCPServer {
|
|
|
1010
1022
|
minSave: true,
|
|
1011
1023
|
skipModifiedGcodes: true,
|
|
1012
1024
|
bedType: bedType ? resolveBambuStudioCliBedType(bedType) : undefined,
|
|
1025
|
+
nozzleType: resolveNozzleType(args?.nozzle_type),
|
|
1013
1026
|
};
|
|
1014
1027
|
threeMFPath = await this.stlManipulator.sliceSTL(threeMFPath, slicerType, slicerPath, slicerProfile || undefined, undefined, printPreset, autoSliceOptions);
|
|
1015
1028
|
console.log("Auto-sliced to: " + threeMFPath);
|
|
@@ -1806,6 +1819,11 @@ class BambuPrinterMCPServer {
|
|
|
1806
1819
|
enum: ["textured_plate", "cool_plate", "engineering_plate", "hot_plate", "supertack_plate"],
|
|
1807
1820
|
description: "Bed plate type for slicing (default: textured_plate). SuperTack is accepted only for pre-sliced print jobs until the BambuStudio CLI identifier is verified."
|
|
1808
1821
|
},
|
|
1822
|
+
nozzle_type: {
|
|
1823
|
+
type: "string",
|
|
1824
|
+
enum: [...VALID_NOZZLE_TYPES],
|
|
1825
|
+
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."
|
|
1826
|
+
},
|
|
1809
1827
|
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." },
|
|
1810
1828
|
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
1811
1829
|
bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
|
|
@@ -1856,6 +1874,11 @@ class BambuPrinterMCPServer {
|
|
|
1856
1874
|
enum: ["textured_plate", "cool_plate", "engineering_plate", "hot_plate", "supertack_plate"],
|
|
1857
1875
|
description: "Bed plate type for slicing (default: textured_plate). SuperTack is accepted only for pre-sliced print jobs until the BambuStudio CLI identifier is verified."
|
|
1858
1876
|
},
|
|
1877
|
+
nozzle_type: {
|
|
1878
|
+
type: "string",
|
|
1879
|
+
enum: [...VALID_NOZZLE_TYPES],
|
|
1880
|
+
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."
|
|
1881
|
+
},
|
|
1859
1882
|
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." },
|
|
1860
1883
|
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." },
|
|
1861
1884
|
repetitions: { type: "number", description: "Print N identical copies of the model. Each copy gets its own plate placement. Example: 3 prints three copies." },
|
|
@@ -1961,6 +1984,7 @@ class BambuPrinterMCPServer {
|
|
|
1961
1984
|
username: { type: "string", description: "Printer username for LAN/local bridge methods; defaults to bblp." },
|
|
1962
1985
|
password: { type: "string", description: "Printer password/access code override for LAN/local bridge methods." },
|
|
1963
1986
|
bed_type: { type: "string", enum: ["textured_plate", "cool_plate", "engineering_plate", "hot_plate", "supertack_plate"], description: "Bed plate type currently installed (default: textured_plate)." },
|
|
1987
|
+
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)." },
|
|
1964
1988
|
plate_index: { type: "number", description: "Zero-based plate index to print from the sliced 3MF; converted to FULU's one-based PrintParams plate_index." },
|
|
1965
1989
|
project_name: { type: "string", description: "Optional project name sent in FULU PrintParams; defaults to the 3MF filename without extension." },
|
|
1966
1990
|
preset_name: { type: "string", description: "Optional preset name sent in FULU PrintParams; defaults to project plus one-based plate index." },
|
|
@@ -2366,6 +2390,11 @@ class BambuPrinterMCPServer {
|
|
|
2366
2390
|
enum: ["textured_plate", "cool_plate", "engineering_plate", "hot_plate", "supertack_plate"],
|
|
2367
2391
|
description: "Bed plate type currently installed (default: textured_plate)"
|
|
2368
2392
|
},
|
|
2393
|
+
nozzle_type: {
|
|
2394
|
+
type: "string",
|
|
2395
|
+
enum: [...VALID_NOZZLE_TYPES],
|
|
2396
|
+
description: "Installed nozzle material, used when the 3MF must be auto-sliced (default: BAMBU_NOZZLE_TYPE, else the preset's stock nozzle)."
|
|
2397
|
+
},
|
|
2369
2398
|
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
2370
2399
|
bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
|
|
2371
2400
|
bambu_token: { type: "string", description: "Access token (default: value from env)" },
|
|
@@ -3064,6 +3093,7 @@ class BambuPrinterMCPServer {
|
|
|
3064
3093
|
if (args?.filament_colours !== undefined)
|
|
3065
3094
|
sliceBambuOptions.filamentColours = parseFilamentColours(String(args.filament_colours));
|
|
3066
3095
|
sliceBambuOptions.bedType = resolveBambuStudioCliBedType(args?.bed_type);
|
|
3096
|
+
sliceBambuOptions.nozzleType = resolveNozzleType(args?.nozzle_type);
|
|
3067
3097
|
if (args?.enable_timelapse !== undefined)
|
|
3068
3098
|
sliceBambuOptions.enableTimelapse = Boolean(args.enable_timelapse);
|
|
3069
3099
|
if (args?.allow_mix_temp !== undefined)
|
|
@@ -3143,6 +3173,7 @@ class BambuPrinterMCPServer {
|
|
|
3143
3173
|
if (args?.filament_colours !== undefined)
|
|
3144
3174
|
sliceBambuOptions.filamentColours = parseFilamentColours(String(args.filament_colours));
|
|
3145
3175
|
sliceBambuOptions.bedType = resolveBambuStudioCliBedType(args?.bed_type);
|
|
3176
|
+
sliceBambuOptions.nozzleType = resolveNozzleType(args?.nozzle_type);
|
|
3146
3177
|
if (args?.enable_timelapse !== undefined)
|
|
3147
3178
|
sliceBambuOptions.enableTimelapse = Boolean(args.enable_timelapse);
|
|
3148
3179
|
if (args?.allow_mix_temp !== undefined)
|
|
@@ -3245,6 +3276,7 @@ class BambuPrinterMCPServer {
|
|
|
3245
3276
|
minSave: true,
|
|
3246
3277
|
skipModifiedGcodes: true,
|
|
3247
3278
|
bedType: printBedType,
|
|
3279
|
+
nozzleType: resolveNozzleType(args?.nozzle_type),
|
|
3248
3280
|
};
|
|
3249
3281
|
if (!explicitSlicerProfile) {
|
|
3250
3282
|
try {
|
|
@@ -3347,7 +3379,7 @@ class BambuPrinterMCPServer {
|
|
|
3347
3379
|
layerInspect: args?.layer_inspect !== undefined ? Boolean(args.layer_inspect) : undefined,
|
|
3348
3380
|
timelapse: args?.timelapse !== undefined ? Boolean(args.timelapse) : undefined,
|
|
3349
3381
|
});
|
|
3350
|
-
|
|
3382
|
+
// Report what the printer did, not merely that a command was published.
|
|
3351
3383
|
break;
|
|
3352
3384
|
}
|
|
3353
3385
|
case "print_collar_charm": {
|
|
@@ -3448,10 +3480,14 @@ class BambuPrinterMCPServer {
|
|
|
3448
3480
|
}
|
|
3449
3481
|
catch (error) {
|
|
3450
3482
|
const message = error instanceof Error ? error.message : String(error);
|
|
3483
|
+
// Refusals and printer rejections repeat unchanged; say what to fix instead of "try again".
|
|
3484
|
+
const refused = /nothing was (?:sent|uploaded)|no command was sent|does not match the job|is ambiguous|is missing or ambiguous|rejected the print command|are refused|is refused|refused because|refused until|refused before/i.test(message);
|
|
3451
3485
|
const structured = {
|
|
3452
3486
|
status: "error",
|
|
3453
3487
|
retryable: false,
|
|
3454
|
-
suggestion:
|
|
3488
|
+
suggestion: refused
|
|
3489
|
+
? `Retrying unchanged repeats this refusal. Fix the reported cause (file, arguments, printer settings, or printer state) first. Error: ${message}`
|
|
3490
|
+
: `Check parameters and try again. Error: ${message}`,
|
|
3455
3491
|
message,
|
|
3456
3492
|
tool: name,
|
|
3457
3493
|
};
|
package/dist/printers/bambu.d.ts
CHANGED
|
@@ -94,14 +94,23 @@ export declare class BambuImplementation {
|
|
|
94
94
|
status: string;
|
|
95
95
|
uploaded: boolean;
|
|
96
96
|
printRequested: boolean;
|
|
97
|
+
dispatch: "started" | "unconfirmed";
|
|
97
98
|
remotePath: string;
|
|
98
99
|
message: string;
|
|
99
100
|
}>;
|
|
100
101
|
private printRawPrepared;
|
|
102
|
+
/**
|
|
103
|
+
* A published print command is not proof the printer took it. Listen to the
|
|
104
|
+
* reports the printer pushes anyway (no extra pushall polling on weak P1
|
|
105
|
+
* boards): a new command-verification HMS means the firmware refused the
|
|
106
|
+
* command; PREPARE/SLICING/RUNNING means it started. Attach before publishing.
|
|
107
|
+
*/
|
|
108
|
+
private watchDispatch;
|
|
101
109
|
startJob(host: string, serial: string, token: string, filename: string, bambuModel?: string): Promise<{
|
|
102
110
|
status: string;
|
|
103
111
|
uploaded: boolean;
|
|
104
112
|
printRequested: boolean;
|
|
113
|
+
dispatch: "started" | "unconfirmed";
|
|
105
114
|
remotePath: string;
|
|
106
115
|
message: string;
|
|
107
116
|
}>;
|
package/dist/printers/bambu.js
CHANGED
|
@@ -36,6 +36,21 @@ function loadClientCreds() {
|
|
|
36
36
|
}
|
|
37
37
|
const CLIENT_CREDS = loadClientCreds();
|
|
38
38
|
const COMMAND_SETTLE_MS = 300;
|
|
39
|
+
/** HMS 0500-0500-0001-0007: firmware 01.08.05+ rejected an unsigned MQTT command. */
|
|
40
|
+
const COMMAND_VERIFICATION_HMS = { attr: 0x05000500, code: 0x00010007 };
|
|
41
|
+
const COMMAND_REJECTED_MESSAGE = "The printer rejected the print command (HMS 0500-0500-0001-0007, \"MQTT command verification failed\"). " +
|
|
42
|
+
"Bambu firmware 01.08.05 and later only accept third-party LAN control with LAN Only Mode and Developer Mode " +
|
|
43
|
+
"enabled (Settings > WLAN on the printer). The checked file is on the printer's storage, but nothing is printing. " +
|
|
44
|
+
"The rejection stays on the printer as a fatal HMS entry that blocks later prints: after enabling those modes, " +
|
|
45
|
+
"clear it with clear_hms_errors or dismiss it on the printer's screen, then print again.";
|
|
46
|
+
const DISPATCH_CANCELLED_MESSAGE = "A stop or heater-off request arrived after the print command was sent, so this call does not report the print as started. " +
|
|
47
|
+
"Check get_printer_status.";
|
|
48
|
+
/** How long to watch fresh reports after a print command (BAMBU_DISPATCH_CHECK_MS, 0 disables). */
|
|
49
|
+
function dispatchCheckMs() {
|
|
50
|
+
const raw = process.env.BAMBU_DISPATCH_CHECK_MS?.trim();
|
|
51
|
+
const value = raw ? Number(raw) : 15000;
|
|
52
|
+
return Number.isInteger(value) && value >= 0 && value <= 60000 ? value : 15000;
|
|
53
|
+
}
|
|
39
54
|
const MODEL_ID_TO_NAME = {
|
|
40
55
|
O1C: "H2C",
|
|
41
56
|
O1C2: "H2C",
|
|
@@ -685,9 +700,23 @@ export class BambuImplementation {
|
|
|
685
700
|
const printer = await this.getPrinter(host, serial, token);
|
|
686
701
|
assertActive();
|
|
687
702
|
if (legacyContainer) {
|
|
688
|
-
|
|
703
|
+
const watcher = this.watchDispatch(printer, host, serial, token, dispatchStatus, assertActive);
|
|
704
|
+
try {
|
|
705
|
+
await invokeWithoutAck(printer, new GCodeFileCommand({ fileName: remoteProjectPath }));
|
|
706
|
+
}
|
|
707
|
+
catch (error) {
|
|
708
|
+
watcher.dispose();
|
|
709
|
+
throw error;
|
|
710
|
+
}
|
|
689
711
|
this.recordCheckedJob(host, serial, remoteProjectPath, requirements);
|
|
690
|
-
|
|
712
|
+
const dispatch = await watcher.settled();
|
|
713
|
+
return {
|
|
714
|
+
status: "success", dispatch, remoteProjectPath,
|
|
715
|
+
message: dispatch === "started"
|
|
716
|
+
? `The printer accepted and started the checked gcode.3mf print: ${options.projectName}`
|
|
717
|
+
: `Uploaded the checked gcode.3mf and sent the print command for ${options.projectName}; the printer has not reported starting yet. ` +
|
|
718
|
+
"Check get_printer_status before assuming it is printing.",
|
|
719
|
+
};
|
|
691
720
|
}
|
|
692
721
|
const b = (v) => (v ? 1 : 0);
|
|
693
722
|
let projectFileCmd;
|
|
@@ -747,12 +776,24 @@ export class BambuImplementation {
|
|
|
747
776
|
},
|
|
748
777
|
};
|
|
749
778
|
}
|
|
750
|
-
|
|
779
|
+
const watcher = this.watchDispatch(printer, host, serial, token, dispatchStatus, assertActive);
|
|
780
|
+
try {
|
|
781
|
+
await printer.publish(projectFileCmd);
|
|
782
|
+
}
|
|
783
|
+
catch (error) {
|
|
784
|
+
watcher.dispose();
|
|
785
|
+
throw error;
|
|
786
|
+
}
|
|
751
787
|
this.recordCheckedJob(host, serial, remoteProjectPath, requirements);
|
|
752
788
|
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
789
|
+
const dispatch = await watcher.settled();
|
|
753
790
|
return {
|
|
754
791
|
status: "success",
|
|
755
|
-
|
|
792
|
+
dispatch,
|
|
793
|
+
message: dispatch === "started"
|
|
794
|
+
? `The printer accepted and started the checked 3MF print: ${options.projectName}`
|
|
795
|
+
: `Uploaded the checked 3MF and sent the print command for ${options.projectName}; the printer has not reported starting yet. ` +
|
|
796
|
+
"Check get_printer_status before assuming it is printing.",
|
|
756
797
|
remoteProjectPath,
|
|
757
798
|
plateFile: projectMetadata.plateFileName,
|
|
758
799
|
platePath: projectMetadata.plateInternalPath,
|
|
@@ -1190,9 +1231,105 @@ export class BambuImplementation {
|
|
|
1190
1231
|
this.assertBedClearance(dispatchStatus, bedClearance);
|
|
1191
1232
|
const printer = await this.getPrinter(host, serial, token);
|
|
1192
1233
|
assertActive();
|
|
1193
|
-
|
|
1234
|
+
const watcher = this.watchDispatch(printer, host, serial, token, dispatchStatus, assertActive);
|
|
1235
|
+
try {
|
|
1236
|
+
await invokeWithoutAck(printer, new GCodeFileCommand({ fileName: remotePath }));
|
|
1237
|
+
}
|
|
1238
|
+
catch (error) {
|
|
1239
|
+
watcher.dispose();
|
|
1240
|
+
throw error;
|
|
1241
|
+
}
|
|
1194
1242
|
this.recordCheckedJob(host, serial, remotePath, requirements);
|
|
1195
|
-
|
|
1243
|
+
const dispatch = await watcher.settled();
|
|
1244
|
+
return {
|
|
1245
|
+
status: "success", uploaded: true, printRequested: true, dispatch, remotePath,
|
|
1246
|
+
message: dispatch === "started"
|
|
1247
|
+
? `The printer accepted and started ${remotePath}.`
|
|
1248
|
+
: `Sent the print command for ${remotePath}; the printer has not reported starting yet. Check get_printer_status before assuming it is printing.`,
|
|
1249
|
+
};
|
|
1250
|
+
}
|
|
1251
|
+
/**
|
|
1252
|
+
* A published print command is not proof the printer took it. Listen to the
|
|
1253
|
+
* reports the printer pushes anyway (no extra pushall polling on weak P1
|
|
1254
|
+
* boards): a new command-verification HMS means the firmware refused the
|
|
1255
|
+
* command; PREPARE/SLICING/RUNNING means it started. Attach before publishing.
|
|
1256
|
+
*/
|
|
1257
|
+
watchDispatch(printer, host, serial, token, before, assertActive) {
|
|
1258
|
+
const windowMs = dispatchCheckMs();
|
|
1259
|
+
if (windowMs === 0)
|
|
1260
|
+
return { settled: async () => { assertActive(); return "unconfirmed"; }, dispose: () => { } };
|
|
1261
|
+
const key = (entry) => `${Number(entry?.attr)}:${Number(entry?.code)}:${entry?.timestamp ?? ""}`;
|
|
1262
|
+
const known = new Set((Array.isArray(before?.raw?.hms) ? before.raw.hms : []).map(key));
|
|
1263
|
+
const judge = (report) => {
|
|
1264
|
+
const hms = Array.isArray(report?.hms) ? report.hms : [];
|
|
1265
|
+
if (hms.some((entry) => Number(entry?.attr) === COMMAND_VERIFICATION_HMS.attr &&
|
|
1266
|
+
Number(entry?.code) === COMMAND_VERIFICATION_HMS.code && !known.has(key(entry))))
|
|
1267
|
+
return "rejected";
|
|
1268
|
+
if (["PREPARE", "SLICING", "RUNNING"].includes(String(report?.gcode_state ?? "").toUpperCase()))
|
|
1269
|
+
return "started";
|
|
1270
|
+
return undefined;
|
|
1271
|
+
};
|
|
1272
|
+
let resolveOutcome;
|
|
1273
|
+
const outcome = new Promise((resolve) => { resolveOutcome = resolve; });
|
|
1274
|
+
const onRaw = (topic, payload) => {
|
|
1275
|
+
if (topic !== `device/${serial}/report`)
|
|
1276
|
+
return;
|
|
1277
|
+
let parsed;
|
|
1278
|
+
try {
|
|
1279
|
+
parsed = JSON.parse(payload.toString());
|
|
1280
|
+
}
|
|
1281
|
+
catch {
|
|
1282
|
+
return;
|
|
1283
|
+
}
|
|
1284
|
+
const verdict = judge(parsed?.print);
|
|
1285
|
+
if (verdict)
|
|
1286
|
+
resolveOutcome(verdict);
|
|
1287
|
+
};
|
|
1288
|
+
const cancelled = () => { try {
|
|
1289
|
+
assertActive();
|
|
1290
|
+
return false;
|
|
1291
|
+
}
|
|
1292
|
+
catch {
|
|
1293
|
+
return true;
|
|
1294
|
+
} };
|
|
1295
|
+
printer?.on?.("rawMessage", onRaw);
|
|
1296
|
+
const timer = setTimeout(() => resolveOutcome(undefined), windowMs);
|
|
1297
|
+
// A stop during the window must not end as "accepted and started".
|
|
1298
|
+
const cancelCheck = setInterval(() => { if (cancelled())
|
|
1299
|
+
resolveOutcome("cancelled"); }, 250);
|
|
1300
|
+
const dispose = () => {
|
|
1301
|
+
clearTimeout(timer);
|
|
1302
|
+
clearInterval(cancelCheck);
|
|
1303
|
+
printer?.off?.("rawMessage", onRaw);
|
|
1304
|
+
resolveOutcome(undefined);
|
|
1305
|
+
};
|
|
1306
|
+
return {
|
|
1307
|
+
dispose,
|
|
1308
|
+
settled: async () => {
|
|
1309
|
+
let verdict;
|
|
1310
|
+
try {
|
|
1311
|
+
verdict = await outcome;
|
|
1312
|
+
}
|
|
1313
|
+
finally {
|
|
1314
|
+
dispose();
|
|
1315
|
+
}
|
|
1316
|
+
// Nothing pushed during the window: take one full report, not a polling loop.
|
|
1317
|
+
if (verdict === undefined && !cancelled()) {
|
|
1318
|
+
try {
|
|
1319
|
+
verdict = judge((await this.getSafetyStatus(host, serial, token))?.raw);
|
|
1320
|
+
}
|
|
1321
|
+
catch { /* stays unconfirmed */ }
|
|
1322
|
+
}
|
|
1323
|
+
if (verdict === "rejected") {
|
|
1324
|
+
// Nothing started, so there is no checked job to resume or clear later.
|
|
1325
|
+
this.checkedJobs.delete(`${host}\n${serial}`);
|
|
1326
|
+
throw new Error(COMMAND_REJECTED_MESSAGE);
|
|
1327
|
+
}
|
|
1328
|
+
if (verdict === "cancelled" || cancelled())
|
|
1329
|
+
throw new Error(DISPATCH_CANCELLED_MESSAGE);
|
|
1330
|
+
return verdict ?? "unconfirmed";
|
|
1331
|
+
},
|
|
1332
|
+
};
|
|
1196
1333
|
}
|
|
1197
1334
|
async startJob(host, serial, token, filename, bambuModel) {
|
|
1198
1335
|
assertDirectPrintSupported(bambuModel, serial);
|
|
@@ -194,7 +194,10 @@ function validateErrors(raw, requireIdle) {
|
|
|
194
194
|
// BambuStudio DevHMS.cpp: code >> 16; DevHMS.h: 1=fatal,2=serious,3=common,4=info.
|
|
195
195
|
// Unknown severities fail closed; attr identifies the module and is not a severity bitmask.
|
|
196
196
|
if (code === undefined || attr === undefined || !Number.isInteger(code) || !Number.isInteger(attr) || code < 0 || code > 0xffffffff || attr < 0 || attr > 0xffffffff || Math.floor(code / 65536) !== 4) {
|
|
197
|
-
|
|
197
|
+
const hex = (value) => value.toString(16).toUpperCase().padStart(8, "0");
|
|
198
|
+
const label = code !== undefined && attr !== undefined && Number.isInteger(code) && Number.isInteger(attr) && code >= 0 && attr >= 0
|
|
199
|
+
? ` (HMS ${hex(attr).slice(0, 4)}-${hex(attr).slice(4)}-${hex(code).slice(0, 4)}-${hex(code).slice(4)})` : "";
|
|
200
|
+
throw new Error(`Printer reports actionable or unknown HMS errors${label}; inspect and resolve them before proceeding.`);
|
|
198
201
|
}
|
|
199
202
|
}
|
|
200
203
|
}
|
|
@@ -1282,6 +1282,19 @@ export class STLManipulator extends EventEmitter {
|
|
|
1282
1282
|
const rawBundle = await this.resolveBambuLikeSettingsBundle(outputBase, slicerType, slicerProfile, printerPreset, bambuOptions, profilesRoot);
|
|
1283
1283
|
// Resolve every BBL dependency before launching the slicer.
|
|
1284
1284
|
const settingsBundle = await this.maybeFlattenBundle(await this.expandProjectFilaments(stlFilePath, rawBundle), bambuOptions, profilesRoot);
|
|
1285
|
+
// The machine preset assumes the stock hotend; record the installed one so
|
|
1286
|
+
// the sliced job's nozzle_type matches what the printer reports.
|
|
1287
|
+
if (bambuOptions?.nozzleType && settingsBundle.settingsArg) {
|
|
1288
|
+
const [machinePath, ...rest] = settingsBundle.settingsArg.split(';');
|
|
1289
|
+
const machine = this.readJsonFile(machinePath);
|
|
1290
|
+
const current = machine.nozzle_type;
|
|
1291
|
+
machine.nozzle_type = Array.isArray(current) && current.length > 0
|
|
1292
|
+
? current.map(() => bambuOptions.nozzleType)
|
|
1293
|
+
: [bambuOptions.nozzleType];
|
|
1294
|
+
settingsBundle.settingsArg = [
|
|
1295
|
+
this.writeTempJson(outputBase, `machine_${bambuOptions.nozzleType}`, machine), ...rest,
|
|
1296
|
+
].join(';');
|
|
1297
|
+
}
|
|
1285
1298
|
// Inherited process G-code is now present; apply Orca's existing
|
|
1286
1299
|
// absolute-extrusion normalization after resolving those ancestors.
|
|
1287
1300
|
// The machine owns the extrusion mode and layer G-code once process
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -111,6 +111,18 @@ type BambuModel = typeof VALID_BAMBU_MODELS[number];
|
|
|
111
111
|
const H2_BAMBU_MODELS = new Set<string>(["h2d", "h2s", "h2c", "x2d"]);
|
|
112
112
|
|
|
113
113
|
const VALID_BED_TYPES = ["textured_plate", "cool_plate", "engineering_plate", "hot_plate", "supertack_plate"] as const;
|
|
114
|
+
const VALID_NOZZLE_TYPES = ["stainless_steel", "hardened_steel", "tungsten_carbide", "brass"] as const;
|
|
115
|
+
|
|
116
|
+
/** The installed hotend material. Bambu machine presets assume the stock nozzle; the print gate compares this with the printer. */
|
|
117
|
+
function resolveNozzleType(value: unknown): string | undefined {
|
|
118
|
+
const raw = value === undefined || value === null || value === "" ? process.env.BAMBU_NOZZLE_TYPE?.trim() : value;
|
|
119
|
+
if (raw === undefined || raw === "") return undefined;
|
|
120
|
+
const normalized = String(raw).trim().toLowerCase().replace(/[\s-]+/g, "_");
|
|
121
|
+
if (!(VALID_NOZZLE_TYPES as readonly string[]).includes(normalized)) {
|
|
122
|
+
throw new Error(`Invalid nozzle type "${raw}". Valid types: ${VALID_NOZZLE_TYPES.join(", ")}`);
|
|
123
|
+
}
|
|
124
|
+
return normalized;
|
|
125
|
+
}
|
|
114
126
|
const VALID_BAMBUSTUDIO_CLI_BED_TYPES = ["textured_plate", "cool_plate", "engineering_plate", "hot_plate"] as const;
|
|
115
127
|
|
|
116
128
|
// Map model IDs to BambuStudio --load-machine preset names
|
|
@@ -1358,6 +1370,7 @@ class BambuPrinterMCPServer {
|
|
|
1358
1370
|
minSave: true,
|
|
1359
1371
|
skipModifiedGcodes: true,
|
|
1360
1372
|
bedType: bedType ? resolveBambuStudioCliBedType(bedType) : undefined,
|
|
1373
|
+
nozzleType: resolveNozzleType(args?.nozzle_type),
|
|
1361
1374
|
};
|
|
1362
1375
|
threeMFPath = await this.stlManipulator.sliceSTL(
|
|
1363
1376
|
threeMFPath, slicerType, slicerPath, slicerProfile || undefined,
|
|
@@ -2332,6 +2345,11 @@ class BambuPrinterMCPServer {
|
|
|
2332
2345
|
enum: ["textured_plate", "cool_plate", "engineering_plate", "hot_plate", "supertack_plate"],
|
|
2333
2346
|
description: "Bed plate type for slicing (default: textured_plate). SuperTack is accepted only for pre-sliced print jobs until the BambuStudio CLI identifier is verified."
|
|
2334
2347
|
},
|
|
2348
|
+
nozzle_type: {
|
|
2349
|
+
type: "string",
|
|
2350
|
+
enum: [...VALID_NOZZLE_TYPES],
|
|
2351
|
+
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."
|
|
2352
|
+
},
|
|
2335
2353
|
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." },
|
|
2336
2354
|
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
2337
2355
|
bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
|
|
@@ -2382,6 +2400,11 @@ class BambuPrinterMCPServer {
|
|
|
2382
2400
|
enum: ["textured_plate", "cool_plate", "engineering_plate", "hot_plate", "supertack_plate"],
|
|
2383
2401
|
description: "Bed plate type for slicing (default: textured_plate). SuperTack is accepted only for pre-sliced print jobs until the BambuStudio CLI identifier is verified."
|
|
2384
2402
|
},
|
|
2403
|
+
nozzle_type: {
|
|
2404
|
+
type: "string",
|
|
2405
|
+
enum: [...VALID_NOZZLE_TYPES],
|
|
2406
|
+
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."
|
|
2407
|
+
},
|
|
2385
2408
|
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." },
|
|
2386
2409
|
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." },
|
|
2387
2410
|
repetitions: { type: "number", description: "Print N identical copies of the model. Each copy gets its own plate placement. Example: 3 prints three copies." },
|
|
@@ -2487,6 +2510,7 @@ class BambuPrinterMCPServer {
|
|
|
2487
2510
|
username: { type: "string", description: "Printer username for LAN/local bridge methods; defaults to bblp." },
|
|
2488
2511
|
password: { type: "string", description: "Printer password/access code override for LAN/local bridge methods." },
|
|
2489
2512
|
bed_type: { type: "string", enum: ["textured_plate", "cool_plate", "engineering_plate", "hot_plate", "supertack_plate"], description: "Bed plate type currently installed (default: textured_plate)." },
|
|
2513
|
+
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)." },
|
|
2490
2514
|
plate_index: { type: "number", description: "Zero-based plate index to print from the sliced 3MF; converted to FULU's one-based PrintParams plate_index." },
|
|
2491
2515
|
project_name: { type: "string", description: "Optional project name sent in FULU PrintParams; defaults to the 3MF filename without extension." },
|
|
2492
2516
|
preset_name: { type: "string", description: "Optional preset name sent in FULU PrintParams; defaults to project plus one-based plate index." },
|
|
@@ -2892,6 +2916,11 @@ class BambuPrinterMCPServer {
|
|
|
2892
2916
|
enum: ["textured_plate", "cool_plate", "engineering_plate", "hot_plate", "supertack_plate"],
|
|
2893
2917
|
description: "Bed plate type currently installed (default: textured_plate)"
|
|
2894
2918
|
},
|
|
2919
|
+
nozzle_type: {
|
|
2920
|
+
type: "string",
|
|
2921
|
+
enum: [...VALID_NOZZLE_TYPES],
|
|
2922
|
+
description: "Installed nozzle material, used when the 3MF must be auto-sliced (default: BAMBU_NOZZLE_TYPE, else the preset's stock nozzle)."
|
|
2923
|
+
},
|
|
2895
2924
|
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
2896
2925
|
bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
|
|
2897
2926
|
bambu_token: { type: "string", description: "Access token (default: value from env)" },
|
|
@@ -3719,6 +3748,7 @@ class BambuPrinterMCPServer {
|
|
|
3719
3748
|
if (args?.load_filament_ids !== undefined) sliceBambuOptions.loadFilamentIds = String(args.load_filament_ids);
|
|
3720
3749
|
if (args?.filament_colours !== undefined) sliceBambuOptions.filamentColours = parseFilamentColours(String(args.filament_colours));
|
|
3721
3750
|
sliceBambuOptions.bedType = resolveBambuStudioCliBedType(args?.bed_type as string | undefined);
|
|
3751
|
+
sliceBambuOptions.nozzleType = resolveNozzleType(args?.nozzle_type);
|
|
3722
3752
|
if (args?.enable_timelapse !== undefined) sliceBambuOptions.enableTimelapse = Boolean(args.enable_timelapse);
|
|
3723
3753
|
if (args?.allow_mix_temp !== undefined) sliceBambuOptions.allowMixTemp = Boolean(args.allow_mix_temp);
|
|
3724
3754
|
if (args?.scale !== undefined) sliceBambuOptions.scale = Number(args.scale);
|
|
@@ -3805,6 +3835,7 @@ class BambuPrinterMCPServer {
|
|
|
3805
3835
|
if (args?.load_filament_ids !== undefined) sliceBambuOptions.loadFilamentIds = String(args.load_filament_ids);
|
|
3806
3836
|
if (args?.filament_colours !== undefined) sliceBambuOptions.filamentColours = parseFilamentColours(String(args.filament_colours));
|
|
3807
3837
|
sliceBambuOptions.bedType = resolveBambuStudioCliBedType(args?.bed_type as string | undefined);
|
|
3838
|
+
sliceBambuOptions.nozzleType = resolveNozzleType(args?.nozzle_type);
|
|
3808
3839
|
if (args?.enable_timelapse !== undefined) sliceBambuOptions.enableTimelapse = Boolean(args.enable_timelapse);
|
|
3809
3840
|
if (args?.allow_mix_temp !== undefined) sliceBambuOptions.allowMixTemp = Boolean(args.allow_mix_temp);
|
|
3810
3841
|
if (args?.scale !== undefined) sliceBambuOptions.scale = Number(args.scale);
|
|
@@ -3932,6 +3963,7 @@ class BambuPrinterMCPServer {
|
|
|
3932
3963
|
minSave: true,
|
|
3933
3964
|
skipModifiedGcodes: true,
|
|
3934
3965
|
bedType: printBedType,
|
|
3966
|
+
nozzleType: resolveNozzleType(args?.nozzle_type),
|
|
3935
3967
|
};
|
|
3936
3968
|
if (!explicitSlicerProfile) {
|
|
3937
3969
|
try {
|
|
@@ -4062,7 +4094,7 @@ class BambuPrinterMCPServer {
|
|
|
4062
4094
|
layerInspect: args?.layer_inspect !== undefined ? Boolean(args.layer_inspect) : undefined,
|
|
4063
4095
|
timelapse: args?.timelapse !== undefined ? Boolean(args.timelapse) : undefined,
|
|
4064
4096
|
});
|
|
4065
|
-
|
|
4097
|
+
// Report what the printer did, not merely that a command was published.
|
|
4066
4098
|
break;
|
|
4067
4099
|
}
|
|
4068
4100
|
|
|
@@ -4214,10 +4246,14 @@ class BambuPrinterMCPServer {
|
|
|
4214
4246
|
|
|
4215
4247
|
} catch (error) {
|
|
4216
4248
|
const message = error instanceof Error ? error.message : String(error);
|
|
4249
|
+
// Refusals and printer rejections repeat unchanged; say what to fix instead of "try again".
|
|
4250
|
+
const refused = /nothing was (?:sent|uploaded)|no command was sent|does not match the job|is ambiguous|is missing or ambiguous|rejected the print command|are refused|is refused|refused because|refused until|refused before/i.test(message);
|
|
4217
4251
|
const structured: StructuredToolError = {
|
|
4218
4252
|
status: "error",
|
|
4219
4253
|
retryable: false,
|
|
4220
|
-
suggestion:
|
|
4254
|
+
suggestion: refused
|
|
4255
|
+
? `Retrying unchanged repeats this refusal. Fix the reported cause (file, arguments, printer settings, or printer state) first. Error: ${message}`
|
|
4256
|
+
: `Check parameters and try again. Error: ${message}`,
|
|
4221
4257
|
message,
|
|
4222
4258
|
tool: name,
|
|
4223
4259
|
};
|
package/src/printers/bambu.ts
CHANGED
|
@@ -45,6 +45,24 @@ function loadClientCreds(): { cert: Buffer; key: Buffer } | null {
|
|
|
45
45
|
const CLIENT_CREDS = loadClientCreds();
|
|
46
46
|
|
|
47
47
|
const COMMAND_SETTLE_MS = 300;
|
|
48
|
+
/** HMS 0500-0500-0001-0007: firmware 01.08.05+ rejected an unsigned MQTT command. */
|
|
49
|
+
const COMMAND_VERIFICATION_HMS = { attr: 0x05000500, code: 0x00010007 };
|
|
50
|
+
const COMMAND_REJECTED_MESSAGE =
|
|
51
|
+
"The printer rejected the print command (HMS 0500-0500-0001-0007, \"MQTT command verification failed\"). " +
|
|
52
|
+
"Bambu firmware 01.08.05 and later only accept third-party LAN control with LAN Only Mode and Developer Mode " +
|
|
53
|
+
"enabled (Settings > WLAN on the printer). The checked file is on the printer's storage, but nothing is printing. " +
|
|
54
|
+
"The rejection stays on the printer as a fatal HMS entry that blocks later prints: after enabling those modes, " +
|
|
55
|
+
"clear it with clear_hms_errors or dismiss it on the printer's screen, then print again.";
|
|
56
|
+
const DISPATCH_CANCELLED_MESSAGE =
|
|
57
|
+
"A stop or heater-off request arrived after the print command was sent, so this call does not report the print as started. " +
|
|
58
|
+
"Check get_printer_status.";
|
|
59
|
+
|
|
60
|
+
/** How long to watch fresh reports after a print command (BAMBU_DISPATCH_CHECK_MS, 0 disables). */
|
|
61
|
+
function dispatchCheckMs(): number {
|
|
62
|
+
const raw = process.env.BAMBU_DISPATCH_CHECK_MS?.trim();
|
|
63
|
+
const value = raw ? Number(raw) : 15_000;
|
|
64
|
+
return Number.isInteger(value) && value >= 0 && value <= 60_000 ? value : 15_000;
|
|
65
|
+
}
|
|
48
66
|
|
|
49
67
|
const MODEL_ID_TO_NAME: Record<string, string> = {
|
|
50
68
|
O1C: "H2C",
|
|
@@ -813,9 +831,17 @@ export class BambuImplementation {
|
|
|
813
831
|
const printer = await this.getPrinter(host, serial, token);
|
|
814
832
|
assertActive();
|
|
815
833
|
if (legacyContainer) {
|
|
816
|
-
|
|
834
|
+
const watcher = this.watchDispatch(printer, host, serial, token, dispatchStatus, assertActive);
|
|
835
|
+
try { await invokeWithoutAck(printer, new GCodeFileCommand({ fileName: remoteProjectPath })); } catch (error) { watcher.dispose(); throw error; }
|
|
817
836
|
this.recordCheckedJob(host, serial, remoteProjectPath, requirements);
|
|
818
|
-
|
|
837
|
+
const dispatch = await watcher.settled();
|
|
838
|
+
return {
|
|
839
|
+
status: "success", dispatch, remoteProjectPath,
|
|
840
|
+
message: dispatch === "started"
|
|
841
|
+
? `The printer accepted and started the checked gcode.3mf print: ${options.projectName}`
|
|
842
|
+
: `Uploaded the checked gcode.3mf and sent the print command for ${options.projectName}; the printer has not reported starting yet. ` +
|
|
843
|
+
"Check get_printer_status before assuming it is printing.",
|
|
844
|
+
};
|
|
819
845
|
}
|
|
820
846
|
|
|
821
847
|
const b = (v: any) => (v ? 1 : 0);
|
|
@@ -876,13 +902,19 @@ export class BambuImplementation {
|
|
|
876
902
|
};
|
|
877
903
|
}
|
|
878
904
|
|
|
879
|
-
|
|
905
|
+
const watcher = this.watchDispatch(printer, host, serial, token, dispatchStatus, assertActive);
|
|
906
|
+
try { await printer.publish(projectFileCmd); } catch (error) { watcher.dispose(); throw error; }
|
|
880
907
|
this.recordCheckedJob(host, serial, remoteProjectPath, requirements);
|
|
881
908
|
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
909
|
+
const dispatch = await watcher.settled();
|
|
882
910
|
|
|
883
911
|
return {
|
|
884
912
|
status: "success",
|
|
885
|
-
|
|
913
|
+
dispatch,
|
|
914
|
+
message: dispatch === "started"
|
|
915
|
+
? `The printer accepted and started the checked 3MF print: ${options.projectName}`
|
|
916
|
+
: `Uploaded the checked 3MF and sent the print command for ${options.projectName}; the printer has not reported starting yet. ` +
|
|
917
|
+
"Check get_printer_status before assuming it is printing.",
|
|
886
918
|
remoteProjectPath,
|
|
887
919
|
plateFile: projectMetadata.plateFileName,
|
|
888
920
|
platePath: projectMetadata.plateInternalPath,
|
|
@@ -1392,9 +1424,76 @@ export class BambuImplementation {
|
|
|
1392
1424
|
this.assertBedClearance(dispatchStatus, bedClearance);
|
|
1393
1425
|
const printer = await this.getPrinter(host, serial, token);
|
|
1394
1426
|
assertActive();
|
|
1395
|
-
|
|
1427
|
+
const watcher = this.watchDispatch(printer, host, serial, token, dispatchStatus, assertActive);
|
|
1428
|
+
try { await invokeWithoutAck(printer, new GCodeFileCommand({ fileName: remotePath })); } catch (error) { watcher.dispose(); throw error; }
|
|
1396
1429
|
this.recordCheckedJob(host, serial, remotePath, requirements);
|
|
1397
|
-
|
|
1430
|
+
const dispatch = await watcher.settled();
|
|
1431
|
+
return {
|
|
1432
|
+
status: "success", uploaded: true, printRequested: true, dispatch, remotePath,
|
|
1433
|
+
message: dispatch === "started"
|
|
1434
|
+
? `The printer accepted and started ${remotePath}.`
|
|
1435
|
+
: `Sent the print command for ${remotePath}; the printer has not reported starting yet. Check get_printer_status before assuming it is printing.`,
|
|
1436
|
+
};
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
/**
|
|
1440
|
+
* A published print command is not proof the printer took it. Listen to the
|
|
1441
|
+
* reports the printer pushes anyway (no extra pushall polling on weak P1
|
|
1442
|
+
* boards): a new command-verification HMS means the firmware refused the
|
|
1443
|
+
* command; PREPARE/SLICING/RUNNING means it started. Attach before publishing.
|
|
1444
|
+
*/
|
|
1445
|
+
private watchDispatch(printer: any, host: string, serial: string, token: string, before: any, assertActive: () => void):
|
|
1446
|
+
{ settled: () => Promise<"started" | "unconfirmed">; dispose: () => void } {
|
|
1447
|
+
const windowMs = dispatchCheckMs();
|
|
1448
|
+
if (windowMs === 0) return { settled: async () => { assertActive(); return "unconfirmed"; }, dispose: () => {} };
|
|
1449
|
+
const key = (entry: any) => `${Number(entry?.attr)}:${Number(entry?.code)}:${entry?.timestamp ?? ""}`;
|
|
1450
|
+
const known = new Set((Array.isArray(before?.raw?.hms) ? before.raw.hms : []).map(key));
|
|
1451
|
+
const judge = (report: any): "started" | "rejected" | undefined => {
|
|
1452
|
+
const hms = Array.isArray(report?.hms) ? report.hms : [];
|
|
1453
|
+
if (hms.some((entry: any) => Number(entry?.attr) === COMMAND_VERIFICATION_HMS.attr &&
|
|
1454
|
+
Number(entry?.code) === COMMAND_VERIFICATION_HMS.code && !known.has(key(entry)))) return "rejected";
|
|
1455
|
+
if (["PREPARE", "SLICING", "RUNNING"].includes(String(report?.gcode_state ?? "").toUpperCase())) return "started";
|
|
1456
|
+
return undefined;
|
|
1457
|
+
};
|
|
1458
|
+
type Verdict = "started" | "rejected" | "cancelled" | undefined;
|
|
1459
|
+
let resolveOutcome!: (value: Verdict) => void;
|
|
1460
|
+
const outcome = new Promise<Verdict>((resolve) => { resolveOutcome = resolve; });
|
|
1461
|
+
const onRaw = (topic: string, payload: Buffer) => {
|
|
1462
|
+
if (topic !== `device/${serial}/report`) return;
|
|
1463
|
+
let parsed: any;
|
|
1464
|
+
try { parsed = JSON.parse(payload.toString()); } catch { return; }
|
|
1465
|
+
const verdict = judge(parsed?.print);
|
|
1466
|
+
if (verdict) resolveOutcome(verdict);
|
|
1467
|
+
};
|
|
1468
|
+
const cancelled = () => { try { assertActive(); return false; } catch { return true; } };
|
|
1469
|
+
printer?.on?.("rawMessage", onRaw);
|
|
1470
|
+
const timer = setTimeout(() => resolveOutcome(undefined), windowMs);
|
|
1471
|
+
// A stop during the window must not end as "accepted and started".
|
|
1472
|
+
const cancelCheck = setInterval(() => { if (cancelled()) resolveOutcome("cancelled"); }, 250);
|
|
1473
|
+
const dispose = () => {
|
|
1474
|
+
clearTimeout(timer);
|
|
1475
|
+
clearInterval(cancelCheck);
|
|
1476
|
+
printer?.off?.("rawMessage", onRaw);
|
|
1477
|
+
resolveOutcome(undefined);
|
|
1478
|
+
};
|
|
1479
|
+
return {
|
|
1480
|
+
dispose,
|
|
1481
|
+
settled: async () => {
|
|
1482
|
+
let verdict: Verdict;
|
|
1483
|
+
try { verdict = await outcome; } finally { dispose(); }
|
|
1484
|
+
// Nothing pushed during the window: take one full report, not a polling loop.
|
|
1485
|
+
if (verdict === undefined && !cancelled()) {
|
|
1486
|
+
try { verdict = judge((await this.getSafetyStatus(host, serial, token))?.raw); } catch { /* stays unconfirmed */ }
|
|
1487
|
+
}
|
|
1488
|
+
if (verdict === "rejected") {
|
|
1489
|
+
// Nothing started, so there is no checked job to resume or clear later.
|
|
1490
|
+
this.checkedJobs.delete(`${host}\n${serial}`);
|
|
1491
|
+
throw new Error(COMMAND_REJECTED_MESSAGE);
|
|
1492
|
+
}
|
|
1493
|
+
if (verdict === "cancelled" || cancelled()) throw new Error(DISPATCH_CANCELLED_MESSAGE);
|
|
1494
|
+
return verdict ?? "unconfirmed";
|
|
1495
|
+
},
|
|
1496
|
+
};
|
|
1398
1497
|
}
|
|
1399
1498
|
|
|
1400
1499
|
async startJob(host: string, serial: string, token: string, filename: string, bambuModel?: string) {
|
|
@@ -190,7 +190,10 @@ function validateErrors(raw: Record<string,any>, requireIdle: boolean) {
|
|
|
190
190
|
// BambuStudio DevHMS.cpp: code >> 16; DevHMS.h: 1=fatal,2=serious,3=common,4=info.
|
|
191
191
|
// Unknown severities fail closed; attr identifies the module and is not a severity bitmask.
|
|
192
192
|
if (code === undefined || attr === undefined || !Number.isInteger(code) || !Number.isInteger(attr) || code < 0 || code > 0xffffffff || attr < 0 || attr > 0xffffffff || Math.floor(code / 65536) !== 4) {
|
|
193
|
-
|
|
193
|
+
const hex = (value: number) => value.toString(16).toUpperCase().padStart(8, "0");
|
|
194
|
+
const label = code !== undefined && attr !== undefined && Number.isInteger(code) && Number.isInteger(attr) && code >= 0 && attr >= 0
|
|
195
|
+
? ` (HMS ${hex(attr).slice(0, 4)}-${hex(attr).slice(4)}-${hex(code).slice(0, 4)}-${hex(code).slice(4)})` : "";
|
|
196
|
+
throw new Error(`Printer reports actionable or unknown HMS errors${label}; inspect and resolve them before proceeding.`);
|
|
194
197
|
}
|
|
195
198
|
}
|
|
196
199
|
}
|
|
@@ -164,6 +164,7 @@ export interface BambuSliceOptions {
|
|
|
164
164
|
loadFilamentIds?: string; // --load-filament-ids "1,2,3,1": filament-to-object mapping
|
|
165
165
|
filamentColours?: string[]; // positional #RRGGBB per loaded filament slot
|
|
166
166
|
bedType?: string; // Bambu bed type, e.g. textured_plate or cool_plate
|
|
167
|
+
nozzleType?: string; // installed hotend material written into the machine preset
|
|
167
168
|
enableTimelapse?: boolean; // --enable-timelapse: timelapse-aware slicing
|
|
168
169
|
allowMixTemp?: boolean; // --allow-mix-temp: allow mixed-temp filaments
|
|
169
170
|
scale?: number; // --scale factor: uniform scale
|
|
@@ -1661,6 +1662,19 @@ export class STLManipulator extends EventEmitter {
|
|
|
1661
1662
|
// Resolve every BBL dependency before launching the slicer.
|
|
1662
1663
|
const settingsBundle = await this.maybeFlattenBundle(
|
|
1663
1664
|
await this.expandProjectFilaments(stlFilePath, rawBundle), bambuOptions, profilesRoot);
|
|
1665
|
+
// The machine preset assumes the stock hotend; record the installed one so
|
|
1666
|
+
// the sliced job's nozzle_type matches what the printer reports.
|
|
1667
|
+
if (bambuOptions?.nozzleType && settingsBundle.settingsArg) {
|
|
1668
|
+
const [machinePath, ...rest] = settingsBundle.settingsArg.split(';');
|
|
1669
|
+
const machine = this.readJsonFile(machinePath);
|
|
1670
|
+
const current = machine.nozzle_type;
|
|
1671
|
+
machine.nozzle_type = Array.isArray(current) && current.length > 0
|
|
1672
|
+
? current.map(() => bambuOptions.nozzleType)
|
|
1673
|
+
: [bambuOptions.nozzleType];
|
|
1674
|
+
settingsBundle.settingsArg = [
|
|
1675
|
+
this.writeTempJson(outputBase, `machine_${bambuOptions.nozzleType}`, machine), ...rest,
|
|
1676
|
+
].join(';');
|
|
1677
|
+
}
|
|
1664
1678
|
// Inherited process G-code is now present; apply Orca's existing
|
|
1665
1679
|
// absolute-extrusion normalization after resolving those ancestors.
|
|
1666
1680
|
// The machine owns the extrusion mode and layer G-code once process
|