mcp-3d-printer-server 1.2.24 → 1.2.26

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 (96) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/CONTRIBUTORS.md +5 -1
  3. package/README.md +64 -29
  4. package/dist/ams-mapping.d.ts +38 -0
  5. package/dist/ams-mapping.js +125 -0
  6. package/dist/bambu-network-bridge.d.ts +200 -0
  7. package/dist/bambu-network-bridge.js +881 -0
  8. package/dist/bambu-network-certs.d.ts +13 -0
  9. package/dist/bambu-network-certs.js +152 -0
  10. package/dist/control-reply.d.ts +21 -0
  11. package/dist/control-reply.js +44 -0
  12. package/dist/counterpart/camera-sources.d.ts +16 -0
  13. package/dist/counterpart/camera-sources.js +138 -0
  14. package/dist/counterpart/camera-stream.d.ts +66 -0
  15. package/dist/counterpart/camera-stream.js +246 -0
  16. package/dist/counterpart/camera-tools.d.ts +247 -0
  17. package/dist/counterpart/camera-tools.js +35 -0
  18. package/dist/counterpart/contract.d.ts +21 -5
  19. package/dist/counterpart/contract.js +8 -2
  20. package/dist/counterpart/route.d.ts +31 -0
  21. package/dist/counterpart/route.js +70 -0
  22. package/dist/counterpart/service.d.ts +60 -4
  23. package/dist/counterpart/service.js +142 -16
  24. package/dist/design-guidance.d.ts +26 -0
  25. package/dist/design-guidance.js +177 -0
  26. package/dist/fulu-orca.d.ts +7 -0
  27. package/dist/fulu-orca.js +18 -2
  28. package/dist/index.js +528 -72
  29. package/dist/printers/bambu-filaments.d.ts +68 -0
  30. package/dist/printers/bambu-filaments.js +262 -0
  31. package/dist/printers/bambu.d.ts +99 -6
  32. package/dist/printers/bambu.js +478 -72
  33. package/dist/printers/creality.d.ts +2 -1
  34. package/dist/printers/creality.js +5 -1
  35. package/dist/printers/generic-base.d.ts +11 -6
  36. package/dist/printers/generic-base.js +21 -25
  37. package/dist/printers/klipper.d.ts +2 -1
  38. package/dist/printers/klipper.js +10 -7
  39. package/dist/printers/octoprint.d.ts +2 -1
  40. package/dist/printers/octoprint.js +15 -8
  41. package/dist/printers/printer-factory.d.ts +1 -1
  42. package/dist/printers/printer-factory.js +3 -3
  43. package/dist/printers/prusa.js +0 -1
  44. package/dist/safety/confirmation.d.ts +11 -16
  45. package/dist/safety/confirmation.js +8 -32
  46. package/dist/safety/hidden-hms.d.ts +32 -0
  47. package/dist/safety/hidden-hms.js +405 -0
  48. package/dist/safety/limits.d.ts +8 -2
  49. package/dist/safety/limits.js +14 -5
  50. package/dist/safety/print-file.d.ts +5 -0
  51. package/dist/safety/print-file.js +156 -19
  52. package/dist/safety/printer-state.d.ts +59 -8
  53. package/dist/safety/printer-state.js +174 -39
  54. package/dist/slicer/bambu-cli.d.ts +6 -2
  55. package/dist/slicer/bambu-cli.js +19 -12
  56. package/dist/slicer/layer-pauses.d.ts +47 -0
  57. package/dist/slicer/layer-pauses.js +130 -0
  58. package/dist/slicer/slicer-error.d.ts +11 -1
  59. package/dist/slicer/slicer-error.js +16 -2
  60. package/dist/stl/stl-manipulator.d.ts +27 -1
  61. package/dist/stl/stl-manipulator.js +216 -76
  62. package/dist/studio-print-options.d.ts +57 -0
  63. package/dist/studio-print-options.js +179 -0
  64. package/package.json +25 -4
  65. package/server.json +2 -2
  66. package/src/ams-mapping.ts +132 -0
  67. package/src/bambu-network-bridge.ts +1075 -0
  68. package/src/bambu-network-certs.ts +154 -0
  69. package/src/control-reply.ts +41 -0
  70. package/src/counterpart/camera-sources.ts +102 -0
  71. package/src/counterpart/camera-stream.ts +195 -0
  72. package/src/counterpart/camera-tools.ts +35 -0
  73. package/src/counterpart/contract.ts +12 -4
  74. package/src/counterpart/route.ts +82 -0
  75. package/src/counterpart/service.ts +150 -16
  76. package/src/design-guidance.ts +188 -0
  77. package/src/fulu-orca.ts +18 -2
  78. package/src/index.ts +546 -87
  79. package/src/printers/bambu-filaments.ts +346 -0
  80. package/src/printers/bambu.ts +506 -81
  81. package/src/printers/creality.ts +8 -3
  82. package/src/printers/generic-base.ts +33 -40
  83. package/src/printers/klipper.ts +12 -8
  84. package/src/printers/octoprint.ts +16 -9
  85. package/src/printers/printer-factory.ts +3 -3
  86. package/src/printers/prusa.ts +0 -1
  87. package/src/safety/confirmation.ts +15 -38
  88. package/src/safety/hidden-hms.ts +406 -0
  89. package/src/safety/limits.ts +15 -5
  90. package/src/safety/print-file.ts +139 -17
  91. package/src/safety/printer-state.ts +179 -40
  92. package/src/slicer/bambu-cli.ts +22 -13
  93. package/src/slicer/layer-pauses.ts +143 -0
  94. package/src/slicer/slicer-error.ts +21 -2
  95. package/src/stl/stl-manipulator.ts +254 -84
  96. package/src/studio-print-options.ts +201 -0
