bambu-printer-mcp 1.1.27 → 1.1.29

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.
Files changed (38) hide show
  1. package/CONTRIBUTORS.md +7 -1
  2. package/README.md +29 -10
  3. package/dist/ams-mapping.d.ts +18 -2
  4. package/dist/ams-mapping.js +36 -5
  5. package/dist/bambu-native.d.ts +55 -0
  6. package/dist/bambu-native.js +212 -13
  7. package/dist/index.js +154 -71
  8. package/dist/printers/bambu.d.ts +29 -2
  9. package/dist/printers/bambu.js +59 -28
  10. package/dist/safety/hidden-hms.d.ts +32 -0
  11. package/dist/safety/hidden-hms.js +405 -0
  12. package/dist/safety/limits.d.ts +6 -0
  13. package/dist/safety/limits.js +9 -0
  14. package/dist/safety/print-file.d.ts +2 -0
  15. package/dist/safety/print-file.js +83 -7
  16. package/dist/safety/printer-state.d.ts +11 -0
  17. package/dist/safety/printer-state.js +115 -22
  18. package/dist/slicer/profile-flatten.js +25 -0
  19. package/dist/slicer/remote-slicer.d.ts +12 -0
  20. package/dist/slicer/remote-slicer.js +240 -0
  21. package/dist/stl/stl-manipulator.d.ts +2 -1
  22. package/dist/stl/stl-manipulator.js +71 -42
  23. package/native/bambu-native-print.cpp +100 -26
  24. package/package.json +7 -5
  25. package/scripts/build-bambu-native.sh +53 -0
  26. package/scripts/setup-bambu-native.mjs +372 -0
  27. package/src/ams-mapping.ts +38 -5
  28. package/src/bambu-native.ts +229 -14
  29. package/src/index.ts +156 -67
  30. package/src/printers/bambu.ts +65 -25
  31. package/src/safety/hidden-hms.ts +406 -0
  32. package/src/safety/limits.ts +9 -0
  33. package/src/safety/print-file.ts +71 -7
  34. package/src/safety/printer-state.ts +117 -22
  35. package/src/slicer/profile-flatten.ts +32 -0
  36. package/src/slicer/remote-slicer.ts +216 -0
  37. package/src/stl/stl-manipulator.ts +70 -43
  38. package/scripts/build-bambu-native.zsh +0 -33
@@ -7,8 +7,9 @@ import os from "node:os";
7
7
  import { Readable } from "node:stream";
8
8
  import { readSafe3mfArchive } from "../safety/archive.js";
9
9
  import { inspectPrintFile } from "../safety/print-file.js";
10
- import { normalizeModel, validateTemperature } from "../safety/limits.js";
11
- import { validatePrinterState, manualHeatingRequirements } from "../safety/printer-state.js";
10
+ import { isDualNozzleModel, normalizeModel, validateTemperature } from "../safety/limits.js";
11
+ import { bridgeAmsMapping2Entry, bridgeAmsMappingValue, externalTrayForNozzle, describeAmsTrayRange } from "../ams-mapping.js";
12
+ import { describeHmsKey, hiddenHmsKeys, hiddenHmsNotices, reportedHmsKey, validatePrinterState, manualHeatingRequirements } from "../safety/printer-state.js";
12
13
  import { withPrinterOperation, withPrintSnapshot, uniquePrintName, normalizedRemotePath, cancelPendingPrinterOperations } from "../safety/artifact.js";
13
14
  import { Client as FTPClient } from "basic-ftp";
14
15
  import { BambuPrinter } from "bambu-js";
@@ -69,7 +70,7 @@ const MODEL_ID_TO_NAME = {
69
70
  const H2_MODEL_NAMES = new Set(["h2", "h2c", "h2d", "h2dpro", "h2d pro", "h2s", "x2d"]);
70
71
  export function assertDirectPrintSupported(model, serial) {
71
72
  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 macOS native eMMC route via print_3mf. " +
73
+ 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
74
  "X2D status and slicing remain available on all supported platforms.");
74
75
  }
75
76
  }
