bambu-printer-mcp 1.1.26 → 1.1.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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();
@@ -372,10 +377,14 @@ export async function inspectPrintFile(filePath, options) {
372
377
  if (/[{}\[\]#]/.test(line))
373
378
  fail('unresolved dynamic command syntax');
374
379
  if (/^SYNC(?=$|\s)/i.test(line)) {
375
- // H2D change_filament_gcode emits SYNC T{ceil(flush_length / 125) * 5}.
376
- // T is a synchronization duration here, not a nozzle temperature. The
377
- // vendor template establishes no independent maximum duration.
380
+ // Vendor SYNC T is a duration, not a heater target. X2D and P2S
381
+ // startup templates additionally emit R0; no other R form is supported.
378
382
  const args = parameters(line.slice(4));
383
+ if (args.has('R')) {
384
+ if (!['x2d', 'p2s'].includes(model) || args.get('R') !== 0)
385
+ fail('unsupported SYNC reset parameters');
386
+ args.delete('R');
387
+ }
379
388
  if (args.size !== 1 || !args.has('T') || !Number.isFinite(args.get('T')) || args.get('T') < 0)
380
389
  fail('unsupported SYNC duration parameters');
381
390
  commandCount++;
@@ -388,6 +397,33 @@ export async function inspectPrintFile(filePath, options) {
388
397
  const code = command[1].toUpperCase() + String(Number(base)) + (subcode === undefined ? '' : '.' + subcode);
389
398
  const argumentsText = line.slice(command[0].length).trim();
390
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
+ }
391
427
  if (!['M117', 'M118', 'M1002', 'M1006', 'M900', 'M970', 'M983.1', 'G383'].includes(code) && /[GM]\s*\d/i.test(argumentsText))
392
428
  fail('multiple commands on one line are unsupported');
393
429
  // Coordinated extrusion closes the startup-only allowance even when the
@@ -574,7 +610,21 @@ export async function inspectPrintFile(filePath, options) {
574
610
  for (const key of args.keys())
575
611
  if (!'SLT'.includes(key))
576
612
  fail(`unsupported M620.17 parameter ${key}`);
577
- 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')));
578
628
  }
579
629
  else if ((/^M620\./.test(code) && !['M620.3', 'M620.6', 'M620.11'].includes(code)) || /^M621\./.test(code)) {
580
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)
@@ -0,0 +1,2 @@
1
+ /** Discover vendor subfolders without following links into another profile tree. */
2
+ export declare function profileJsonFiles(directory: string): string[];
@@ -0,0 +1,24 @@
1
+ import { lstatSync, readdirSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ /** Discover vendor subfolders without following links into another profile tree. */
4
+ export function profileJsonFiles(directory) {
5
+ let entries;
6
+ try {
7
+ if (lstatSync(directory).isSymbolicLink())
8
+ return [];
9
+ entries = readdirSync(directory, { withFileTypes: true });
10
+ }
11
+ catch (error) {
12
+ if (error.code === 'ENOENT')
13
+ return [];
14
+ throw error;
15
+ }
16
+ entries.sort((a, b) => a.name.localeCompare(b.name));
17
+ const files = entries.filter(entry => entry.isFile() && entry.name.endsWith('.json'))
18
+ .map(entry => path.join(directory, entry.name));
19
+ for (const entry of entries) {
20
+ if (entry.isDirectory())
21
+ files.push(...profileJsonFiles(path.join(directory, entry.name)));
22
+ }
23
+ return files;
24
+ }