@@ -1,6 +1,7 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { readBoundedPrintFile, loadSafe3mfArchive } from './archive.js';
3
- import { MACHINE_LIMITS, normalizeMaterial, normalizeModel, validateStartupPurgeTemperature, validateTemperature } from './limits.js';
3
+ import { projectFilaments } from "../ams-mapping.js";
4
+ import { MACHINE_LIMITS, isDualNozzleModel, normalizeMaterial, normalizeModel, validateStartupPurgeTemperature, validateTemperature } from './limits.js';
4
5
  const NUMBER = /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)$/;
5
6
  const fail = (message) => { throw new Error(`Print safety: ${message}`); };
6
7
  function list(value) {
@@ -35,6 +36,67 @@ function consistent(values, convert, field) {
35
36
  return fail(`contradictory ${field} metadata`);
36
37
  return converted[0];
37
38
  }
39
+ /** Bambu Studio keeps filament options per variant in the project, but writes
40
+ * one selected row per filament in the plate header (Print::full_print_config).
41
+ * Match the declared 1-based self index and variant, never truncate the table
42
+ * or borrow the slicer's row-zero fallback for a missing selection.
43
+ */
44
+ function firstLayerTemperatures(project, headers, count) {
45
+ const key = 'nozzle_temperature_initial_layer';
46
+ const field = 'first-layer nozzle temperature';
47
+ const temperatures = (value) => {
48
+ const entries = list(value);
49
+ return entries.length && entries.every(v => NUMBER.test(v) && Number.isFinite(Number(v))) ? entries.map(Number) : undefined;
50
+ };
51
+ const perFilament = (value) => {
52
+ const values = temperatures(value);
53
+ return values?.length === count ? values : undefined;
54
+ };
55
+ const selectedValues = metadataValues(headers, [key]);
56
+ if (project[key] === undefined)
57
+ return consistent(selectedValues, perFilament, field);
58
+ if (project.filament_extruder_variant === undefined && project.filament_self_index === undefined)
59
+ return consistent([project[key], ...selectedValues], perFilament, field);
60
+ const table = temperatures(project[key]);
61
+ const variants = list(project.filament_extruder_variant);
62
+ const ids = list(project.filament_self_index);
63
+ const validVariant = (value) => /^(Direct Drive|Bowden) (Standard|High Flow|Hybrid|TPU High Flow|E3D High Flow)$/.test(value);
64
+ if (!table || variants.length !== table.length || ids.length !== table.length ||
65
+ variants.some(v => !validVariant(v)) || ids.some(id => !/^[1-9]\d*$/.test(id) || Number(id) > count))
66
+ return fail(`unknown or malformed ${field} variant table`);
67
+ const selectedVariants = consistent(metadataValues(headers, ['filament_extruder_variant']), value => {
68
+ const values = list(value);
69
+ return values.length === count && values.every(validVariant) ? values : undefined;
70
+ }, 'selected filament extruder variant');
71
+ const selectedIds = consistent(metadataValues(headers, ['filament_self_index']), value => {
72
+ const values = list(value);
73
+ return values.length === count && values.every((id, index) => id === String(index + 1)) ? values : undefined;
74
+ }, 'selected filament self index');
75
+ // These are logical extruders: physical_extruder_map is used for spool
76
+ // routing, not this variant lookup. Hybrid selects the Standard variant.
77
+ const mappingKeys = ['filament_map', 'extruder_type', 'filament_volume_map'];
78
+ if (mappingKeys.every(key => metadataValues(headers, [key]).length)) {
79
+ const selection = (key) => consistent(metadataValues(headers, [key]), value => {
80
+ const entries = list(value);
81
+ return entries.length ? entries : undefined;
82
+ }, `selected ${key.replace(/_/g, ' ')}`);
83
+ const map = selection('filament_map');
84
+ const types = selection('extruder_type');
85
+ const volumes = selection('filament_volume_map');
86
+ const flows = { '0': 'Standard', '1': 'High Flow', '2': 'Standard', '3': 'TPU High Flow', '5': 'E3D High Flow' };
87
+ if (map.length !== count || volumes.length !== count || map.some(id => !/^[1-9]\d*$/.test(id) || Number(id) > types.length) ||
88
+ volumes.some(v => !Object.prototype.hasOwnProperty.call(flows, v)) || types.some(v => !['Direct Drive', 'Bowden'].includes(v)) ||
89
+ selectedVariants.some((variant, i) => variant !== `${types[Number(map[i]) - 1]} ${flows[volumes[i]]}`))
90
+ return fail('contradictory selected filament extruder variant mapping');
91
+ }
92
+ const resolved = selectedIds.map((id, index) => {
93
+ const rows = ids.flatMap((candidate, row) => candidate === id && variants[row] === selectedVariants[index] ? [row] : []);
94
+ if (rows.length !== 1)
95
+ return fail(`ambiguous or missing ${field} variant for filament ${id}`);
96
+ return table[rows[0]];
97
+ });
98
+ return consistent([resolved, ...selectedValues], perFilament, field);
99
+ }
38
100
  function stripComments(line) {
39
101
  let clean = '';
40
102
  let depth = 0;
@@ -62,6 +124,11 @@ function stripComments(line) {
62
124
  /** A deliberately narrow parameter lexer for commands affecting heat/material selection.
63
125
  * Unknown nonthermal vendor commands do not require a global G-code allowlist.
64
126
  */
127
+ /** True when the parameter is present, finite and within [low, high]. */
128
+ function inRange(args, key, low, high) {
129
+ const value = args.get(key);
130
+ return value !== undefined && Number.isFinite(value) && value >= low && value <= high;
131
+ }
65
132
  function parameters(text, flags = '') {
66
133
  const result = new Map();
67
134
  let rest = text.trim();
@@ -163,6 +230,7 @@ export async function inspectPrintFile(filePath, options) {
163
230
  const model = normalizeModel(options.model);
164
231
  if (!model)
165
232
  return fail(`unknown or missing printer model '${options.model}'`);
233
+ const dualNozzle = isDualNozzleModel(model);
166
234
  const plateIndex = options.plateIndex ?? 0;
167
235
  if (!Number.isInteger(plateIndex) || plateIndex < 0)
168
236
  return fail('selected plate index must be a nonnegative integer');
@@ -303,6 +371,24 @@ export async function inspectPrintFile(filePath, options) {
303
371
  const values = list(value);
304
372
  return values.length && values.every(v => !!normalizeMaterial(v)) ? values.map(v => normalizeMaterial(v)) : undefined;
305
373
  }, 'filament material');
374
+ // GCode.cpp uses 1-based filament_map logical extruders and then indexes
375
+ // physical_extruder_map to select the physical nozzle. Only this selected
376
+ // plate's headers establish that binding; unsliced project defaults do not.
377
+ const headerMap = (key, count, low, high) => {
378
+ const values = metadataValues(headers, [key]);
379
+ if (!values.length)
380
+ return undefined;
381
+ return consistent(values, value => {
382
+ const entries = list(value);
383
+ return entries.length === count && entries.every(v => NUMBER.test(v) && Number.isInteger(Number(v)) && Number(v) >= low && Number(v) <= high)
384
+ ? entries.map(Number) : undefined;
385
+ }, key.replace(/_/g, ' '));
386
+ };
387
+ const physicalExtruderMap = headerMap('physical_extruder_map', nozzleDiameters.length, 0, dualNozzle ? Math.min(nozzleDiameters.length, 2) - 1 : nozzleDiameters.length - 1);
388
+ if (physicalExtruderMap && new Set(physicalExtruderMap).size !== physicalExtruderMap.length)
389
+ fail('physical extruder map must be a complete permutation');
390
+ const filamentMap = headerMap('filament_map', materials.length, 1, nozzleDiameters.length);
391
+ const filamentNozzleIndices = filamentMap && physicalExtruderMap ? filamentMap.map(logical => physicalExtruderMap[logical - 1]) : undefined;
306
392
  const bedValues = metadataValues([...metadata, plate], ['curr_bed_type', 'bed_type']);
307
393
  const bedType = bedValues.length ? consistent(bedValues, normalizeBed, 'bed type') : undefined;
308
394
  if (options.bedType && (!bedType || normalizeBed(options.bedType) !== bedType))
@@ -365,17 +451,25 @@ export async function inspectPrintFile(filePath, options) {
365
451
  let line = stripComments(lines[index]);
366
452
  if (!line || line === '%')
367
453
  continue;
368
- // The official H2C template emits this literal separator without a semicolon.
454
+ // Accept only the exact vendor banners in their documented model/scope.
455
+ // X2D emits its filament-change banner throughout a print; H2C's record
456
+ // separator stays inside its explicitly bounded startup block.
457
+ if (line === '======== X2D filament_change gcode ==========' && model === 'x2d')
458
+ continue;
369
459
  if (line === '========== record data ==========' && h2cStartup(index))
370
460
  continue;
371
461
  line = line.replace(/^N\d+\s*/i, '').replace(/\*\d+\s*$/, '');
372
462
  if (/[{}\[\]#]/.test(line))
373
463
  fail('unresolved dynamic command syntax');
374
464
  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.
465
+ // Vendor SYNC T is a duration, not a heater target. X2D and P2S
466
+ // startup templates additionally emit R0; no other R form is supported.
378
467
  const args = parameters(line.slice(4));
468
+ if (args.has('R')) {
469
+ if (!['x2d', 'p2s'].includes(model) || args.get('R') !== 0)
470
+ fail('unsupported SYNC reset parameters');
471
+ args.delete('R');
472
+ }
379
473
  if (args.size !== 1 || !args.has('T') || !Number.isFinite(args.get('T')) || args.get('T') < 0)
380
474
  fail('unsupported SYNC duration parameters');
381
475
  commandCount++;
@@ -388,6 +482,33 @@ export async function inspectPrintFile(filePath, options) {
388
482
  const code = command[1].toUpperCase() + String(Number(base)) + (subcode === undefined ? '' : '.' + subcode);
389
483
  const argumentsText = line.slice(command[0].length).trim();
390
484
  commandCount++;
485
+ if (code === 'M9711') {
486
+ // Official X2D, H2D and H2C time-lapse capture (Bambu Studio and OrcaSlicer
487
+ // templates), emitted on every layer: M is the time-lapse mode and E the
488
+ // physical extruder, not a second command; T is a duration, not a heater.
489
+ // It sets no temperature. Positions stay inside the machine volume.
490
+ const invalid = () => fail('unsupported M9711 time-lapse parameters');
491
+ if (!['x2d', 'h2d', 'h2c'].includes(model))
492
+ invalid();
493
+ let args;
494
+ try {
495
+ args = parameters(argumentsText);
496
+ }
497
+ catch {
498
+ return invalid();
499
+ }
500
+ const [width, depth, height] = MACHINE_LIMITS[model].volume;
501
+ const position = ['X', 'Y', 'U', 'V'].filter(key => args.has(key)).join('');
502
+ const limit = { X: width, U: width, Y: depth, V: depth };
503
+ if ([...args.keys()].some(key => !'MEXYUVZSCOT'.includes(key)) ||
504
+ // These models have two physical extruders, whatever the file declares.
505
+ ![0, 1].includes(args.get('M')) || !Number.isInteger(args.get('E')) || !inRange(args, 'E', 0, Math.min(nozzleDiameters.length, 2) - 1) ||
506
+ args.get('S') !== 11 || args.get('C') !== 10 || args.get('O') !== 0 || !inRange(args, 'T', 0, 600000) ||
507
+ !inRange(args, 'Z', 0, height) ||
508
+ !['', 'XY', 'UV'].includes(position) || [...position].some(key => !inRange(args, key, 0, limit[key])))
509
+ invalid();
510
+ continue;
511
+ }
391
512
  if (!['M117', 'M118', 'M1002', 'M1006', 'M900', 'M970', 'M983.1', 'G383'].includes(code) && /[GM]\s*\d/i.test(argumentsText))
392
513
  fail('multiple commands on one line are unsupported');
393
514
  // Coordinated extrusion closes the startup-only allowance even when the
@@ -411,7 +532,15 @@ export async function inspectPrintFile(filePath, options) {
411
532
  continue;
412
533
  }
413
534
  if (['M104', 'M109', 'M140', 'M190', 'M141', 'M191'].includes(code)) {
414
- const args = parameters(argumentsText, 'A');
535
+ const args = parameters(argumentsText, code === 'M104' ? 'AM' : 'A');
536
+ // Studio 2.9's GCodeWriter marks layer-temperature changes with bare M.
537
+ // Accept only its exact S-target / optional T form; targets and tool
538
+ // selectors still pass through the independent thermal checks below.
539
+ if (args.has('M')) {
540
+ if (!Number.isNaN(args.get('M')) || !args.has('S') || [...args.keys()].some(key => !'MST'.includes(key)))
541
+ fail('unsupported M104 layer-change temperature parameters');
542
+ args.delete('M');
543
+ }
415
544
  // Official A1 and A1 mini start G-code waits with M109 S<target> H<limit>
416
545
  // (S25 H140, S100 H170, S220 H300). H is not a heater target: S stays the
417
546
  // checked target, so H is accepted only within those vendor bounds.
@@ -421,10 +550,7 @@ export async function inspectPrintFile(filePath, options) {
421
550
  // editable maxima or an unchecked offset as thermal evidence.
422
551
  if (code !== 'M104' || !h2cStartup(index) || args.size !== 2 || args.get('O') !== -80 || !Number.isNaN(args.get('A')))
423
552
  fail('unsupported H2C startup offset parameters');
424
- const targets = consistent(metadataValues(metadata, ['nozzle_temperature_initial_layer']), value => {
425
- const values = list(value);
426
- return values.length === materials.length && values.every(v => NUMBER.test(v) && Number.isFinite(Number(v))) ? values.map(Number) : undefined;
427
- }, 'first-layer nozzle temperature');
553
+ const targets = firstLayerTemperatures(project, headers, materials.length);
428
554
  for (let position = 0; position < targets.length; position++) {
429
555
  validateTemperature('nozzle', targets[position], model, [materials[position]]);
430
556
  const preheat = Math.max(0, targets[position] - 80);
@@ -441,9 +567,16 @@ export async function inspectPrintFile(filePath, options) {
441
567
  continue;
442
568
  }
443
569
  const vendorWait = code === 'M109' && ['a1', 'a1mini'].includes(model);
570
+ // Bambu Studio addresses a physical nozzle on dual-nozzle machines with N<index>
571
+ // (X2D `M104 T1 S25 N0` multi-extruder pre-cooling and wipe-tower reheats).
572
+ // N selects a nozzle, never a target, so it is accepted only on those models,
573
+ // only for nozzle heaters, and only within the declared nozzle count.
574
+ const nozzleIndex = ['M104', 'M109'].includes(code) && dualNozzle;
444
575
  for (const key of args.keys())
445
- if (!'SRTA'.includes(key) && !(key === 'H' && vendorWait))
576
+ if (!'SRTA'.includes(key) && !(key === 'H' && vendorWait) && !(key === 'N' && nozzleIndex))
446
577
  fail(`unsupported ${code} temperature parameter ${key}`);
578
+ if (args.has('N') && (!Number.isInteger(args.get('N')) || args.get('N') < 0 || args.get('N') >= Math.min(nozzleDiameters.length, 2)))
579
+ fail(`unsupported ${code} nozzle index N`);
447
580
  if (args.has('H') && (!Number.isFinite(args.get('H')) || args.get('H') < 0 || args.get('H') > 300))
448
581
  fail(`unsupported ${code} wait parameter H`);
449
582
  if (!args.has('S') && !args.has('R'))
@@ -453,13 +586,13 @@ export async function inspectPrintFile(filePath, options) {
453
586
  // Without a verified per-filament nozzle map, apply all possible materials.
454
587
  if (args.has('T') && (!Number.isInteger(args.get('T')) || args.get('T') < 0 || args.get('T') >= nozzleDiameters.length))
455
588
  fail('physical heater target has no declared nozzle');
456
- const position = args.has('T') ? undefined : active;
589
+ const position = args.has('T') || args.has('N') ? undefined : active;
457
590
  const commonFlush = code === 'M109' && args.size === 1 && args.get('S') === 290 && model === 'x1e' && !depositionStarted && !commonFlushUsed && boundedX1ePurge(lines, index);
458
591
  if (commonFlush)
459
592
  commonFlushUsed = true;
460
593
  for (const key of ['S', 'R'])
461
594
  if (args.has(key))
462
- heat(component, args.get(key), position, args.has('T') || args.has('A'), commonFlush);
595
+ heat(component, args.get(key), position, args.has('T') || args.has('N') || args.has('A'), commonFlush);
463
596
  }
464
597
  else if (code === 'M620' || code === 'M621') {
465
598
  const args = parameters(argumentsText, model === 'x2d' ? 'MAB' : model === 'h2c' ? 'MAN' : 'MA');
@@ -519,9 +652,10 @@ export async function inspectPrintFile(filePath, options) {
519
652
  }
520
653
  else if (code === 'M620.14') {
521
654
  const args = parameters(argumentsText);
522
- // H2C tower coordinates include the rear wipe area outside object bounds.
523
- if (!h2cStartup(index) || args.size !== 2 || !['X', 'Y'].every(key => args.has(key) && Number.isFinite(args.get(key)) && args.get(key) >= 0 && args.get(key) <= 350))
524
- fail('unsupported H2C startup tower parameters');
655
+ // H2C uses these coordinates during startup and a selected filament change.
656
+ // The rear wipe area extends outside object bounds (official fallback Y336).
657
+ if (!(h2cStartup(index) || (model === 'h2c' && pending !== undefined)) || args.size !== 2 || !['X', 'Y'].every(key => args.has(key) && Number.isFinite(args.get(key)) && args.get(key) >= 0 && args.get(key) <= 350))
658
+ fail('unsupported H2C tower parameters');
525
659
  }
526
660
  else if (model === 'h2c' && ['G383', 'G383.3', 'G383.7'].includes(code) && /U/i.test(argumentsText)) {
527
661
  const args = parameters(argumentsText);
@@ -563,9 +697,12 @@ export async function inspectPrintFile(filePath, options) {
563
697
  // H2D change_filament_gcode supplies the incoming filament's cooling
564
698
  // temperature as C{new_filament_temp - filament_cooling_before_tower}.
565
699
  const args = parameters(argumentsText);
566
- if (args.size !== 1 || !args.has('C'))
700
+ // H2C also supplies P{filament_pre_cooling_temperature_nc[next_filament_id]}
701
+ // before switching hotends. P and C both constrain the incoming material.
702
+ const target = model === 'h2c' && args.has('P') ? 'P' : 'C';
703
+ if (args.size !== 1 || !args.has(target))
567
704
  fail('unsupported M620.15 cooling temperature parameters');
568
- heat('nozzle', args.get('C'), pending ?? active);
705
+ heat('nozzle', args.get(target), pending ?? active);
569
706
  }
570
707
  else if (code === 'M620.17') {
571
708
  const args = parameters(argumentsText);
@@ -620,5 +757,5 @@ export async function inspectPrintFile(filePath, options) {
620
757
  return fail('selected G-code contains no printable commands');
621
758
  if (!used.size)
622
759
  materials.forEach((_, index) => used.add(index));
623
- return { model, nozzleDiameters, materials, usedFilamentPositions: [...used].sort((a, b) => a - b), plateInternalPath, sha256, maxNozzleTemperature, maxBedTemperature, maxChamberTemperature, bedType, selectsAms, nozzleTypes, nozzleFlows };
760
+ return { model, nozzleDiameters, materials, usedFilamentPositions: [...used].sort((a, b) => a - b), plateInternalPath, sha256, maxNozzleTemperature, maxBedTemperature, maxChamberTemperature, bedType, selectsAms, nozzleTypes, nozzleFlows, filamentNozzleIndices, ...(Array.isArray(project.filament_type) ? { projectFilaments: projectFilaments(project) } : {}) };
624
761
  }
@@ -1,12 +1,5 @@
1
1
  import type { BambuClient } from "bambu-node";
2
- /**
3
- * Publish one MQTT command with a string sequence id and no ACK wait.
4
- * Unpatched bambu-node 3.22.21 command classes send numeric sequence ids;
5
- * bambu-printer-mcp patches them to strings for H2-series firmware. This
6
- * server does not install that patch, so safety-relevant commands are
7
- * published here with the patched wire shape: {category:{sequence_id,command,...}}.
8
- */
9
- export declare function publishBambuCommand(printer: Pick<BambuClient, "publish">, category: "print" | "pushing" | "info", command: string, extra?: Record<string, unknown>): Promise<void>;
2
+ export declare const SERIAL_MODELS: Record<string, string>;
10
3
  /** Only raw reports received after this request are evidence. Never read printer.data here. */
11
4
  export declare function readFreshPrinterStatus(printer: BambuClient, serial: string, timeoutMs?: number): Promise<any>;
12
5
  export interface PrinterStateRequirements {
@@ -14,6 +7,8 @@ export interface PrinterStateRequirements {
14
7
  nozzleDiameters: number[];
15
8
  materials?: string[];
16
9
  usedFilamentPositions?: number[];
10
+ /** Verified physical nozzle index for each project filament, composed from the inspected plate mappings. */
11
+ filamentNozzleIndices?: number[];
17
12
  amsMapping?: number[];
18
13
  useAMS?: boolean;
19
14
  requireIdle?: boolean;
@@ -44,10 +39,66 @@ export interface ValidatedPrinterState {
44
39
  reportedNozzles: ReportedNozzle[];
45
40
  filaments: ReportedFilament[];
46
41
  }
42
+ export declare function finiteNumber(value: unknown): number | undefined;
43
+ /**
44
+ * X2D retains FAILED after a canceled job even with no active print error.
45
+ * Bambu Studio classifies FAILED as a finished job; its v02.08.02.61
46
+ * hms_en_20P table (926a7192574bcb9b3a732e1ec59a46d79cb45466)
47
+ * calls 0300400C "The task was canceled."
48
+ * Only this observed terminal condition qualifies, never an arbitrary failure.
49
+ * Callers still validate HMS, identity, hardware and material configuration,
50
+ * and print dispatch rechecks the identity of the observed job.
51
+ */
52
+ export declare function isCancelledX2DJob(raw: Record<string, any>, model: string): boolean;
53
+ /** Bambu Studio 2.8.2.61 marks FAILED as finished. Limit P1S recovery to
54
+ * a named prior job explicitly reporting idle, no print error and no HMS.
55
+ * Dispatch rechecks the observed job identity. */
56
+ export declare function isFinishedP1SJob(raw: Record<string, any>, model: string): boolean;
47
57
  /** Nozzles whose reported diameter and type are configured settings rather than chip readings. */
48
58
  export declare function unchippedNozzles(raw: unknown): number[];
59
+ export type HmsEntry = {
60
+ attr: number;
61
+ code: number;
62
+ severity: number;
63
+ hex: string;
64
+ };
65
+ export declare function hmsCode(entry: any): HmsEntry | undefined;
66
+ /** The one key format for a reported entry, shared by the gate and clear_hms_errors. */
67
+ export declare function reportedHmsKey(entry: unknown): string;
68
+ /** Bambu's display form of a cleared-code key such as "hms:83887616:131184"; other keys unchanged. */
69
+ export declare function describeHmsKey(key: string): string;
70
+ /**
71
+ * A serious or common entry that Bambu lists for this serial prefix with empty
72
+ * text: Bambu Studio, Bambu Handy and the printer screen never show it, and
73
+ * the printer keeps printing with it (an X2D raises 0500-0600-0002-0070 at
74
+ * every boot). Bambu Studio selects the table by serial prefix, so this does
75
+ * too, but only when that prefix belongs to the verified model: a sparse report
76
+ * without a serial must not let a misconfigured serial pick another model's
77
+ * table. Fatal, invalid-level and unlisted entries never qualify.
78
+ */
79
+ export declare function isHiddenNotice(serial: string, model: string, entry: HmsEntry): boolean;
80
+ /** Labels of reported entries that pass only because Bambu hides them for this printer. */
81
+ export declare function hiddenHmsNotices(raw: unknown, serial: string, model: string): string[];
82
+ /** Cleared-code keys of reported hidden entries; clearing never removes them, so they are not re-acknowledged. */
83
+ export declare function hiddenHmsKeys(raw: unknown, serial: string, model: string): string[];
49
84
  /** Verifies reported configuration, not a physical nozzle/spool inspection. */
50
85
  export declare function validatePrinterState(status: any, requirements: PrinterStateRequirements): ValidatedPrinterState;
51
86
  /** Manual M104 cannot remap filament slots. Validate the material already at the nozzle. */
52
87
  export declare function manualHeatingRequirements(status: any, model: string, nozzleDiameter: number, material: string): PrinterStateRequirements;
88
+ export type ExternalStorageState = "none" | "normal" | "abnormal" | "readonly";
89
+ /**
90
+ * The SD card or USB stick state, read as Bambu Studio does: the `sdcard`
91
+ * flag, then `home_flag` bits 8-9, then on firmware that reports cfg/fun/aux/stat
92
+ * the `aux` bits 12-13, each later source overriding the earlier one
93
+ * (DevStorage.cpp, DeviceManager.cpp parse_home_flag and parse_new_info).
94
+ */
95
+ export declare function externalStorageState(raw: unknown): ExternalStorageState;
96
+ /**
97
+ * Publish one MQTT command with a string sequence id and no ACK wait.
98
+ * Unpatched bambu-node 3.22.21 command classes send numeric sequence ids;
99
+ * bambu-printer-mcp patches them to strings for H2-series firmware. This
100
+ * server does not install that patch, so safety-relevant commands are
101
+ * published here with the patched wire shape: {category:{sequence_id,command,...}}.
102
+ */
103
+ export declare function publishBambuCommand(printer: Pick<BambuClient, "publish">, category: "print" | "pushing" | "info", command: string, extra?: Record<string, unknown>): Promise<void>;
53
104
  export {};