@@ -392,21 +393,37 @@ export class BambuImplementation {
392
393
  finishedJobIdentity(status) {
393
394
  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
395
  }
396
+ /**
397
+ * The human print confirmation. Its result goes to assertBedClearance at
398
+ * every later state check of the same print, which stops the print if the
399
+ * situation the human confirmed has changed.
400
+ */
395
401
  async confirmPrintPreflight(serial, status, inspection) {
396
402
  const finishedJob = this.finishedJobIdentity(status);
397
403
  const cleared = this.clearedErrors.get(serial);
404
+ const notices = hiddenHmsNotices(status?.raw, serial, inspection.model);
398
405
  await this.confirmHardwareAction(`Start a checked print on ${inspection.model.toUpperCase()} (${serial})? Nozzles: ${inspection.nozzleDiameters.join(", ")} mm. ` +
399
406
  `Materials: ${inspection.materials.join(", ")}. Peak targets: nozzle ${inspection.maxNozzleTemperature}°C, bed ${inspection.maxBedTemperature}°C, chamber ${inspection.maxChamberTemperature}°C. ` +
400
407
  `File SHA-256: ${inspection.sha256}. Confirm the physical spool labels and that the build plate is clear.` +
401
408
  (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.` : ""), finishedJob !== undefined || !!cleared);
403
- return finishedJob;
409
+ (cleared ? ` Previously cleared hardware codes: ${cleared.map(describeHmsKey).join(", ")}. Confirm their physical causes have been resolved.` : "") +
410
+ (notices.length ? ` The printer also reports ${notices.join(", ")}, which Bambu lists for this model without text and never shows as a fault; ` +
411
+ `${notices.length === 1 ? "it does" : "they do"} not block printing.` : ""), finishedJob !== undefined || !!cleared);
412
+ return { finishedJob, serial, model: inspection.model, notices };
404
413
  }
405
- assertBedClearance(status, confirmedFinishedJob) {
414
+ /** Stop a print whose confirmed situation changed: a newly finished job, or a hidden HMS notice the preflight did not name. */
415
+ assertBedClearance(status, preflight) {
406
416
  const current = this.finishedJobIdentity(status);
407
- if (current !== undefined && current !== confirmedFinishedJob) {
417
+ if (current !== undefined && current !== preflight.finishedJob) {
408
418
  throw new Error("Printer reports a newly finished job. Confirm that its part and debris have been removed before retrying the print.");
409
419
  }
420
+ // An X2D raises its hidden camera notice about 15 seconds after boot, so it
421
+ // can appear after the human confirmed. Never pass a notice nobody was told about.
422
+ const unnamed = hiddenHmsNotices(status?.raw, preflight.serial, preflight.model).filter(notice => !preflight.notices.includes(notice));
423
+ if (unnamed.length) {
424
+ throw new Error(`Printer reported ${unnamed.join(", ")} after the print confirmation. ` +
425
+ "Bambu hides it and it does not block printing, but it was not named when you confirmed; start the print again to review it.");
426
+ }
410
427
  }
411
428
  async getPrinter(host, serial, token) {
412
429
  return this.printerStore.getPrinter(host, serial, token);
@@ -604,12 +621,17 @@ export class BambuImplementation {
604
621
  // `plate_N.json.filament_ids` -- and expand to a full project-level
605
622
  // array here. `amsMapping` is the raw escape hatch (takes precedence
606
623
  // when both are supplied).
624
+ // Dual-nozzle printers address two external spools: 254 feeds the left/deputy
625
+ // nozzle and 255 the right/main nozzle (Bambu Studio VIRTUAL_TRAY_DEPUTY_ID /
626
+ // VIRTUAL_TRAY_MAIN_ID). Single-nozzle printers keep 254 as the only external tray.
627
+ const dualNozzle = isDualNozzleModel(model);
628
+ const externalTrays = dualNozzle ? [254, 255] : [254];
607
629
  const validateTrayValue = (v, label) => {
608
630
  if (!Number.isInteger(v) ||
609
631
  v < -1 ||
610
632
  (v > 15 && v < 128) ||
611
- v > 254) {
612
- throw new Error(`${label} values must be integers in [-1, 15] (absolute tray) or 128-254 (HT/external); got ${v}`);
633
+ v > (dualNozzle ? 255 : 254)) {
634
+ throw new Error(`${label} values must be integers in ${describeAmsTrayRange({ dualNozzle })}; got ${v}`);
613
635
  }
614
636
  };
615
637
  let baseMapping;
@@ -639,7 +661,7 @@ export class BambuImplementation {
639
661
  const projectLen = Math.max(projectMetadata.projectFilamentCount, ...positions.map((p) => p + 1), 1);
640
662
  baseMapping = Array(projectLen).fill(-1);
641
663
  positions.forEach((pos, i) => {
642
- baseMapping[pos] = options.useAMS === false ? 254 : i;
664
+ baseMapping[pos] = options.useAMS === false ? externalTrayForNozzle(inspection.filamentNozzleIndices?.[pos], dualNozzle) : i;
643
665
  });
644
666
  }
645
667
  let amsMapping;
@@ -647,15 +669,7 @@ export class BambuImplementation {
647
669
  if (usesH2ProjectFile) {
648
670
  const projLen = Math.max(projectMetadata.projectFilamentCount, baseMapping.length, 1);
649
671
  amsMapping = Array.from({ length: projLen }, (_, i) => i < baseMapping.length ? baseMapping[i] : -1);
650
- amsMapping2 = amsMapping.map((v) => {
651
- if (v < 0 || v === 255)
652
- return { ams_id: 255, slot_id: 255 };
653
- if (v === 254)
654
- return { ams_id: 254, slot_id: 254 };
655
- if (v >= 128)
656
- return { ams_id: 128, slot_id: v - 128 };
657
- return { ams_id: Math.floor(v / 4), slot_id: v % 4 };
658
- });
672
+ amsMapping2 = amsMapping.map((v) => bridgeAmsMapping2Entry(v, dualNozzle));
659
673
  }
660
674
  else {
661
675
  amsMapping = Array.from({ length: Math.max(5, baseMapping.length, projectMetadata.projectFilamentCount) }, (_, i) => i < baseMapping.length ? baseMapping[i] : -1);
@@ -665,8 +679,8 @@ export class BambuImplementation {
665
679
  if (baseMapping[position] === undefined || baseMapping[position] < 0) {
666
680
  throw new Error(`Missing physical filament mapping for project filament ${position}.`);
667
681
  }
668
- if (options.useAMS === false && baseMapping[position] !== 254) {
669
- throw new Error("External-spool printing requires external spool mapping (254).");
682
+ if (options.useAMS === false && !externalTrays.includes(baseMapping[position])) {
683
+ throw new Error(`External-spool printing requires external spool mapping (${externalTrays.join(" or ")}).`);
670
684
  }
671
685
  }
672
686
  const legacyContainer = options.filePath.toLowerCase().endsWith(".gcode.3mf") && !usesH2ProjectFile && !isA1;
@@ -752,7 +766,7 @@ export class BambuImplementation {
752
766
  project_id: submissionId,
753
767
  subtask_id: submissionId,
754
768
  task_id: submissionId,
755
- ams_mapping: amsMapping,
769
+ ams_mapping: amsMapping.map(value => bridgeAmsMappingValue(value, dualNozzle)),
756
770
  ams_mapping2: amsMapping2,
757
771
  },
758
772
  };
@@ -822,6 +836,19 @@ export class BambuImplementation {
822
836
  throw new Error(`Failed to cancel print: ${error.message}`);
823
837
  }
824
838
  }
839
+ /** Raw MQTT stop for routes that already ran the shared cancellation steps. */
840
+ async sendStopOverMqtt(host, serial, token) {
841
+ const printer = await this.getPrinter(host, serial, token);
842
+ await invokeWithoutAck(printer, new UpdateStateCommand({ state: "stop" }));
843
+ return { status: "success", message: "Cancel command sent over MQTT." };
844
+ }
845
+ /** Raw MQTT heater-off; switching off needs no material or model declaration. */
846
+ async sendHeaterOffOverMqtt(host, serial, token, heater) {
847
+ const printer = await this.getPrinter(host, serial, token);
848
+ const gcode = `${heater === "bed" ? "M140" : "M104"} S0`;
849
+ await invokeWithoutAck(printer, new GCodeLineCommand({ gcodes: [gcode] }));
850
+ return { status: "success", message: `Heater-off sent over MQTT for ${heater}.`, command: gcode };
851
+ }
825
852
  async pauseJob(host, serial, token, dispatch) {
826
853
  const printer = dispatch ? undefined : await this.getPrinter(host, serial, token);
827
854
  try {
@@ -867,10 +894,12 @@ export class BambuImplementation {
867
894
  const codesFor = (status) => {
868
895
  if (!Array.isArray(status.raw?.hms) || status.raw?.print_error === undefined)
869
896
  throw new Error("Fresh error codes are required before clearing hardware errors.");
870
- return [`print_error:${status.raw.print_error}`, ...status.raw.hms.map((entry) => `hms:${entry.attr}:${entry.code}`)].sort();
897
+ // Hidden notices never block and survive clean_print_error, so they are not errors to clear or re-acknowledge.
898
+ const hidden = new Set(hiddenHmsKeys(status.raw, serial, String(status.model ?? "")));
899
+ return [`print_error:${status.raw.print_error}`, ...status.raw.hms.map(reportedHmsKey).filter((key) => !hidden.has(key))].sort();
871
900
  };
872
901
  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);
902
+ 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
903
  const beforeDispatch = async () => {
875
904
  assertActive();
876
905
  const current = codesFor(await this.getSafetyStatus(host, serial, token));
@@ -1001,11 +1030,13 @@ export class BambuImplementation {
1001
1030
  }
1002
1031
  const printer = dispatch ? undefined : await this.getPrinter(host, serial, token);
1003
1032
  assertActive();
1004
- if (dispatch)
1005
- await dispatch(heater, targetTemperature, assertActive, beforeDispatch);
1006
- else
1033
+ const dispatched = dispatch ? await dispatch(heater, targetTemperature, assertActive, beforeDispatch) : undefined;
1034
+ if (!dispatch)
1007
1035
  await invokeWithoutAck(printer, new GCodeLineCommand({ gcodes: [gcode] }));
1008
- return { status: "success", message: `Temperature command sent for ${normalizedComponent}.`, command: gcode };
1036
+ // Keep route details from a dual-route Linux heater-off.
1037
+ const routeDetails = dispatched && typeof dispatched === "object" && "routes" in dispatched
1038
+ ? { routes: dispatched.routes, native_error: dispatched.native_error, mqtt_error: dispatched.mqtt_error } : {};
1039
+ return { status: "success", message: `Temperature command sent for ${normalizedComponent}.`, command: gcode, ...routeDetails };
1009
1040
  };
1010
1041
  if (targetTemperature === 0) {
1011
1042
  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[]>>;
@@ -0,0 +1,405 @@
1
+ // GENERATED by scripts/generate-hidden-hms.mjs from Bambu Studio v02.08.02.61
2
+ // (commit 926a7192574bcb9b3a732e1ec59a46d79cb45466, resources/hms/hms_en_<serial prefix>.json).
3
+ // Codes Bambu lists for a serial prefix with empty text in every entry. Do not edit by hand.
4
+ export const HIDDEN_HMS_SOURCE = {
5
+ studioTag: "v02.08.02.61",
6
+ studioCommit: "926a7192574bcb9b3a732e1ec59a46d79cb45466",
7
+ /** Per table: Bambu's device_hms version and the SHA-256 of the English file used. */
8
+ tables: {
9
+ "093": { version: 202508191204, sha256: "7801fd6c355854b62ecb62ee32727b4e5dcca908841dbcfd5e5de45b3dfd1c25" },
10
+ "094": { version: 202503231214, sha256: "047c15e2140738ec08258b74536e6cb996cddf0c586c5c39e2318e54125a9e76" },
11
+ "20P": { version: 202511162200, sha256: "e4f067d027b9bcbb9bbf6cb90f41f11130cf4d83dffd3dee101db7826dc808c3" },
12
+ "22E": { version: 202510142200, sha256: "87426021aea2bd4e8d721e46e1c86808fd7c6fc06f52baf6d952ead9e8d14d1e" },
13
+ "239": { version: 202506171403, sha256: "c1d23d5302756d57df5cb9bc795d95fc54ef1db7565dca1f04b69a024886426a" },
14
+ "31B": { version: 202511162200, sha256: "dcf63657bff5451f0715ea3c032efe482279bc8cbc28cb9b8ae51c7354826574" },
15
+ },
16
+ };
17
+ export const HIDDEN_HMS = {
18
+ "093": [
19
+ "0300180000000000",
20
+ "050001000001000A",
21
+ "0500010000020003",
22
+ "0500010000020008",
23
+ "0500010000020009",
24
+ "0500010000030009",
25
+ "0500030000010053",
26
+ "0500030000020010",
27
+ "0500030000020011",
28
+ "0500030000020012",
29
+ "0500030000020013",
30
+ "0500030000020014",
31
+ "0500030000020015",
32
+ "0500030000020016",
33
+ "0500030000020017",
34
+ "0500030000020018",
35
+ "0500030000030007",
36
+ "0500040000020020",
37
+ "0500050000010008",
38
+ "0500050000010009",
39
+ "050005000001000A",
40
+ "050005000001000B",
41
+ "050005000001000C",
42
+ "0500050000020030",
43
+ "0500060000020005",
44
+ "0500060000020007",
45
+ "0500060000020008",
46
+ "0500060000020011",
47
+ "0500060000020012",
48
+ "0500060000020013",
49
+ "0500060000020014",
50
+ "0500060000020021",
51
+ "0500060000020022",
52
+ "0500060000020023",
53
+ "0500060000020024",
54
+ "0500060000020041",
55
+ "0500060000020043",
56
+ "0500060000020044",
57
+ "0500060000020051",
58
+ "0500060000020053",
59
+ "0500060000020054",
60
+ "0500060000020061",
61
+ "0500060000020062",
62
+ "05000600000200A0",
63
+ "05000600000200A1",
64
+ "0C00010000010018",
65
+ "0C0001000002000E",
66
+ "0C00010000020015",
67
+ "0C00010000020016",
68
+ "0C00010000020019",
69
+ "0C0001000002001A",
70
+ "0C0001000002001B",
71
+ "0C00040000020006",
72
+ ],
73
+ "094": [
74
+ "0300180000000000",
75
+ "0300260000000000",
76
+ "050001000001000A",
77
+ "0500010000020003",
78
+ "0500010000020008",
79
+ "0500010000020009",
80
+ "0500010000030009",
81
+ "0500030000020010",
82
+ "0500030000020011",
83
+ "0500030000020012",
84
+ "0500030000020013",
85
+ "0500030000020014",
86
+ "0500030000020015",
87
+ "0500030000020016",
88
+ "0500030000020017",
89
+ "0500030000020018",
90
+ "0500030000030007",
91
+ "0500040000020020",
92
+ "0500050000010008",
93
+ "0500050000010009",
94
+ "050005000001000A",
95
+ "050005000001000B",
96
+ "050005000001000C",
97
+ "0500060000020001",
98
+ "0500060000020002",
99
+ "0500060000020003",
100
+ "0500060000020005",
101
+ "0500060000020006",
102
+ "0500060000020007",
103
+ "0500060000020008",
104
+ "0500060000020011",
105
+ "0500060000020012",
106
+ "0500060000020013",
107
+ "0500060000020014",
108
+ "0500060000020021",
109
+ "0500060000020022",
110
+ "0500060000020023",
111
+ "0500060000020024",
112
+ "0500060000020041",
113
+ "0500060000020042",
114
+ "0500060000020043",
115
+ "0500060000020044",
116
+ "0500060000020051",
117
+ "0500060000020052",
118
+ "0500060000020053",
119
+ "0500060000020054",
120
+ "0500060000020061",
121
+ "0500060000020062",
122
+ "05000600000200A0",
123
+ "05000600000200A1",
124
+ "0C00010000020015",
125
+ "0C00010000020016",
126
+ "0C00010000020019",
127
+ "0C0001000002001A",
128
+ "0C0001000002001B",
129
+ "0C00040000020006",
130
+ ],
131
+ "20P": [
132
+ "0300180000000000",
133
+ "0300260000000000",
134
+ "050001000001000A",
135
+ "0500010000020003",
136
+ "0500010000020008",
137
+ "0500010000020009",
138
+ "050001000002000B",
139
+ "050001000002000F",
140
+ "0500010000020010",
141
+ "0500010000020011",
142
+ "0500010000030009",
143
+ "0500020000030009",
144
+ "050002000003000A",
145
+ "050002000003000B",
146
+ "050002000003000C",
147
+ "0500030000010053",
148
+ "0500030000020010",
149
+ "0500030000020011",
150
+ "0500030000020012",
151
+ "0500030000020013",
152
+ "0500030000020014",
153
+ "0500030000020015",
154
+ "0500030000020016",
155
+ "0500030000020017",
156
+ "0500030000020018",
157
+ "0500030000020019",
158
+ "0500030000020050",
159
+ "0500030000020051",
160
+ "0500030000020052",
161
+ "0500030000020053",
162
+ "0500030000020054",
163
+ "0500030000030007",
164
+ "0500040000010056",
165
+ "0500040000010060",
166
+ "0500040000010061",
167
+ "0500040000020020",
168
+ "0500050000010008",
169
+ "0500050000010009",
170
+ "050005000001000A",
171
+ "050005000001000B",
172
+ "050005000001000C",
173
+ "0500050000020023",
174
+ "0500050000020030",
175
+ "0500060000020005",
176
+ "0500060000020006",
177
+ "0500060000020007",
178
+ "0500060000020008",
179
+ "0500060000020009",
180
+ "0500060000020011",
181
+ "0500060000020012",
182
+ "0500060000020013",
183
+ "0500060000020014",
184
+ "0500060000020015",
185
+ "0500060000020021",
186
+ "0500060000020022",
187
+ "0500060000020023",
188
+ "0500060000020024",
189
+ "0500060000020025",
190
+ "0500060000020035",
191
+ "0500060000020041",
192
+ "0500060000020042",
193
+ "0500060000020044",
194
+ "0500060000020045",
195
+ "0500060000020051",
196
+ "0500060000020052",
197
+ "0500060000020054",
198
+ "0500060000020061",
199
+ "0500060000020062",
200
+ "0500060000020070",
201
+ "0500060000020071",
202
+ "05000600000200A0",
203
+ "05000600000200A1",
204
+ "0500070000020001",
205
+ "0500070000020002",
206
+ "0500070000020003",
207
+ "0500070000020101",
208
+ "0500070000020201",
209
+ "0503000000030027",
210
+ "0503000000030028",
211
+ "0503000000030029",
212
+ "0C00010000010018",
213
+ "0C0001000002000E",
214
+ "0C00010000020015",
215
+ "0C00010000020016",
216
+ "0C00010000020019",
217
+ "0C0001000002001A",
218
+ "0C0001000002001B",
219
+ "0C0003000003001A",
220
+ "0C00040000020006",
221
+ ],
222
+ "22E": [
223
+ "0300180000000000",
224
+ "050001000001000A",
225
+ "0500010000020003",
226
+ "0500010000020008",
227
+ "0500010000020009",
228
+ "0500010000030009",
229
+ "0500030000010053",
230
+ "0500030000020010",
231
+ "0500030000020011",
232
+ "0500030000020012",
233
+ "0500030000020013",
234
+ "0500030000020014",
235
+ "0500030000020015",
236
+ "0500030000020016",
237
+ "0500030000020017",
238
+ "0500030000020018",
239
+ "0500030000030007",
240
+ "0500040000010056",
241
+ "0500040000020020",
242
+ "0500050000010008",
243
+ "0500050000010009",
244
+ "050005000001000A",
245
+ "050005000001000B",
246
+ "050005000001000C",
247
+ "0500050000020030",
248
+ "0500060000020005",
249
+ "0500060000020007",
250
+ "0500060000020008",
251
+ "0500060000020011",
252
+ "0500060000020012",
253
+ "0500060000020013",
254
+ "0500060000020014",
255
+ "0500060000020021",
256
+ "0500060000020022",
257
+ "0500060000020023",
258
+ "0500060000020024",
259
+ "0500060000020044",
260
+ "0500060000020054",
261
+ "0500060000020061",
262
+ "0500060000020062",
263
+ "05000600000200A0",
264
+ "05000600000200A1",
265
+ "0500070000020001",
266
+ "0500070000020002",
267
+ "0500070000020003",
268
+ "0500070000020101",
269
+ "0500070000020201",
270
+ "0503000000030027",
271
+ "0503000000030028",
272
+ "0503000000030029",
273
+ "0C00010000010018",
274
+ "0C0001000002000E",
275
+ "0C00010000020015",
276
+ "0C00010000020016",
277
+ "0C00010000020019",
278
+ "0C0001000002001A",
279
+ "0C0001000002001B",
280
+ "0C00040000020006",
281
+ ],
282
+ "239": [
283
+ "0300180000000000",
284
+ "0300260000000000",
285
+ "0500010000020003",
286
+ "0500010000020008",
287
+ "0500010000020009",
288
+ "0500010000030009",
289
+ "0500030000020010",
290
+ "0500030000020011",
291
+ "0500030000020012",
292
+ "0500030000020013",
293
+ "0500030000020014",
294
+ "0500030000020015",
295
+ "0500030000020016",
296
+ "0500030000020017",
297
+ "0500030000020018",
298
+ "0500030000030007",
299
+ "0500040000020020",
300
+ "0500050000010008",
301
+ "0500050000010009",
302
+ "050005000001000A",
303
+ "050005000001000B",
304
+ "050005000001000C",
305
+ "0500060000020005",
306
+ "0500060000020006",
307
+ "0500060000020007",
308
+ "0500060000020008",
309
+ "0500060000020011",
310
+ "0500060000020012",
311
+ "0500060000020013",
312
+ "0500060000020014",
313
+ "0500060000020021",
314
+ "0500060000020022",
315
+ "0500060000020023",
316
+ "0500060000020024",
317
+ "0500060000020041",
318
+ "0500060000020042",
319
+ "0500060000020043",
320
+ "0500060000020044",
321
+ "0500060000020051",
322
+ "0500060000020052",
323
+ "0500060000020053",
324
+ "0500060000020054",
325
+ "0500060000020061",
326
+ "0500060000020062",
327
+ "05000600000200A0",
328
+ "05000600000200A1",
329
+ "0C00010000020015",
330
+ "0C00010000020016",
331
+ "0C00010000020019",
332
+ "0C0001000002001A",
333
+ "0C0001000002001B",
334
+ "0C00040000020006",
335
+ ],
336
+ "31B": [
337
+ "0300180000000000",
338
+ "0300260000000000",
339
+ "050001000001000A",
340
+ "0500010000020003",
341
+ "0500010000020008",
342
+ "0500010000020009",
343
+ "0500010000030009",
344
+ "0500030000010053",
345
+ "0500030000020010",
346
+ "0500030000020011",
347
+ "0500030000020012",
348
+ "0500030000020013",
349
+ "0500030000020014",
350
+ "0500030000020015",
351
+ "0500030000020016",
352
+ "0500030000020017",
353
+ "0500030000020018",
354
+ "0500030000030007",
355
+ "0500040000010056",
356
+ "0500040000020020",
357
+ "0500050000010008",
358
+ "0500050000010009",
359
+ "050005000001000A",
360
+ "050005000001000B",
361
+ "050005000001000C",
362
+ "0500050000020030",
363
+ "0500060000020005",
364
+ "0500060000020006",
365
+ "0500060000020007",
366
+ "0500060000020008",
367
+ "0500060000020011",
368
+ "0500060000020012",
369
+ "0500060000020013",
370
+ "0500060000020014",
371
+ "0500060000020021",
372
+ "0500060000020022",
373
+ "0500060000020023",
374
+ "0500060000020024",
375
+ "0500060000020041",
376
+ "0500060000020042",
377
+ "0500060000020043",
378
+ "0500060000020044",
379
+ "0500060000020051",
380
+ "0500060000020052",
381
+ "0500060000020053",
382
+ "0500060000020054",
383
+ "0500060000020061",
384
+ "0500060000020062",
385
+ "05000600000200A0",
386
+ "05000600000200A1",
387
+ "0500070000020001",
388
+ "0500070000020002",
389
+ "0500070000020003",
390
+ "0500070000020101",
391
+ "0500070000020201",
392
+ "0503000000030027",
393
+ "0503000000030028",
394
+ "0503000000030029",
395
+ "0C00010000010018",
396
+ "0C0001000002000E",
397
+ "0C00010000020015",
398
+ "0C00010000020016",
399
+ "0C00010000020019",
400
+ "0C0001000002001A",
401
+ "0C0001000002001B",
402
+ "0C0003000003001A",
403
+ "0C00040000020006",
404
+ ],
405
+ };
@@ -21,6 +21,12 @@ export declare const MACHINE_LIMITS: Readonly<Record<string, {
21
21
  chamber: number;
22
22
  volume: readonly number[];
23
23
  }>>;
24
+ /** Printers with two physical nozzles. Bambu Studio numbers the right/main nozzle 0
25
+ * (MAIN_EXTRUDER_ID) and the left/deputy nozzle 1 (DEPUTY_EXTRUDER_ID), and reports one
26
+ * virtual external-spool slot per nozzle: 255 (VIRTUAL_TRAY_MAIN_ID) and 254
27
+ * (VIRTUAL_TRAY_DEPUTY_ID). The single-nozzle H2S is deliberately absent. */
28
+ export declare const DUAL_NOZZLE_MODELS: ReadonlySet<string>;
29
+ export declare function isDualNozzleModel(model: unknown): boolean;
24
30
  export declare function normalizeModel(value: unknown): string | undefined;
25
31
  export declare function normalizeMaterial(value: unknown): string | undefined;
26
32
  export declare function validateTemperature(component: 'nozzle' | 'bed' | 'chamber', value: unknown, model: string, materials?: string[]): number;
@@ -30,6 +30,15 @@ export const MACHINE_LIMITS = {
30
30
  h2s: { nozzle: 350, bed: 120, chamber: 65, volume: [340, 320, 340] },
31
31
  x2d: { nozzle: 300, bed: 120, chamber: 65, volume: [256, 256, 260] },
32
32
  };
33
+ /** Printers with two physical nozzles. Bambu Studio numbers the right/main nozzle 0
34
+ * (MAIN_EXTRUDER_ID) and the left/deputy nozzle 1 (DEPUTY_EXTRUDER_ID), and reports one
35
+ * virtual external-spool slot per nozzle: 255 (VIRTUAL_TRAY_MAIN_ID) and 254
36
+ * (VIRTUAL_TRAY_DEPUTY_ID). The single-nozzle H2S is deliberately absent. */
37
+ export const DUAL_NOZZLE_MODELS = new Set(['h2d', 'h2dpro', 'h2c', 'x2d']);
38
+ export function isDualNozzleModel(model) {
39
+ const normalized = normalizeModel(model);
40
+ return normalized !== undefined && DUAL_NOZZLE_MODELS.has(normalized);
41
+ }
33
42
  export function normalizeModel(value) {
34
43
  if (typeof value !== 'string')
35
44
  return undefined;
@@ -12,6 +12,8 @@ export interface PrintFileInspection {
12
12
  selectsAms: boolean;
13
13
  nozzleTypes?: string[];
14
14
  nozzleFlows?: string[];
15
+ /** Physical nozzle index for each declared filament, composed from the selected plate headers. */
16
+ filamentNozzleIndices?: number[];
15
17
  }
16
18
  /** Inspect the exact selected plate. This does not simulate firmware or arbitrary motion:
17
19
  * only declared object bounds are checked, never purge/homing/wipe travel coordinates.