bambu-printer-mcp 1.1.27 → 1.1.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
+ };
@@ -62,6 +62,11 @@ function stripComments(line) {
62
62
  /** A deliberately narrow parameter lexer for commands affecting heat/material selection.
63
63
  * Unknown nonthermal vendor commands do not require a global G-code allowlist.
64
64
  */
65
+ /** True when the parameter is present, finite and within [low, high]. */
66
+ function inRange(args, key, low, high) {
67
+ const value = args.get(key);
68
+ return value !== undefined && Number.isFinite(value) && value >= low && value <= high;
69
+ }
65
70
  function parameters(text, flags = '') {
66
71
  const result = new Map();
67
72
  let rest = text.trim();
@@ -392,6 +397,33 @@ export async function inspectPrintFile(filePath, options) {
392
397
  const code = command[1].toUpperCase() + String(Number(base)) + (subcode === undefined ? '' : '.' + subcode);
393
398
  const argumentsText = line.slice(command[0].length).trim();
394
399
  commandCount++;
400
+ if (code === 'M9711') {
401
+ // Official X2D and H2D time-lapse capture (Bambu Studio and OrcaSlicer
402
+ // templates), emitted on every layer: M is the time-lapse mode and E the
403
+ // physical extruder, not a second command; T is a duration, not a heater.
404
+ // It sets no temperature. Positions stay inside the machine volume.
405
+ const invalid = () => fail('unsupported M9711 time-lapse parameters');
406
+ if (!['x2d', 'h2d'].includes(model))
407
+ invalid();
408
+ let args;
409
+ try {
410
+ args = parameters(argumentsText);
411
+ }
412
+ catch {
413
+ return invalid();
414
+ }
415
+ const [width, depth, height] = MACHINE_LIMITS[model].volume;
416
+ const position = ['X', 'Y', 'U', 'V'].filter(key => args.has(key)).join('');
417
+ const limit = { X: width, U: width, Y: depth, V: depth };
418
+ if ([...args.keys()].some(key => !'MEXYUVZSCOT'.includes(key)) ||
419
+ // X2D and H2D have two physical extruders, whatever the file declares.
420
+ ![0, 1].includes(args.get('M')) || !Number.isInteger(args.get('E')) || !inRange(args, 'E', 0, Math.min(nozzleDiameters.length, 2) - 1) ||
421
+ args.get('S') !== 11 || args.get('C') !== 10 || args.get('O') !== 0 || !inRange(args, 'T', 0, 600000) ||
422
+ !inRange(args, 'Z', 0, height) ||
423
+ !['', 'XY', 'UV'].includes(position) || [...position].some(key => !inRange(args, key, 0, limit[key])))
424
+ invalid();
425
+ continue;
426
+ }
395
427
  if (!['M117', 'M118', 'M1002', 'M1006', 'M900', 'M970', 'M983.1', 'G383'].includes(code) && /[GM]\s*\d/i.test(argumentsText))
396
428
  fail('multiple commands on one line are unsupported');
397
429
  // Coordinated extrusion closes the startup-only allowance even when the
@@ -578,7 +610,21 @@ export async function inspectPrintFile(filePath, options) {
578
610
  for (const key of args.keys())
579
611
  if (!'SLT'.includes(key))
580
612
  fail(`unsupported M620.17 parameter ${key}`);
581
- heat('nozzle', args.get('S'), requirePosition(args.get('L')));
613
+ if (args.get('L') === -1 && model === 'x2d') {
614
+ // OrcaSlicer's X2D start G-code preheats a nozzle with no assigned
615
+ // filament (L-1) for toolhead-offset calibration; Bambu Studio writes
616
+ // L0 there. No project filament is selected, so check the target
617
+ // against the hardware limit and every declared material, like other
618
+ // heater commands without a verified filament. That is never looser
619
+ // than an accepted L<position> form. Material left in the nozzle from
620
+ // an earlier job is live state that no file can declare. T names one
621
+ // of the two physical nozzles, whatever the file declares.
622
+ if (args.get('T') > 1)
623
+ fail('unsupported M620.17 nozzle/material temperature mapping');
624
+ heat('nozzle', args.get('S'), undefined, true);
625
+ }
626
+ else
627
+ heat('nozzle', args.get('S'), requirePosition(args.get('L')));
582
628
  }
583
629
  else if ((/^M620\./.test(code) && !['M620.3', 'M620.6', 'M620.11'].includes(code)) || /^M621\./.test(code)) {
584
630
  fail(`unsupported thermal-affecting filament command ${code}`);
@@ -1,4 +1,5 @@
1
1
  import { BambuClient } from "bambu-node";
2
+ export declare const SERIAL_MODELS: Record<string, string>;
2
3
  /** Only raw reports received after this request are evidence. Never read printer.data here. */
3
4
  export declare function readFreshPrinterStatus(printer: BambuClient, serial: string, timeoutMs?: number): Promise<any>;
4
5
  export interface PrinterStateRequirements {
@@ -36,6 +37,14 @@ export interface ValidatedPrinterState {
36
37
  reportedNozzles: ReportedNozzle[];
37
38
  filaments: ReportedFilament[];
38
39
  }
40
+ /** The one key format for a reported entry, shared by the gate and clear_hms_errors. */
41
+ export declare function reportedHmsKey(entry: unknown): string;
42
+ /** Bambu's display form of a cleared-code key such as "hms:83887616:131184"; other keys unchanged. */
43
+ export declare function describeHmsKey(key: string): string;
44
+ /** Labels of reported entries that pass only because Bambu hides them for this printer. */
45
+ export declare function hiddenHmsNotices(raw: unknown, serial: string, model: string): string[];
46
+ /** Cleared-code keys of reported hidden entries; clearing never removes them, so they are not re-acknowledged. */
47
+ export declare function hiddenHmsKeys(raw: unknown, serial: string, model: string): string[];
39
48
  /** Verifies reported configuration, not a physical nozzle/spool inspection. */
40
49
  export declare function validatePrinterState(status: any, requirements: PrinterStateRequirements): ValidatedPrinterState;
41
50
  /** Manual M104 cannot remap filament slots. Validate the material already at the nozzle. */
@@ -1,3 +1,4 @@
1
+ import { HIDDEN_HMS } from "./hidden-hms.js";
1
2
  import { GetVersionCommand, PushAllCommand } from "bambu-node";
2
3
  import { normalizeMaterial, normalizeModel } from "./limits.js";
3
4
  const MAX_REPORT_AGE_MS = 15000;
@@ -5,7 +6,7 @@ const MODEL_IDS = {
5
6
  O1C: "H2C", O1C2: "H2C", O1D: "H2D", O1E: "H2D Pro", O1S: "H2S", N6: "X2D",
6
7
  N2S: "A1", A1M: "A1 Mini", C11: "P1P", C12: "P1S", C13: "X1E", "BL-P001": "X1C", "BL-P002": "X1",
7
8
  };
8
- const SERIAL_MODELS = {
9
+ export const SERIAL_MODELS = {
9
10
  "093": "H2S", "094": "H2D", "31B": "H2C", "239": "H2D Pro", "20P": "X2D",
10
11
  "00M": "X1C", "00W": "X1", "03W": "X1E", "01S": "P1P", "01P": "P1S", "22E": "P2S", "039": "A1", "030": "A1 Mini",
11
12
  };
@@ -184,7 +185,77 @@ function reportedNozzles(raw) {
184
185
  }
185
186
  return output;
186
187
  }
187
- function validateErrors(raw, requireIdle) {
188
+ const hiddenHmsSets = new Map(Object.entries(HIDDEN_HMS).map(([prefix, codes]) => [prefix, new Set(codes)]));
189
+ function hmsCode(entry) {
190
+ const code = finiteNumber(entry?.code);
191
+ const attr = finiteNumber(entry?.attr);
192
+ const valid = (value) => value !== undefined && Number.isInteger(value) && value >= 0 && value <= 0xffffffff;
193
+ if (!valid(code) || !valid(attr))
194
+ return undefined;
195
+ // BambuStudio DevHMS.cpp: code >> 16; DevHMS.h: 1=fatal,2=serious,3=common,4=info.
196
+ return { attr, code, severity: Math.floor(code / 65536), hex: (attr.toString(16).padStart(8, "0") + code.toString(16).padStart(8, "0")).toUpperCase() };
197
+ }
198
+ const hmsLabel = (hex) => `HMS ${hex.slice(0, 4)}-${hex.slice(4, 8)}-${hex.slice(8, 12)}-${hex.slice(12)}`;
199
+ const hmsKey = (entry) => `hms:${entry.attr}:${entry.code}`;
200
+ /** The one key format for a reported entry, shared by the gate and clear_hms_errors. */
201
+ export function reportedHmsKey(entry) {
202
+ const parsed = hmsCode(entry);
203
+ return parsed ? hmsKey(parsed) : `hms:${entry?.attr}:${entry?.code}`;
204
+ }
205
+ /** Bambu's display form of a cleared-code key such as "hms:83887616:131184"; other keys unchanged. */
206
+ export function describeHmsKey(key) {
207
+ const match = /^hms:(\d+):(\d+)$/.exec(key);
208
+ const entry = match ? hmsCode({ attr: Number(match[1]), code: Number(match[2]) }) : undefined;
209
+ return entry ? hmsLabel(entry.hex) : key;
210
+ }
211
+ /**
212
+ * A serious or common entry that Bambu lists for this serial prefix with empty
213
+ * text: Bambu Studio, Bambu Handy and the printer screen never show it, and
214
+ * the printer keeps printing with it (an X2D raises 0500-0600-0002-0070 at
215
+ * every boot). Bambu Studio selects the table by serial prefix, so this does
216
+ * too, but only when that prefix belongs to the verified model: a sparse report
217
+ * without a serial must not let a misconfigured serial pick another model's
218
+ * table. Fatal, invalid-level and unlisted entries never qualify.
219
+ */
220
+ function isHiddenNotice(serial, model, entry) {
221
+ if (entry.severity !== 2 && entry.severity !== 3)
222
+ return false;
223
+ const prefix = serial.trim().toUpperCase().slice(0, 3);
224
+ const normalized = normalizeModel(model);
225
+ if (!normalized || normalizeModel(SERIAL_MODELS[prefix] ?? "") !== normalized)
226
+ return false;
227
+ return hiddenHmsSets.get(prefix)?.has(entry.hex) === true;
228
+ }
229
+ function hiddenHmsEntries(raw, serial, model) {
230
+ if (!object(raw))
231
+ return [];
232
+ const identity = observedIdentity(raw, serial);
233
+ if (!identity.observedSerial || identity.model !== normalizeModel(model))
234
+ return [];
235
+ const entries = Array.isArray(raw.hms) ? raw.hms : [];
236
+ const seen = new Set();
237
+ return entries.map(hmsCode).filter((entry) => !!entry && isHiddenNotice(identity.observedSerial, model, entry) && !seen.has(entry.hex) && !!seen.add(entry.hex));
238
+ }
239
+ /** Labels of reported entries that pass only because Bambu hides them for this printer. */
240
+ export function hiddenHmsNotices(raw, serial, model) {
241
+ return hiddenHmsEntries(raw, serial, model).map(entry => hmsLabel(entry.hex));
242
+ }
243
+ /** Cleared-code keys of reported hidden entries; clearing never removes them, so they are not re-acknowledged. */
244
+ export function hiddenHmsKeys(raw, serial, model) {
245
+ return hiddenHmsEntries(raw, serial, model).map(hmsKey);
246
+ }
247
+ const loggedHiddenNotices = new Set();
248
+ /** Name each hidden entry a check passed once per printer on stderr, so headless runs still record it. */
249
+ function logHiddenNotices(raw, serial, model) {
250
+ for (const entry of hiddenHmsEntries(raw, serial, model)) {
251
+ const key = `${serial}:${entry.hex}`;
252
+ if (loggedHiddenNotices.has(key))
253
+ continue;
254
+ loggedHiddenNotices.add(key);
255
+ console.error(`Printer ${serial} reports ${hmsLabel(entry.hex)}, which Bambu lists for it without text and never shows as a fault; it does not block operations.`);
256
+ }
257
+ }
258
+ function validateErrors(raw, requireIdle, serial, model) {
188
259
  const state = typeof raw.gcode_state === "string" ? raw.gcode_state.toUpperCase() : "UNKNOWN";
189
260
  if (requireIdle && !["IDLE", "FINISH"].includes(state))
190
261
  throw new Error(`Printer state ${state} is not safely idle.`);
@@ -194,18 +265,14 @@ function validateErrors(raw, requireIdle) {
194
265
  throw new Error("Printer reports a print error or did not report its error state.");
195
266
  if (!Array.isArray(raw.hms))
196
267
  throw new Error("Printer HMS error state is missing or malformed.");
197
- for (const entry of raw.hms) {
198
- const code = finiteNumber(entry?.code);
199
- const attr = finiteNumber(entry?.attr);
200
- // BambuStudio DevHMS.cpp: code >> 16; DevHMS.h: 1=fatal,2=serious,3=common,4=info.
268
+ for (const reported of raw.hms) {
269
+ const entry = hmsCode(reported);
201
270
  // Unknown severities fail closed; attr identifies the module and is not a severity bitmask.
202
- if (code === undefined || attr === undefined || !Number.isInteger(code) || !Number.isInteger(attr) || code < 0 || code > 0xffffffff || attr < 0 || attr > 0xffffffff || Math.floor(code / 65536) !== 4) {
203
- const hex = (value) => value.toString(16).toUpperCase().padStart(8, "0");
204
- const label = code !== undefined && attr !== undefined && Number.isInteger(code) && Number.isInteger(attr) && code >= 0 && attr >= 0
205
- ? ` (HMS ${hex(attr).slice(0, 4)}-${hex(attr).slice(4)}-${hex(code).slice(0, 4)}-${hex(code).slice(4)})` : "";
206
- throw new Error(`Printer reports actionable or unknown HMS errors${label}; inspect and resolve them before proceeding.`);
207
- }
271
+ if (entry && (entry.severity === 4 || isHiddenNotice(serial, model, entry)))
272
+ continue;
273
+ throw new Error(`Printer reports actionable or unknown HMS errors${entry ? ` (${hmsLabel(entry.hex)})` : ""}; inspect and resolve them before proceeding.`);
208
274
  }
275
+ logHiddenNotices(raw, serial, model);
209
276
  }
210
277
  function trayRange(value, label) {
211
278
  if (value === undefined || value === null || value === "" || value === "0" || value === 0)
@@ -265,7 +332,7 @@ export function validatePrinterState(status, requirements) {
265
332
  const model = normalizeModel(requirements.model);
266
333
  if (!model || !identity.model || model !== identity.model)
267
334
  throw new Error(`Printer model identity mismatch: requested ${requirements.model}, observed ${identity.model ?? "unknown"}.`);
268
- validateErrors(raw, requirements.requireIdle !== false);
335
+ validateErrors(raw, requirements.requireIdle !== false, identity.observedSerial ?? "", model);
269
336
  if (requirements.requireLoadedFilament && (raw.ams?.tray_now !== undefined || Array.isArray(raw.ams?.ams))) {
270
337
  const current = finiteNumber(raw.ams?.tray_now);
271
338
  if (current === 255)
@@ -228,6 +228,29 @@ function enforceMatchingNozzles(arr) {
228
228
  }
229
229
  }
230
230
  }
231
+ /**
232
+ * BambuStudio picks each per-variant machine, process, and filament value
233
+ * (e.g. filament_max_volumetric_speed) by "<extruder_type> <nozzle_volume_type>"
234
+ * among the variants each extruder declares in extruder_variant_list. A flow
235
+ * type the preset doesn't declare has no values to pick, so refuse it instead
236
+ * of labelling a job for a nozzle it wasn't sliced for.
237
+ */
238
+ function assertNozzleVariantDeclared(flat, volumeType, machineLeaf) {
239
+ const strings = (key) => Array.isArray(flat[key]) ? flat[key].map(String) : [];
240
+ const extruderTypes = strings("extruder_type");
241
+ const declaredLists = strings("extruder_variant_list");
242
+ const count = flat["nozzle_volume_type"].length;
243
+ for (let i = 0; i < count; i++) {
244
+ const extruderType = extruderTypes[i] ?? extruderTypes[0];
245
+ const declared = (declaredLists[i] ?? declaredLists[0] ?? "").split(",").map((v) => v.trim()).filter(Boolean);
246
+ const matches = (variant) => extruderType ? variant === `${extruderType} ${volumeType}` : variant.endsWith(` ${volumeType}`);
247
+ // Presets without variant declarations only have Standard values.
248
+ if (declared.length === 0 ? volumeType === "Standard" : declared.some(matches))
249
+ continue;
250
+ throw new Error(`Slicing for a ${volumeType} nozzle is refused: "${machineLeaf}" declares no ${volumeType} variant ` +
251
+ `for extruder ${i + 1} (declared: ${declared.join(", ") || "none"}).`);
252
+ }
253
+ }
231
254
  /** Best-effort extruder count for fallback nozzle_volume_type sizing. */
232
255
  /** BambuStudio's built-in filament_colour default. */
233
256
  export const DEFAULT_FILAMENT_COLOUR = "#00AE42";
@@ -545,6 +568,8 @@ export async function flattenForCli(opts) {
545
568
  });
546
569
  // CLI-specific post-processing on machine profile only.
547
570
  deriveNozzleVolumeType(machineFlat, opts.nozzleVolumeType);
571
+ if (opts.nozzleVolumeType)
572
+ assertNozzleVariantDeclared(machineFlat, opts.nozzleVolumeType, opts.machineLeaf);
548
573
  applyMachineModelBedMetadata(machineFlat, machineIndex);
549
574
  // A standalone custom machine can accompany process/filament profiles that
550
575
  // need BBL resolution; do not impose bundled model config on that machine.
@@ -39,6 +39,7 @@ export interface BambuSliceOptions {
39
39
  filamentColours?: string[];
40
40
  bedType?: string;
41
41
  nozzleType?: string;
42
+ nozzleVolumeType?: 'Standard' | 'High Flow';
42
43
  enableTimelapse?: boolean;
43
44
  allowMixTemp?: boolean;
44
45
  scale?: number;