bambu-printer-mcp 1.1.25 → 1.1.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CONTRIBUTORS.md CHANGED
@@ -7,7 +7,8 @@ Thank you to everyone who builds, tests, reports problems, and shares real print
7
7
  | Contributor | Contribution |
8
8
  | --- | --- |
9
9
  | [Aleksei Popovich (alexpapay)](https://github.com/alexpapay) | Fixes cross-platform AMS profile resolution and packed P2S chamber status in [#43](https://github.com/DMontgomery40/bambu-printer-mcp/pull/43), contributes regressions, and supplies read-only Windows P2S/AMS 2 Pro status and local slicing evidence. No physical print is claimed for this contribution. |
10
- | [travismcashan](https://github.com/travismcashan) | Reports stock X2D material-switch and runout-purge rejection, misleading unchecked confirmation, and stdout diagnostics in [#44](https://github.com/DMontgomery40/bambu-printer-mcp/issues/44). Supplies exact preset syntax and firmware evidence, then reports a clean physical X2D cube print with a local workaround; maintainer validation remains isolated and mocked. |
10
+ | [Adam (@adamkaplan)](https://github.com/adamkaplan) | Identifies the reversed H2C/H2D Pro serial prefixes in [#47](https://github.com/DMontgomery40/bambu-printer-mcp/issues/47), verifies corrected H2C status locally, and isolates the official H2C startup commands and config-block marker failure in [#48](https://github.com/DMontgomery40/bambu-printer-mcp/issues/48). Provides exact Bambu Studio version/template hash and unsafe-file reproductions; maintainer validation is offline and mocked. |
11
+ | [travismcashan](https://github.com/travismcashan) | Reports stock X2D material-switch and runout-purge rejection, misleading unchecked confirmation, and stdout diagnostics in [#44](https://github.com/DMontgomery40/bambu-printer-mcp/issues/44). Supplies exact preset syntax and firmware evidence, then reports a clean physical X2D cube print with a local workaround; maintainer validation remains isolated and mocked. Reports the accepted native job and subsequent helper teardown crash in [#46](https://github.com/DMontgomery40/bambu-printer-mcp/issues/46), with completed-print and macOS crash evidence informing result preservation and duplicate-print warnings. |
11
12
  | [nitpreet22](https://github.com/nitpreet22) | Fixes rejection of the stock A1/A1 mini `M109 H` startup waits in [#41](https://github.com/DMontgomery40/bambu-printer-mcp/pull/41), contributes temperature and model regressions, and supplies Windows Bambu Studio profile and real sliced A1 job inspection evidence. Their physical print used Bambu Studio, not this MCP change. |
12
13
  | [Steavie (steavie)](https://github.com/steavie) | Reports the reversed A1/A1 mini serial prefixes in [#39](https://github.com/DMontgomery40/bambu-printer-mcp/issues/39), with A1 status and configuration evidence that identifies the status and fresh-identity regression. |
13
14
  | [JaviOFC](https://github.com/JaviOFC) | Reports the X2D identity rejection in [#36](https://github.com/DMontgomery40/bambu-printer-mcp/issues/36) and supplies current firmware, connection-mode, status-field, serial-prefix, and cloud-job evidence showing that `model_id` can contain a print-job identifier while `model` correctly identifies the printer. |
package/README.md CHANGED
@@ -878,6 +878,24 @@ To stop drying:
878
878
  }
879
879
  ```
880
880
 
881
+ #### inspect_print_file
882
+
883
+ Inspect a prepared sliced 3MF or textual G-code file offline before deciding what to export or print. This tool copies the file into a private read-only snapshot and reuses the same archive, selected-plate, model/nozzle/material and temperature checks used by print preflight. It never contacts a printer or auto-slices an unsliced project.
884
+
885
+ ```json
886
+ {
887
+ "file_path": "/path/to/bracket.gcode.3mf",
888
+ "bambu_model": "p1s",
889
+ "plate_index": 0,
890
+ "nozzle_diameters": [0.4],
891
+ "bed_type": "textured_plate"
892
+ }
893
+ ```
894
+
895
+ `file_path` and explicit `bambu_model` are required; the model is never inferred from configuration. `plate_index` is zero-based and defaults to 0. Nozzle/bed checks are optional comparisons against file declarations, not observations of installed hardware. Only these five argument names are accepted; credentials, host and executable/bridge overrides are not part of this tool.
896
+
897
+ The generic result has `receipt_version: "1"`, `evidence: "file-only"`, `dispatch_authorized: false`, `artifact` (`sha256`, `byte_length`, `file_format`), the requested `selection`, and the original `inspection` including declared model, nozzles, materials/used filament positions, internal plate path and checked temperature peaks. Hash and byte length identify the exact inspected snapshot; a consumer must compare them with its own artifact and selected plate. The temporary snapshot is removed, and its path is not a dispatch capability. The receipt does not prove printer readiness, physical material/nozzle contents, fit, full motion simulation or permission to print. Actual print routes still independently inspect the dispatched snapshot and require fresh hardware state and human preflight.
898
+
881
899
  #### print_3mf
882
900
 
883
901
  The primary tool for starting a Bambu print. **Recommended input: a pre-sliced `.gcode.3mf` exported from FULU OrcaSlicer-bambulab, OrcaSlicer, or Bambu Studio** — see [slicing guide](https://github.com/DMontgomery40/bambu-printer-mcp/blob/main/docs/SLICING.md). This tool handles the complete workflow:
@@ -932,7 +950,7 @@ The npm package and desktop bundle include `native/bambu-native-print.cpp` and `
932
950
 
933
951
  For X2D, `print_3mf` defaults to `connection_mode: "bambu_native"`; legacy `lan_mqtt_ftps` requests are redirected to it. Supply `ams_slots`, a complete project-level `ams_mapping`, or `auto_match_ams: true`. An external-spool job requires explicit `use_ams: false`. Raw `ams_mapping2` and nozzle/extra-option overrides are rejected before dispatch; the server derives both AMS representations from the checked structured mapping. Model/nozzle/material inspection, fresh printer-state checks, and human preflight apply before the helper receives a private snapshot. The native helper requests fresh shared printer-state authorization immediately before each print submission, including a certificate retry, and before checked heating, resume, or error-clearing commands; custom helpers must support this handshake. Stop and heater-off remain available without preflight. Native upload-only requests use the same all-plate inspection as FTPS uploads. They accept `project_name`, `preset_name`, `bed_type`, and an existing `plate_index`; AMS, nozzle, and calibration print options are rejected. Use `print_3mf` for checked print settings.
934
952
 
935
- The helper waits for connection/certificate exchange and retries only the initial `-4030` send once. Previous hardware testing reached `RUNNING` on X2D; this revision is validated with mocked transport regressions and clean installs, not a new physical print. `bambu_connect` is an optional macOS handoff for user review in Bambu Connect and does not start a print. Native task, heater, and error controls retain the common safety checks; raw controls cannot bypass them. Request cancellation, stop, and heater-off interrupt pending native helpers; the server waits for process exit before releasing a checked file. A command already sent to the printer cannot be recalled by cancelling the request, so verify printer state before retrying.
953
+ The helper waits for connection/certificate exchange and retries only the initial `-4030` send once. Rebuild the helper after upgrading to receive the teardown fix. A successful dispatch receipt ends the helper before plug-in cleanup can turn an accepted job into an error. If an older/custom helper crashes after dispatch, the server reports uncertainty: check the printer state and job name before retrying, because resending can duplicate a print. Previous hardware testing reached `RUNNING` on X2D; this revision is validated with mocked transport regressions and clean installs, not a new physical print. `bambu_connect` is an optional macOS handoff for user review in Bambu Connect and does not start a print. Native task, heater, and error controls retain the common safety checks; raw controls cannot bypass them. Request cancellation, stop, and heater-off interrupt pending native helpers; the server waits for process exit before releasing a checked file. A command already sent to the printer cannot be recalled by cancelling the request, so verify printer state before retrying.
936
954
 
937
955
  #### x2d_native_control
938
956
 
@@ -180,7 +180,7 @@ function runNativeHelper(mode, env, timeoutMs, onUpdate, execution = {}) {
180
180
  reject(failure);
181
181
  return;
182
182
  }
183
- if (needsAuthorization && code === 0 && (authorizing || authorizedAttempts === 0)) {
183
+ if (needsAuthorization && (code === 0 || updates.includes("native_print result=0") || stdout.trim() === "native_print result=0") && (authorizing || authorizedAttempts === 0)) {
184
184
  reject(new Error("Native helper exited without completed dispatch authorization."));
185
185
  return;
186
186
  }
@@ -189,7 +189,7 @@ function runNativeHelper(mode, env, timeoutMs, onUpdate, execution = {}) {
189
189
  updates.push(trailing);
190
190
  onUpdate?.(trailing);
191
191
  }
192
- resolve({ resultCode: code ?? (signal ? 1 : 0), updates, stderr });
192
+ resolve({ resultCode: code ?? (signal ? 1 : 0), signal, authorizedAttempts, updates, stderr });
193
193
  });
194
194
  });
195
195
  }
@@ -551,6 +551,10 @@ export async function printWithBambuNative(options, onUpdate, execution) {
551
551
  BAMBU_NATIVE_TIMELAPSE: boolEnv(options.timelapse, false),
552
552
  }, 300000, onUpdate, execution);
553
553
  if (result.resultCode !== 0) {
554
+ const explicitRejection = result.updates.some(line => /^native_print result=-?[1-9]\d*$/.test(line));
555
+ if (result.authorizedAttempts > 0 && (result.updates.includes("native_print result=0") || (result.signal && !explicitRejection))) {
556
+ throw new Error("Bambu native print dispatch is uncertain: the printer may have accepted this job before the helper exited. Check printer status and the job name before retrying; do not automatically resend it.");
557
+ }
554
558
  const detail = [
555
559
  ...result.updates,
556
560
  ...(result.stderr ? [result.stderr.trim()] : []),
package/dist/index.js CHANGED
@@ -2377,6 +2377,23 @@ class BambuPrinterMCPServer {
2377
2377
  required: ["action", "ams_id"]
2378
2378
  }
2379
2379
  },
2380
+ {
2381
+ name: "inspect_print_file",
2382
+ description: "Inspect an exact private snapshot of a prepared sliced 3MF or textual G-code file and selected plate without contacting a printer. Returns versioned file-only evidence; no printer readiness or permission to dispatch is established. Does not auto-slice.",
2383
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
2384
+ inputSchema: {
2385
+ type: "object",
2386
+ additionalProperties: false,
2387
+ properties: {
2388
+ file_path: { type: "string", minLength: 1, description: "Local prepared .3mf or textual .gcode artifact to inspect; checksum-only and unsliced projects reject." },
2389
+ bambu_model: { type: "string", enum: [...VALID_BAMBU_MODELS], description: "Explicit file-inspection target model; never inferred from environment or live state." },
2390
+ plate_index: { type: "integer", minimum: 0, description: "Zero-based selected plate; defaults to 0. Raw G-code only supports 0." },
2391
+ nozzle_diameters: { type: "array", minItems: 1, maxItems: 2, items: { type: "number", enum: [0.2, 0.4, 0.6, 0.8] }, description: "Optional requested nozzle diameters to compare with file declarations; this does not observe installed hardware." },
2392
+ bed_type: { type: "string", enum: [...VALID_BED_TYPES], description: "Optional declared bed type to compare with file metadata." },
2393
+ },
2394
+ required: ["file_path", "bambu_model"],
2395
+ },
2396
+ },
2380
2397
  {
2381
2398
  name: "print_3mf",
2382
2399
  description: "Print a 3MF file on a Bambu Lab printer. Auto-slices if the 3MF has no gcode. IMPORTANT: bambu_model must be specified to ensure safe printer operation.",
@@ -2613,6 +2630,42 @@ class BambuPrinterMCPServer {
2613
2630
  try {
2614
2631
  let result;
2615
2632
  switch (name) {
2633
+ case "inspect_print_file": {
2634
+ const allowed = new Set(["file_path", "bambu_model", "plate_index", "nozzle_diameters", "bed_type"]);
2635
+ if (!args || Object.keys(args).some(key => !allowed.has(key)))
2636
+ throw new Error("inspect_print_file accepts only file/model/plate/nozzle/bed arguments.");
2637
+ if (typeof args.file_path !== "string" || !args.file_path.trim() || args.file_path.includes("\0"))
2638
+ throw new Error("file_path must name a local prepared artifact.");
2639
+ if (typeof args.bambu_model !== "string" || !args.bambu_model.trim())
2640
+ throw new Error("An explicit bambu_model is required for file inspection.");
2641
+ const model = validateBambuModel(args.bambu_model);
2642
+ const plateIndex = args.plate_index === undefined ? 0 : args.plate_index;
2643
+ if (typeof plateIndex !== "number" || !Number.isSafeInteger(plateIndex) || plateIndex < 0)
2644
+ throw new Error("plate_index must be a nonnegative safe integer.");
2645
+ if (args.nozzle_diameters !== undefined && (!Array.isArray(args.nozzle_diameters) || args.nozzle_diameters.some(value => typeof value !== "number")))
2646
+ throw new Error("nozzle_diameters must be numeric sizes.");
2647
+ const nozzles = requestedPrintNozzles(args);
2648
+ if (args.bed_type !== undefined && (typeof args.bed_type !== "string" || !args.bed_type.trim()))
2649
+ throw new Error("bed_type must be a supported plate type.");
2650
+ const bedType = args.bed_type === undefined ? undefined : resolveBedType(args.bed_type);
2651
+ if (extra.signal.aborted)
2652
+ throw new Error("File inspection cancelled.");
2653
+ result = await withPrintSnapshot(args.file_path, async (snapshot) => {
2654
+ const inspection = await inspectPrintFile(snapshot, { model, nozzleDiameters: nozzles, plateIndex, bedType });
2655
+ if (extra.signal.aborted)
2656
+ throw new Error("File inspection cancelled.");
2657
+ const byteLength = (await fs.promises.stat(snapshot)).size;
2658
+ return {
2659
+ receipt_version: "1",
2660
+ evidence: "file-only",
2661
+ dispatch_authorized: false,
2662
+ artifact: { sha256: inspection.sha256, byte_length: byteLength, file_format: path.extname(snapshot).toLowerCase() === ".3mf" ? "3mf" : "gcode" },
2663
+ selection: { bambu_model: model, plate_index: plateIndex, ...(nozzles === undefined ? {} : { nozzle_diameters: nozzles }), ...(bedType === undefined ? {} : { bed_type: bedType }) },
2664
+ inspection,
2665
+ };
2666
+ });
2667
+ break;
2668
+ }
2616
2669
  case "get_printer_status":
2617
2670
  result = await this.bambu.getStatus(host, bambuSerial, bambuToken);
2618
2671
  break;
@@ -118,9 +118,9 @@ class TolerantBambuClient extends BambuClient {
118
118
  return "H2S";
119
119
  if (sn.startsWith("094"))
120
120
  return "H2D";
121
- if (sn.startsWith("239"))
122
- return "H2C";
123
121
  if (sn.startsWith("31B"))
122
+ return "H2C";
123
+ if (sn.startsWith("239"))
124
124
  return "H2DPRO";
125
125
  if (sn.startsWith("20P"))
126
126
  return "X2D";
@@ -80,7 +80,7 @@ function parameters(text, flags = '') {
80
80
  return result;
81
81
  }
82
82
  // Layer comments only close a startup exception; they can never enable one.
83
- const isLayerMarker = (line) => /^\s*;\s*(?:LAYER_CHANGE|CHANGE_LAYER|LAYER\s*:|layer num\/total_layer_count\s*:)/i.test(line);
83
+ const isLayerMarker = (line) => /^\s*;\s*(?:(?:LAYER_CHANGE|CHANGE_LAYER)(?:\s|$)|LAYER\s*:|layer num\/total_layer_count\s*:)/i.test(line);
84
84
  function boundedX1ePurge(lines, index) {
85
85
  // Official X1E common flush: 50 mm at 200 mm/min, followed immediately by
86
86
  // lowering the target. No dwell, XY extrusion, repeated block or open-ended
@@ -331,6 +331,10 @@ export async function inspectPrintFile(filePath, options) {
331
331
  let previous;
332
332
  let maxNozzleTemperature = 0, maxBedTemperature = 0, maxChamberTemperature = 0, commandCount = 0;
333
333
  let depositionStarted = false, commonFlushUsed = false;
334
+ // Bound the H2C vendor forms to its one explicit executable startup block.
335
+ const startupStarts = lines.flatMap((line, index) => /^\s*;\s*EXECUTABLE_BLOCK_START\s*$/i.test(line) ? [index] : []);
336
+ const startupEnds = lines.flatMap((line, index) => /^\s*;\s*MACHINE_START_GCODE_END\s*$/i.test(line) ? [index] : []);
337
+ const h2cStartup = (index) => model === 'h2c' && !depositionStarted && startupStarts.length === 1 && startupEnds.length === 1 && index > startupStarts[0] && index < startupEnds[0];
334
338
  const heat = (component, value, position, allMaterials = false, startupPurge = false) => {
335
339
  const affected = allMaterials || position === undefined ? materials.map((_, index) => index) : [requirePosition(position)];
336
340
  if (component === 'nozzle' && startupPurge)
@@ -356,19 +360,26 @@ export async function inspectPrintFile(filePath, options) {
356
360
  };
357
361
  for (let index = 0; index < lines.length; index++) {
358
362
  try {
359
- if (isLayerMarker(lines[index]))
363
+ if (isLayerMarker(lines[index]) || /^\s*;\s*MACHINE_START_GCODE_END\s*$/i.test(lines[index]))
360
364
  closeStartupWindow();
361
365
  let line = stripComments(lines[index]);
362
366
  if (!line || line === '%')
363
367
  continue;
368
+ // The official H2C template emits this literal separator without a semicolon.
369
+ if (line === '========== record data ==========' && h2cStartup(index))
370
+ continue;
364
371
  line = line.replace(/^N\d+\s*/i, '').replace(/\*\d+\s*$/, '');
365
372
  if (/[{}\[\]#]/.test(line))
366
373
  fail('unresolved dynamic command syntax');
367
374
  if (/^SYNC(?=$|\s)/i.test(line)) {
368
- // H2D change_filament_gcode emits SYNC T{ceil(flush_length / 125) * 5}.
369
- // T is a synchronization duration here, not a nozzle temperature. The
370
- // vendor template establishes no independent maximum duration.
375
+ // Vendor SYNC T is a duration, not a heater target. X2D and P2S
376
+ // startup templates additionally emit R0; no other R form is supported.
371
377
  const args = parameters(line.slice(4));
378
+ if (args.has('R')) {
379
+ if (!['x2d', 'p2s'].includes(model) || args.get('R') !== 0)
380
+ fail('unsupported SYNC reset parameters');
381
+ args.delete('R');
382
+ }
372
383
  if (args.size !== 1 || !args.has('T') || !Number.isFinite(args.get('T')) || args.get('T') < 0)
373
384
  fail('unsupported SYNC duration parameters');
374
385
  commandCount++;
@@ -408,6 +419,31 @@ export async function inspectPrintFile(filePath, options) {
408
419
  // Official A1 and A1 mini start G-code waits with M109 S<target> H<limit>
409
420
  // (S25 H140, S100 H170, S220 H300). H is not a heater target: S stays the
410
421
  // checked target, so H is accepted only within those vendor bounds.
422
+ if (args.has('O')) {
423
+ // H2C's exact all-hotend preheat offset is relative to the declared
424
+ // first-layer targets. Check every base and derived target, never use
425
+ // editable maxima or an unchecked offset as thermal evidence.
426
+ if (code !== 'M104' || !h2cStartup(index) || args.size !== 2 || args.get('O') !== -80 || !Number.isNaN(args.get('A')))
427
+ fail('unsupported H2C startup offset parameters');
428
+ const targets = consistent(metadataValues(metadata, ['nozzle_temperature_initial_layer']), value => {
429
+ const values = list(value);
430
+ return values.length === materials.length && values.every(v => NUMBER.test(v) && Number.isFinite(Number(v))) ? values.map(Number) : undefined;
431
+ }, 'first-layer nozzle temperature');
432
+ for (let position = 0; position < targets.length; position++) {
433
+ validateTemperature('nozzle', targets[position], model, [materials[position]]);
434
+ const preheat = Math.max(0, targets[position] - 80);
435
+ validateTemperature('nozzle', preheat, model, [materials[position]]);
436
+ nozzleTarget = Math.max(nozzleTarget, preheat);
437
+ maxNozzleTemperature = Math.max(maxNozzleTemperature, preheat);
438
+ }
439
+ continue;
440
+ }
441
+ if (args.has('D')) {
442
+ if (!['M140', 'M190'].includes(code) || !h2cStartup(index) || args.size !== 1)
443
+ fail('unsupported H2C startup bed parameters');
444
+ heat('bed', args.get('D'), undefined, true);
445
+ continue;
446
+ }
411
447
  const vendorWait = code === 'M109' && ['a1', 'a1mini'].includes(model);
412
448
  for (const key of args.keys())
413
449
  if (!'SRTA'.includes(key) && !(key === 'H' && vendorWait))
@@ -430,7 +466,9 @@ export async function inspectPrintFile(filePath, options) {
430
466
  heat(component, args.get(key), position, args.has('T') || args.has('A'), commonFlush);
431
467
  }
432
468
  else if (code === 'M620' || code === 'M621') {
433
- const args = parameters(argumentsText, model === 'x2d' ? 'MAB' : 'MA');
469
+ const args = parameters(argumentsText, model === 'x2d' ? 'MAB' : model === 'h2c' ? 'MAN' : 'MA');
470
+ if (args.has('N') && (code !== 'M620' || !h2cStartup(index) || args.size !== 1 || !Number.isNaN(args.get('N'))))
471
+ fail('unsupported H2C startup hotend-remap flag');
434
472
  // X2D templates use B as a bare material-switch flag, never a target.
435
473
  if (args.has('B') && (model !== 'x2d' || !Number.isNaN(args.get('B'))))
436
474
  fail('unsupported material-switch B flag');
@@ -483,10 +521,27 @@ export async function inspectPrintFile(filePath, options) {
483
521
  else if (/^G150\./.test(code) && /T/i.test(argumentsText)) {
484
522
  fail(`unsupported thermal-affecting wipe command ${code}`);
485
523
  }
524
+ else if (code === 'M620.14') {
525
+ const args = parameters(argumentsText);
526
+ // H2C tower coordinates include the rear wipe area outside object bounds.
527
+ 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))
528
+ fail('unsupported H2C startup tower parameters');
529
+ }
530
+ else if (model === 'h2c' && ['G383', 'G383.3', 'G383.7'].includes(code) && /U/i.test(argumentsText)) {
531
+ const args = parameters(argumentsText);
532
+ const expected = code === 'G383.7' ? 'UJ' : code === 'G383.3' ? 'UL' : 'OUL';
533
+ if (!h2cStartup(index) || args.size !== expected.length || [...args.keys()].some(key => !expected.includes(key)) || args.get('U') !== 140 ||
534
+ (code === 'G383.7' && args.get('J') !== 0) || (code === 'G383' && args.get('O') !== 2))
535
+ fail('unsupported H2C startup probing parameters');
536
+ const position = args.has('L') ? requirePosition(args.get('L')) : active;
537
+ heat('nozzle', args.get('U'), position);
538
+ }
486
539
  else if (code === 'G383' || code === 'G383.3') {
487
540
  // Bambu probing routines carry nozzle temperature in T and optionally
488
541
  // the project filament position in L (including the H2D G383.3 form).
489
542
  const args = parameters(argumentsText);
543
+ if (args.has('U'))
544
+ fail('unsupported G383 temperature parameter U outside H2C startup');
490
545
  if (code === 'G383.3' && (!args.has('T') || [...args.keys()].some(key => !'TL'.includes(key))))
491
546
  fail('unsupported G383.3 temperature parameters');
492
547
  const position = args.has('L') ? requirePosition(args.get('L')) : active;
@@ -6,7 +6,7 @@ const MODEL_IDS = {
6
6
  N2S: "A1", A1M: "A1 Mini", C11: "P1P", C12: "P1S", C13: "X1E", "BL-P001": "X1C", "BL-P002": "X1",
7
7
  };
8
8
  const SERIAL_MODELS = {
9
- "093": "H2S", "094": "H2D", "239": "H2C", "31B": "H2D Pro", "20P": "X2D",
9
+ "093": "H2S", "094": "H2D", "31B": "H2C", "239": "H2D Pro", "20P": "X2D",
10
10
  "00M": "X1C", "00W": "X1", "03W": "X1E", "01S": "P1P", "01P": "P1S", "22E": "P2S", "039": "A1", "030": "A1 Mini",
11
11
  };
12
12
  function object(value) {
@@ -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
+ }
@@ -27,6 +27,7 @@ import fs from "node:fs/promises";
27
27
  import { existsSync, realpathSync } from "node:fs";
28
28
  import path from "node:path";
29
29
  import crypto from "node:crypto";
30
+ import { profileJsonFiles } from './profile-files.js';
30
31
  /* -------------------------------------------------------------------------- */
31
32
  /* Indexing */
32
33
  /* -------------------------------------------------------------------------- */
@@ -48,19 +49,7 @@ async function buildNameIndex(profilesRoot, vendor, userProfileRoots = []) {
48
49
  ...userProfileRoots.flatMap(root => ['process', 'filament'].map(sub => path.join(root, sub))),
49
50
  ];
50
51
  for (const dir of directories) {
51
- let entries;
52
- try {
53
- entries = await fs.readdir(dir);
54
- }
55
- catch {
56
- // Vendor or subdir missing -- skip silently; flatten will fail later
57
- // with a precise "name not found" error.
58
- continue;
59
- }
60
- for (const entry of entries) {
61
- if (!entry.endsWith(".json"))
62
- continue;
63
- const filePath = path.join(dir, entry);
52
+ for (const filePath of profileJsonFiles(dir)) {
64
53
  let raw;
65
54
  try {
66
55
  raw = await fs.readFile(filePath, "utf8");
@@ -10,6 +10,7 @@ import { EventEmitter } from 'events';
10
10
  import * as crypto from 'crypto';
11
11
  import { execFile } from 'child_process';
12
12
  import { flattenForCli, detectProfilesRoot, resolveBblMachineProfile } from '../slicer/profile-flatten.js';
13
+ import { profileJsonFiles } from '../slicer/profile-files.js';
13
14
  const readFileAsync = promisify(fs.readFile);
14
15
  const writeFileAsync = promisify(fs.writeFile);
15
16
  const BAMBU_PROFILE_ROOTS = [
@@ -134,10 +135,10 @@ export class STLManipulator extends EventEmitter {
134
135
  return undefined;
135
136
  }
136
137
  for (const root of roots) {
137
- const candidate = path.join(root, category, `${profileName}.json`);
138
- if (fs.existsSync(candidate)) {
138
+ const candidate = profileJsonFiles(path.join(root, category))
139
+ .find(file => path.basename(file) === `${profileName}.json`);
140
+ if (candidate)
139
141
  return candidate;
140
- }
141
142
  }
142
143
  return undefined;
143
144
  }
@@ -148,11 +149,7 @@ export class STLManipulator extends EventEmitter {
148
149
  if (!fs.existsSync(filamentDir)) {
149
150
  continue;
150
151
  }
151
- for (const entry of fs.readdirSync(filamentDir)) {
152
- if (!entry.endsWith('.json')) {
153
- continue;
154
- }
155
- const filePath = path.join(filamentDir, entry);
152
+ for (const filePath of profileJsonFiles(filamentDir)) {
156
153
  try {
157
154
  const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8'));
158
155
  const filamentId = typeof parsed?.filament_id === 'string' ? parsed.filament_id.trim() : '';
@@ -725,6 +725,11 @@ void runOperation(NativeApi &api, NativeOperation operation) {
725
725
  }
726
726
  }
727
727
  outputLine(std::string(uploadOnly ? "native_upload" : "native_print") + " result=" + std::to_string(result));
728
+ // A one-shot print helper has finished dispatch. Plug-in worker teardown can
729
+ // crash after acceptance; let the OS reclaim it without changing that result.
730
+ // outputLine flushes the receipt before exiting. Upload/control paths retain
731
+ // their existing cleanup, and no dispatched print is replayed here.
732
+ if (!uploadOnly) std::_Exit(result == 0 ? 0 : 20);
728
733
  destroyAgent();
729
734
  if (result != 0) std::exit(20);
730
735
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bambu-printer-mcp",
3
- "version": "1.1.25",
3
+ "version": "1.1.27",
4
4
  "description": "MCP server for Bambu Lab 3D printers with STL manipulation, slicing, and print management",
5
5
  "main": "dist/index.js",
6
6
  "repository": {
@@ -29,4 +29,4 @@ index 916e924..6eb81e9 100644
29
29
  {if activate_air_filtration[current_extruder] && support_air_filtration}
30
30
  M106 P3 S{during_print_exhaust_fan_speed_num[current_extruder]}
31
31
  -{endif}`],filament_id:"GFL01",filament_flow_ratio:["0.98"],filament_vendor:["Polymaker"]}};var W;(function(e){e[e.PRINTING=0]="PRINTING",e[e.BED_LEVELING=1]="BED_LEVELING",e[e.HEATBED_PREHEATING=2]="HEATBED_PREHEATING",e[e.XY_MECH_MODE_SWEEP=3]="XY_MECH_MODE_SWEEP",e[e.CHANGE_MATERIAL=4]="CHANGE_MATERIAL",e[e.M400_PAUSE=5]="M400_PAUSE",e[e.FILAMENT_RUNOUT_PAUSE=6]="FILAMENT_RUNOUT_PAUSE",e[e.HOTEND_HEATING=7]="HOTEND_HEATING",e[e.EXTRUDE_COMPENSATION_SCAN=8]="EXTRUDE_COMPENSATION_SCAN",e[e.BED_SCAN=9]="BED_SCAN",e[e.FIRST_LAYER_SCAN=10]="FIRST_LAYER_SCAN",e[e.BE_SURFACE_TYPT_IDENTIFICATION=11]="BE_SURFACE_TYPT_IDENTIFICATION",e[e.SCANNER_EXTRINSIC_PARA_CALI=12]="SCANNER_EXTRINSIC_PARA_CALI",e[e.TOOLHEAD_HOMING=13]="TOOLHEAD_HOMING",e[e.NOZZLE_TIP_CLEANING=14]="NOZZLE_TIP_CLEANING",e[e.EXTRUDER_TEMP_PROTECT_CALI=15]="EXTRUDER_TEMP_PROTECT_CALI",e[e.USER_PAUSE=16]="USER_PAUSE",e[e.TOOLHEAD_SHELL_OFF_PAUSE=17]="TOOLHEAD_SHELL_OFF_PAUSE",e[e.SCANNER_LASER_PARA_CALI=18]="SCANNER_LASER_PARA_CALI",e[e.EXTRUDER_ABSOLUTE_FLOW_CALI=19]="EXTRUDER_ABSOLUTE_FLOW_CALI",e[e.HOTEND_TEMPERATURE_ERROR_PAUSE=20]="HOTEND_TEMPERATURE_ERROR_PAUSE",e[e.HEATED_BED_TEMPERATURE_ERROR_PAUSE=21]="HEATED_BED_TEMPERATURE_ERROR_PAUSE",e[e.FILAMENT_UNLOADING=22]="FILAMENT_UNLOADING",e[e.SKIP_STEP_PAUSE=23]="SKIP_STEP_PAUSE",e[e.FILAMENT_LOADING=24]="FILAMENT_LOADING",e[e.MOTOR_NOISE_CALIBRATION=25]="MOTOR_NOISE_CALIBRATION",e[e.AMS_LOST_PAUSE=26]="AMS_LOST_PAUSE",e[e.HEAT_BREAK_FAN_PAUSE=27]="HEAT_BREAK_FAN_PAUSE",e[e.CHAMBER_TEMPERATURE_CONTROL_ERROR_PAUSE=28]="CHAMBER_TEMPERATURE_CONTROL_ERROR_PAUSE",e[e.CHAMBER_COOLING=29]="CHAMBER_COOLING",e[e.USER_INSERT_GCODE_PAUSE=30]="USER_INSERT_GCODE_PAUSE",e[e.MOTOR_NOISE_SHOWOFF=31]="MOTOR_NOISE_SHOWOFF",e[e.NOZZLE_FILAMENT_COVERED_DETECTED_PAUSE=32]="NOZZLE_FILAMENT_COVERED_DETECTED_PAUSE",e[e.CUTTER_ERROR_PAUSE=33]="CUTTER_ERROR_PAUSE",e[e.FIRST_LAYER_ERROR_PAUSE=34]="FIRST_LAYER_ERROR_PAUSE",e[e.NOZZLE_CLOG_PAUSE=35]="NOZZLE_CLOG_PAUSE"})(W||(W={}));var X;(function(e){e.SILENT="1",e.NORMAL="2",e.SPORT="3",e.LUDICROUS="4"})(X||(X={}));var V;(function(e){e[e.IDLE=0]="IDLE",e[e.READING=1]="READING",e[e.GCODE_TRANS=2]="GCODE_TRANS",e[e.GCODE_RUNNING=3]="GCODE_RUNNING",e[e.ASSISTANT=4]="ASSISTANT",e[e.SWITCH_FILAMENT=5]="SWITCH_FILAMENT",e[e.HAS_FILAMENT=6]="HAS_FILAMENT"})(V||(V={}));var $;(function(e){e.DOWNLOADING="DOWNLOADING",e.UPGRADE_REQUEST="UPGRADE_REQUEST",e.PRE_FLASH_START="PRE_FLASH_START",e.PRE_FLASH_SUCCESS="PRE_FLASH_SUCCESS"})($||($={}));var Z;(function(e){e.UPGRADE_SUCCESS="UPGRADE_SUCCESS",e.DOWNLOAD_FAIL="DOWNLOAD_FAIL",e.FLASH_FAIL="FLASH_FAIL",e.PRE_FLASH_FAIL="PRE_FLASH_FAIL",e.UPGRADE_FAIL="UPGRADE_FAIL"})(Z||(Z={}));var Y;(function(e){e[e.UpgradeNoError=0]="UpgradeNoError",e[e.UpgradeDownloadFailed=1]="UpgradeDownloadFailed",e[e.UpgradeVerifyFailed=2]="UpgradeVerifyFailed",e[e.UpgradeFlashFailed=3]="UpgradeFlashFailed",e[e.UpgradePrinting=4]="UpgradePrinting"})(Y||(Y={}));var K;(function(e){e[e.UNAVAILABLE=0]="UNAVAILABLE",e[e.IDLE=1]="IDLE",e[e.UPGRADING=2]="UPGRADING",e[e.UPGRADE_FINISHED=3]="UPGRADE_FINISHED"})(K||(K={}));var Q;(function(e){e.PLA="PLA",e.ABS="ABS",e.TPU="TPU",e.PC="PC",e.ASA="ASA",e.PA_CF="PA-CF",e.PA6_CF="PA6-CF",e.PET_CF="PET-CF",e.PETG="PETG",e.PETG_CF="PETG-CF",e.PLA_AERO="PLA-AERO",e.PLA_CF="PLA-CF",e.PPA_CF="PPA-CF",e.PPA_GF="PPA-GF",e.PA="PA",e.HIPS="HIPS",e.PPS="PPS",e.PPS_CF="PPS-CF",e.PVA="PVA",e.PLA_S="PLA-S",e.PA_S="PLA-S"})(Q||(Q={}));function Je(e){return k[e]??null}r(Je,"getFilamentConfig");function h(e){return e.command==="push_status"&&Object.keys(e).length>=40}r(h,"isPushAllCommand");function ee(e){return e.command==="gcode_line"&&!!e?.param?.startsWith("M106")}r(ee,"isUpdateFanCommand");function te(e){return e.command==="push_status"&&e.hasOwnProperty("lights_report")}r(te,"isUpdateLightCommand");function _e(e){return e.command==="push_status"&&(e.hasOwnProperty("spd_lvl")||e.hasOwnProperty("spd_mag"))}r(_e,"isUpdateSpeedCommand");function re(e){return e.command===e.command}r(re,"isUpdateStateCommand");function ae(e){return e.command==="gcode_line"&&!!(e?.param?.startsWith("M104")||e?.param?.startsWith("M140"))}r(ae,"isUpdateTempCommand");function g(e){return!!e?.mc_print&&!!e?.mc_print?.command&&["push_info"].includes(e.mc_print.command)}r(g,"isMCPrintMessage");function ht(e){return e.command==="push_info"}r(ht,"isPushInfoCommand");function gt(e){let[,_,t,l]=e.param.match(/^\[(.+?)](?:\[(.+?)])?\s*(.+)$/)??[];return{sequenceId:Number(e.sequence_id),category:_,subcategory:t,content:l,rawParam:e.param}}r(gt,"getCleanPushInfoCommand");function x(e){return e.command==="get_version"}r(x,"isGetVersionCommand");function z(e){return!!e?.info&&!!e?.info?.command&&["get_version"].includes(e.info.command)}r(z,"isInfoMessage");var E=class E extends f{constructor({fileName:t}){super({param:t});n(this,"category","print");n(this,"command","gcode_file");n(this,"ownsResponse",q)}};r(E,"GCodeFileCommand");var ie=E;var S=class S extends d{constructor({gcodes:t}){super(t);n(this,"command","gcode_line");n(this,"ownsResponse",j)}};r(S,"GCodeLineCommand");var ne=S;var G=class G extends f{constructor(){super();n(this,"category","info");n(this,"command","get_version");n(this,"ownsResponse",x)}};r(G,"GetVersionCommand");var b=G;var M=class M extends f{constructor(){super();n(this,"category","pushing");n(this,"command","pushall");n(this,"ownsResponse",h)}};r(M,"PushAllCommand");var c=M;var le;(function(e){e[e.PART_COOLING_FAN=1]="PART_COOLING_FAN",e[e.AUXILIARY_FAN=2]="AUXILIARY_FAN",e[e.CHAMBER_FAN=3]="CHAMBER_FAN"})(le||(le={}));var L=class L extends d{constructor({fan:t,speed:l}){super([`M106 P${t.toString()} S${255*l/100}`]);n(this,"command","gcode_line");n(this,"ownsResponse",ee)}};r(L,"UpdateFanCommand");var me=L;var F=class F extends f{constructor({light:t,mode:l,loopOptions:a={led_on_time:0,led_off_time:0,loop_times:0,interval_time:0}}){super({led_node:t,led_mode:l,...a});n(this,"category","system");n(this,"command","ledctrl");n(this,"ownsResponse",te)}};r(F,"UpdateLightCommand");var oe=F;var N=class N extends f{constructor({speed:t}){super({param:t.toString()});n(this,"category","print");n(this,"command","print_speed");n(this,"ownsResponse",_e)}};r(N,"UpdateSpeedCommand");var pe=N;var C=class C extends f{constructor({state:t}){super();n(this,"category","print");n(this,"command");n(this,"ownsResponse",re);this.command=t}};r(C,"UpdateStateCommand");var se=C;var fe;(function(e){e.BED="M140",e.EXTRUDER="M104"})(fe||(fe={}));var T=class T extends d{constructor({part:t,temperature:l}){super([`${t} S${l}`]);n(this,"command","gcode_line");n(this,"ownsResponse",ae)}};r(T,"UpdateTempCommand");var ue=T;var p;(function(e){e.X1C="X1C",e.X1="X1",e.X1E="X1E",e.P1P="P1P",e.P1S="P1S",e.A1="A1",e.A1M="A1M"})(p||(p={}));import{createId as xe}from"@paralleldrive/cuid2";var ze=new RegExp(/\.[^/.]+$/),R=class R{constructor(_,t,l,a=new Date){n(this,"_jobData");let o=[p.P1P,p.P1S].includes(l);this._jobData={id:xe(),printerId:t,printerModel:l,name:_.subtask_name.replace(ze,""),fileName:_.subtask_name,gcodeName:_.gcode_file,jobOrigin:_.print_type,totalLayerNumber:_.total_layer_num,totalPrintTime:_.mc_remaining_time,status:_.gcode_state,stage:_.stg_cur,percentDone:_.mc_percent,layerNumber:_.layer_num,remainingTime:_.mc_remaining_time,skippedObjects:_.s_obj,statusHistory:[{status:_.gcode_state,changedAt:a}],speedLevelHistory:[{speedLevel:_.spd_lvl,speedMagnitude:_.spd_mag,changedAt:a}],printingStageHistory:[{stage:_.stg_cur,changedAt:a}],temperatureHistory:[{bedTemp:_.bed_temper,bedTargetTemp:_.bed_target_temper,interiorTemp:o?_.frame_temper:_.chamber_temper,nozzleTemp:_.nozzle_temper,nozzleTargetTemp:_.nozzle_target_temper,changedAt:a}],tempLastUpdatedAt:new Date,errorCodes:_.hms.map(m=>({attr:m.attr,code:m.code,thrownAt:a,layerNumber:_.layer_num})),createdAt:a}}update(_){if(!this._jobData)return;let t=this._jobData,l=new Date,a=[p.P1P,p.P1S].includes(this._jobData.printerModel);_.gcode_state&&t.statusHistory.at(-1)?.status!==_.gcode_state&&t.statusHistory.push({status:_.gcode_state,changedAt:l}),_.spd_lvl&&_.spd_mag&&t.speedLevelHistory.at(-1)?.speedLevel!==_.spd_lvl&&t.speedLevelHistory.push({speedLevel:_.spd_lvl,speedMagnitude:_.spd_mag,changedAt:l}),_.stg_cur&&t.printingStageHistory.at(-1)?.stage!==_.stg_cur&&t.printingStageHistory.push({stage:_.stg_cur,changedAt:l});let o=!!(_.bed_temper||_.bed_target_temper||(a?_.frame_temper:_.chamber_temper)||_.nozzle_temper||_.nozzle_target_temper),m=t.tempLastUpdatedAt.getTime()+5e3<l.getTime();if(o&&m){let s=a?_.frame_temper:_.chamber_temper,u=t.temperatureHistory.at(-1);t.temperatureHistory.push({bedTemp:_.bed_temper??u.bedTemp,bedTargetTemp:_.bed_target_temper??u.bedTargetTemp,interiorTemp:s??u.interiorTemp,nozzleTemp:_.nozzle_temper??u.nozzleTemp,nozzleTargetTemp:_.nozzle_target_temper??u.nozzleTargetTemp,changedAt:l}),t.tempLastUpdatedAt=l}let i={status:_.gcode_state??t.status,stage:_.stg_cur??t.stage,percentDone:_.mc_percent??t.percentDone,layerNumber:_.layer_num??t.layerNumber,remainingTime:_.mc_remaining_time??t.remainingTime,skippedObjects:_.s_obj??t.skippedObjects,errorCodes:_.hms?.map((s,u)=>({attr:s.attr??t.errorCodes[u].attr,code:s.code??t.errorCodes[u].code,thrownAt:l??t.errorCodes[u].thrownAt,layerNumber:_.layer_num??t.errorCodes[u].layerNumber}))??t.errorCodes};this._jobData={...this._jobData,...i}}get data(){return this._jobData}};r(R,"Job");var w=R;import*as ce from"mqtt";import*as ye from"eventemitter3";import{createId as be}from"@paralleldrive/cuid2";function I(e){return new Promise(_=>setTimeout(_,e))}r(I,"sleep");function O(e,_){let t={},l=r((a,o,m)=>{for(let i in o){let s=o[i],u=a[i];if(!(i in a&&JSON.stringify(a[i])===JSON.stringify(o[i]))){if(typeof s=="object"&&s!==null&&u!==null&&!Array.isArray(s)){m[i]=m[i]||{},l(a[i],o[i],m[i]);continue}m[i]=o[i]}}},"checkDifferences");return l(e,_,t),t}r(O,"subtractObject");var D=class D extends ye.EventEmitter{constructor(t){super();n(this,"clientOptions");n(this,"id");n(this,"mqttClient");n(this,"config");n(this,"_printerData");n(this,"_printerStatus");n(this,"jobHistory");n(this,"currentJob");this.clientOptions=t,this.id=be(),this.mqttClient=void 0,this._printerData={modules:[],model:void 0},this._printerStatus="OFFLINE",this.jobHistory=[],this.currentJob=null,this.config={host:t.host,port:t.port??8883,accessToken:t.accessToken,serialNumber:t.serialNumber,throwOnOfflineCommands:t.throwOnOfflineCommands??!1,reconnectInterval:t.reconnectInterval??1e3,connectTimeout:t.connectTimeout??2e3,keepAlive:t.keepAlive??20},this.jobHistory=t.jobHistory??[],this.currentJob=t.currentJob??null}emit(t,...l){return super.emit(t,...l)}off(t,l){return super.off(t,l),this}once(t,l){return super.once(t,l),this}on(t,l){return super.on(t,l),this}async connectMQTT(){return new Promise((t,l)=>{if(this.mqttClient)throw new Error("Can't establish a new connection while running another one!");this.mqttClient=ce.connect(`mqtts://${this.config.host}:${this.config.port}`,{username:"bblp",password:this.config.accessToken,reconnectPeriod:this.clientOptions.reconnectInterval,connectTimeout:this.clientOptions.connectTimeout,keepalive:this.clientOptions.keepAlive,resubscribe:!0,rejectUnauthorized:!1}),this.mqttClient.on("connect",async(...a)=>{await this.onConnect(...a),this.emit("client:connect"),t()}),this.mqttClient.on("disconnect",()=>{this.emit("client:disconnect",!1),this.emit("printer:statusUpdate",this._printerStatus,"OFFLINE"),this._printerStatus="OFFLINE",this.currentJob&&this.emit("job:pause",this.currentJob,!0)}),this.mqttClient.on("offline",()=>{this.emit("client:disconnect",!0),this.emit("printer:statusUpdate",this._printerStatus,"OFFLINE"),this._printerStatus="OFFLINE",this.currentJob&&this.emit("job:pause",this.currentJob,!0)}),this.mqttClient.on("message",(a,o)=>this.emit("rawMessage",a,o)),this.mqttClient.on("error",a=>{this.emit("client:error",a),l(a)})})}async connect(){return await Promise.all([this.connectMQTT()])}async disconnect(t=!1,l){return new Promise(a=>this.mqttClient?.end(t,l,a))}subscribe(t){if(this._printerStatus!=="OFFLINE"&&this.config.throwOnOfflineCommands)throw new Error(`Unable to subscribe to topic "${t}" while disconnected from printer!`);return new Promise((l,a)=>{if(!this.mqttClient)return a("Client not connected.");this.mqttClient.subscribe(t,m=>{if(m)return a(`Error subscribing to topic '${t}': ${m.message}`)});let o=r(async(m,i)=>{m===t&&await this.onMessage(i.toString(),t)},"listener");this.mqttClient.on("message",o),l()})}async executeCommand(t){if(this._printerStatus!=="OFFLINE"&&this.config.throwOnOfflineCommands)throw new Error("Unable to send commands while disconnected from printer!");return new Promise((l,a)=>{t.invoke(this).catch(m=>a(m));let o=setTimeout(()=>{a(new Error("Command execution timed out after 5 seconds."))},5e3);this.on("rawMessage",(m,i)=>{let s=JSON.parse(i.toString()),u=Object.keys(s)[0];if(!(z(s)||g(s)||y(s)))return;let P=s[u];P?.command&&t.ownsResponse(P)&&(clearTimeout(o),l(P))})})}async onConnect(t){await this.subscribe(`device/${this.config.serialNumber}/report`),await I(1e3),await this.executeCommand(new b),(this._printerData.model===p.P1P||this._printerData.model===p.P1S||this._printerData.model===p.A1||this._printerData.model===p.A1M)&&(await this.executeCommand(new c),setInterval(async()=>{await this.executeCommand(new c)},5*60*1e3))}async onMessage(t,l){let a=JSON.parse(t),o=Object.keys(a)[0];if(z(a)||g(a)||y(a)){if(y(a)&&(this.emit("message",l,o,a.print),(h(a.print)||J(a.print))&&(this._printerData={...this._printerData,...a.print},this.emit("printer:dataUpdate",this._printerData,a.print),a.print.gcode_state&&a.print.gcode_state!==this._printerStatus))){let m=this._printerStatus,i=a.print.gcode_state;if(this.emit("printer:statusUpdate",m,i),(i==="PREPARE"||i==="SLICING")&&(i="IDLE"),(m==="PREPARE"||m==="SLICING")&&(m="RUNNING"),(m==="IDLE"||m==="FINISH"||m==="FAILED")&&i==="RUNNING")this.currentJob!==null&&this.jobHistory.push(this.currentJob),this.currentJob=new w(this._printerData,this.id,this._printerData.model),this.emit("job:start",this.currentJob);else if(m==="RUNNING"&&(i==="FINISH"||i==="FAILED"||i==="IDLE"))i==="FINISH"?this.currentJob!==null&&this.emit("job:finish",this.currentJob,"SUCCESS"):i==="FAILED"?this.currentJob!==null&&this.emit("job:finish",this.currentJob,"FAILED"):i==="IDLE"&&this.currentJob!==null&&this.emit("job:finish",this.currentJob,"UNEXPECTED"),this.currentJob!==null&&this.jobHistory.push(this.currentJob),this.currentJob=null;else if(m==="RUNNING"&&i==="PAUSE")this.currentJob&&this.emit("job:pause",this.currentJob,!1);else if(m==="PAUSE"&&i==="RUNNING")this.currentJob&&this.emit("job:unpause",this.currentJob);else if(m==="OFFLINE"&&i==="PAUSE")this.emit("job:offlineRecovery",this.currentJob);else if(!(m==="OFFLINE"&&i==="RUNNING")){if(!((m==="IDLE"||m==="OFFLINE")&&(i==="FINISH"||i==="FAILED"||i==="IDLE"))){if(!((m==="FINISH"||i==="FAILED")&&i==="IDLE")){if(m!==i)throw new Error("Edge case detected while updating printer status!")}}}if(this._printerStatus=a.print.gcode_state,this.currentJob){let s=this.currentJob?.data;this.currentJob.update(a.print),this.emit("job:update",this.currentJob,O(s,this.currentJob.data))}}if(g(a)&&this.emit("message",l,o,a.print),z(a)&&(this.emit("message",l,o,a.info),x(a.info))){let m=[...new Set([...this._printerData.modules,...a.info.module])];this._printerData.modules=m,this.emit("printer:dataUpdate",this._printerData,{modules:m});let i=this._printerData.modules.find(s=>s.name==="ota");if(!i)throw new Error("OTA module version data not found, unable to determine printer model!");if(i.sn.startsWith("00M"))this._printerData.model=p.X1C;else if(i.sn.startsWith("00W"))this._printerData.model=p.X1;else if(i.sn.startsWith("03W"))this._printerData.model=p.X1E;else if(i.sn.startsWith("01S"))this._printerData.model=p.P1P;else if(i.sn.startsWith("01P"))this._printerData.model=p.P1S;else if(i.sn.startsWith("030"))this._printerData.model=p.A1;else if(i.sn.startsWith("039"))this._printerData.model=p.A1M;else throw new Error("Printer model not supported!");this.emit("printer:dataUpdate",this._printerData,{model:this._printerData.model})}}}publish(t){return new Promise((l,a)=>{if(!this.mqttClient)return a("Client not connected.");let o=typeof t=="string"?t:JSON.stringify(t),m=`device/${this.config.serialNumber}/request`;this.mqttClient.publish(m,o,i=>{if(i)return a(`Error publishing to topic '${m}': ${i.message}`);l()})})}get data(){return this._printerData}get status(){return this._printerStatus}};r(D,"BambuClient");var de=D;export{f as AbstractCommand,V as AmsRFIDStatus,de as BambuClient,le as Fan,Q as FilamentType,d as GCodeCommand,ie as GCodeFileCommand,ne as GCodeLineCommand,b as GetVersionCommand,w as Job,W as PrintStage,p as PrinterModel,c as PushAllCommand,X as SpeedLevel,me as UpdateFanCommand,oe as UpdateLightCommand,pe as UpdateSpeedCommand,se as UpdateStateCommand,ue as UpdateTempCommand,K as UpgradeDisplayState,Y as UpgradeErrorCode,Z as UpgradeStatusFinishState,$ as UpgradeStatusProgressState,gt as getCleanPushInfoCommand,Je as getFilamentConfig,j as isGCodeLineCommand,q as isGcodeFileCommand,x as isGetVersionCommand,z as isInfoMessage,g as isMCPrintMessage,y as isPrintMessage,Oe as isProjectFileCommand,h as isPushAllCommand,ht as isPushInfoCommand,J as isPushStatusCommand,ee as isUpdateFanCommand,te as isUpdateLightCommand,_e as isUpdateSpeedCommand,re as isUpdateStateCommand,ae as isUpdateTempCommand};
32
- +{endif}`],filament_id:"GFL01",filament_flow_ratio:["0.98"],filament_vendor:["Polymaker"]}};var W;(function(e){e[e.PRINTING=0]="PRINTING",e[e.BED_LEVELING=1]="BED_LEVELING",e[e.HEATBED_PREHEATING=2]="HEATBED_PREHEATING",e[e.XY_MECH_MODE_SWEEP=3]="XY_MECH_MODE_SWEEP",e[e.CHANGE_MATERIAL=4]="CHANGE_MATERIAL",e[e.M400_PAUSE=5]="M400_PAUSE",e[e.FILAMENT_RUNOUT_PAUSE=6]="FILAMENT_RUNOUT_PAUSE",e[e.HOTEND_HEATING=7]="HOTEND_HEATING",e[e.EXTRUDE_COMPENSATION_SCAN=8]="EXTRUDE_COMPENSATION_SCAN",e[e.BED_SCAN=9]="BED_SCAN",e[e.FIRST_LAYER_SCAN=10]="FIRST_LAYER_SCAN",e[e.BE_SURFACE_TYPT_IDENTIFICATION=11]="BE_SURFACE_TYPT_IDENTIFICATION",e[e.SCANNER_EXTRINSIC_PARA_CALI=12]="SCANNER_EXTRINSIC_PARA_CALI",e[e.TOOLHEAD_HOMING=13]="TOOLHEAD_HOMING",e[e.NOZZLE_TIP_CLEANING=14]="NOZZLE_TIP_CLEANING",e[e.EXTRUDER_TEMP_PROTECT_CALI=15]="EXTRUDER_TEMP_PROTECT_CALI",e[e.USER_PAUSE=16]="USER_PAUSE",e[e.TOOLHEAD_SHELL_OFF_PAUSE=17]="TOOLHEAD_SHELL_OFF_PAUSE",e[e.SCANNER_LASER_PARA_CALI=18]="SCANNER_LASER_PARA_CALI",e[e.EXTRUDER_ABSOLUTE_FLOW_CALI=19]="EXTRUDER_ABSOLUTE_FLOW_CALI",e[e.HOTEND_TEMPERATURE_ERROR_PAUSE=20]="HOTEND_TEMPERATURE_ERROR_PAUSE",e[e.HEATED_BED_TEMPERATURE_ERROR_PAUSE=21]="HEATED_BED_TEMPERATURE_ERROR_PAUSE",e[e.FILAMENT_UNLOADING=22]="FILAMENT_UNLOADING",e[e.SKIP_STEP_PAUSE=23]="SKIP_STEP_PAUSE",e[e.FILAMENT_LOADING=24]="FILAMENT_LOADING",e[e.MOTOR_NOISE_CALIBRATION=25]="MOTOR_NOISE_CALIBRATION",e[e.AMS_LOST_PAUSE=26]="AMS_LOST_PAUSE",e[e.HEAT_BREAK_FAN_PAUSE=27]="HEAT_BREAK_FAN_PAUSE",e[e.CHAMBER_TEMPERATURE_CONTROL_ERROR_PAUSE=28]="CHAMBER_TEMPERATURE_CONTROL_ERROR_PAUSE",e[e.CHAMBER_COOLING=29]="CHAMBER_COOLING",e[e.USER_INSERT_GCODE_PAUSE=30]="USER_INSERT_GCODE_PAUSE",e[e.MOTOR_NOISE_SHOWOFF=31]="MOTOR_NOISE_SHOWOFF",e[e.NOZZLE_FILAMENT_COVERED_DETECTED_PAUSE=32]="NOZZLE_FILAMENT_COVERED_DETECTED_PAUSE",e[e.CUTTER_ERROR_PAUSE=33]="CUTTER_ERROR_PAUSE",e[e.FIRST_LAYER_ERROR_PAUSE=34]="FIRST_LAYER_ERROR_PAUSE",e[e.NOZZLE_CLOG_PAUSE=35]="NOZZLE_CLOG_PAUSE"})(W||(W={}));var X;(function(e){e.SILENT="1",e.NORMAL="2",e.SPORT="3",e.LUDICROUS="4"})(X||(X={}));var V;(function(e){e[e.IDLE=0]="IDLE",e[e.READING=1]="READING",e[e.GCODE_TRANS=2]="GCODE_TRANS",e[e.GCODE_RUNNING=3]="GCODE_RUNNING",e[e.ASSISTANT=4]="ASSISTANT",e[e.SWITCH_FILAMENT=5]="SWITCH_FILAMENT",e[e.HAS_FILAMENT=6]="HAS_FILAMENT"})(V||(V={}));var $;(function(e){e.DOWNLOADING="DOWNLOADING",e.UPGRADE_REQUEST="UPGRADE_REQUEST",e.PRE_FLASH_START="PRE_FLASH_START",e.PRE_FLASH_SUCCESS="PRE_FLASH_SUCCESS"})($||($={}));var Z;(function(e){e.UPGRADE_SUCCESS="UPGRADE_SUCCESS",e.DOWNLOAD_FAIL="DOWNLOAD_FAIL",e.FLASH_FAIL="FLASH_FAIL",e.PRE_FLASH_FAIL="PRE_FLASH_FAIL",e.UPGRADE_FAIL="UPGRADE_FAIL"})(Z||(Z={}));var Y;(function(e){e[e.UpgradeNoError=0]="UpgradeNoError",e[e.UpgradeDownloadFailed=1]="UpgradeDownloadFailed",e[e.UpgradeVerifyFailed=2]="UpgradeVerifyFailed",e[e.UpgradeFlashFailed=3]="UpgradeFlashFailed",e[e.UpgradePrinting=4]="UpgradePrinting"})(Y||(Y={}));var K;(function(e){e[e.UNAVAILABLE=0]="UNAVAILABLE",e[e.IDLE=1]="IDLE",e[e.UPGRADING=2]="UPGRADING",e[e.UPGRADE_FINISHED=3]="UPGRADE_FINISHED"})(K||(K={}));var Q;(function(e){e.PLA="PLA",e.ABS="ABS",e.TPU="TPU",e.PC="PC",e.ASA="ASA",e.PA_CF="PA-CF",e.PA6_CF="PA6-CF",e.PET_CF="PET-CF",e.PETG="PETG",e.PETG_CF="PETG-CF",e.PLA_AERO="PLA-AERO",e.PLA_CF="PLA-CF",e.PPA_CF="PPA-CF",e.PPA_GF="PPA-GF",e.PA="PA",e.HIPS="HIPS",e.PPS="PPS",e.PPS_CF="PPS-CF",e.PVA="PVA",e.PLA_S="PLA-S",e.PA_S="PLA-S"})(Q||(Q={}));function Je(e){return k[e]??null}r(Je,"getFilamentConfig");function h(e){return e.command==="push_status"&&Object.keys(e).length>=40}r(h,"isPushAllCommand");function ee(e){return e.command==="gcode_line"&&!!e?.param?.startsWith("M106")}r(ee,"isUpdateFanCommand");function te(e){return e.command==="push_status"&&e.hasOwnProperty("lights_report")}r(te,"isUpdateLightCommand");function _e(e){return e.command==="push_status"&&(e.hasOwnProperty("spd_lvl")||e.hasOwnProperty("spd_mag"))}r(_e,"isUpdateSpeedCommand");function re(e){return e.command===e.command}r(re,"isUpdateStateCommand");function ae(e){return e.command==="gcode_line"&&!!(e?.param?.startsWith("M104")||e?.param?.startsWith("M140"))}r(ae,"isUpdateTempCommand");function g(e){return!!e?.mc_print&&!!e?.mc_print?.command&&["push_info"].includes(e.mc_print.command)}r(g,"isMCPrintMessage");function ht(e){return e.command==="push_info"}r(ht,"isPushInfoCommand");function gt(e){let[,_,t,l]=e.param.match(/^\[(.+?)](?:\[(.+?)])?\s*(.+)$/)??[];return{sequenceId:Number(e.sequence_id),category:_,subcategory:t,content:l,rawParam:e.param}}r(gt,"getCleanPushInfoCommand");function x(e){return e.command==="get_version"}r(x,"isGetVersionCommand");function z(e){return!!e?.info&&!!e?.info?.command&&["get_version"].includes(e.info.command)}r(z,"isInfoMessage");var E=class E extends f{constructor({fileName:t}){super({param:t});n(this,"category","print");n(this,"command","gcode_file");n(this,"ownsResponse",q)}};r(E,"GCodeFileCommand");var ie=E;var S=class S extends d{constructor({gcodes:t}){super(t);n(this,"command","gcode_line");n(this,"ownsResponse",j)}};r(S,"GCodeLineCommand");var ne=S;var G=class G extends f{constructor(){super();n(this,"category","info");n(this,"command","get_version");n(this,"ownsResponse",x)}};r(G,"GetVersionCommand");var b=G;var M=class M extends f{constructor(){super();n(this,"category","pushing");n(this,"command","pushall");n(this,"ownsResponse",h)}};r(M,"PushAllCommand");var c=M;var le;(function(e){e[e.PART_COOLING_FAN=1]="PART_COOLING_FAN",e[e.AUXILIARY_FAN=2]="AUXILIARY_FAN",e[e.CHAMBER_FAN=3]="CHAMBER_FAN"})(le||(le={}));var L=class L extends d{constructor({fan:t,speed:l}){super([`M106 P${t.toString()} S${255*l/100}`]);n(this,"command","gcode_line");n(this,"ownsResponse",ee)}};r(L,"UpdateFanCommand");var me=L;var F=class F extends f{constructor({light:t,mode:l,loopOptions:a={led_on_time:0,led_off_time:0,loop_times:0,interval_time:0}}){super({led_node:t,led_mode:l,...a});n(this,"category","system");n(this,"command","ledctrl");n(this,"ownsResponse",te)}};r(F,"UpdateLightCommand");var oe=F;var N=class N extends f{constructor({speed:t}){super({param:t.toString()});n(this,"category","print");n(this,"command","print_speed");n(this,"ownsResponse",_e)}};r(N,"UpdateSpeedCommand");var pe=N;var C=class C extends f{constructor({state:t}){super();n(this,"category","print");n(this,"command");n(this,"ownsResponse",re);this.command=t}};r(C,"UpdateStateCommand");var se=C;var fe;(function(e){e.BED="M140",e.EXTRUDER="M104"})(fe||(fe={}));var T=class T extends d{constructor({part:t,temperature:l}){super([`${t} S${l}`]);n(this,"command","gcode_line");n(this,"ownsResponse",ae)}};r(T,"UpdateTempCommand");var ue=T;var p;(function(e){e.X1C="X1C",e.X1="X1",e.X1E="X1E",e.P1P="P1P",e.P1S="P1S",e.P2S="P2S",e.H2S="H2S",e.H2D="H2D",e.H2C="H2C",e.H2DPRO="H2DPRO",e.A1="A1",e.A1M="A1M"})(p||(p={}));import{createId as xe}from"@paralleldrive/cuid2";var ze=new RegExp(/\.[^/.]+$/),R=class R{constructor(_,t,l,a=new Date){n(this,"_jobData");let o=[p.P1P,p.P1S].includes(l);this._jobData={id:xe(),printerId:t,printerModel:l,name:_.subtask_name.replace(ze,""),fileName:_.subtask_name,gcodeName:_.gcode_file,jobOrigin:_.print_type,totalLayerNumber:_.total_layer_num,totalPrintTime:_.mc_remaining_time,status:_.gcode_state,stage:_.stg_cur,percentDone:_.mc_percent,layerNumber:_.layer_num,remainingTime:_.mc_remaining_time,skippedObjects:_.s_obj,statusHistory:[{status:_.gcode_state,changedAt:a}],speedLevelHistory:[{speedLevel:_.spd_lvl,speedMagnitude:_.spd_mag,changedAt:a}],printingStageHistory:[{stage:_.stg_cur,changedAt:a}],temperatureHistory:[{bedTemp:_.bed_temper,bedTargetTemp:_.bed_target_temper,interiorTemp:o?_.frame_temper:_.chamber_temper,nozzleTemp:_.nozzle_temper,nozzleTargetTemp:_.nozzle_target_temper,changedAt:a}],tempLastUpdatedAt:new Date,errorCodes:_.hms.map(m=>({attr:m.attr,code:m.code,thrownAt:a,layerNumber:_.layer_num})),createdAt:a}}update(_){if(!this._jobData)return;let t=this._jobData,l=new Date,a=[p.P1P,p.P1S].includes(this._jobData.printerModel);_.gcode_state&&t.statusHistory.at(-1)?.status!==_.gcode_state&&t.statusHistory.push({status:_.gcode_state,changedAt:l}),_.spd_lvl&&_.spd_mag&&t.speedLevelHistory.at(-1)?.speedLevel!==_.spd_lvl&&t.speedLevelHistory.push({speedLevel:_.spd_lvl,speedMagnitude:_.spd_mag,changedAt:l}),_.stg_cur&&t.printingStageHistory.at(-1)?.stage!==_.stg_cur&&t.printingStageHistory.push({stage:_.stg_cur,changedAt:l});let o=!!(_.bed_temper||_.bed_target_temper||(a?_.frame_temper:_.chamber_temper)||_.nozzle_temper||_.nozzle_target_temper),m=t.tempLastUpdatedAt.getTime()+5e3<l.getTime();if(o&&m){let s=a?_.frame_temper:_.chamber_temper,u=t.temperatureHistory.at(-1);t.temperatureHistory.push({bedTemp:_.bed_temper??u.bedTemp,bedTargetTemp:_.bed_target_temper??u.bedTargetTemp,interiorTemp:s??u.interiorTemp,nozzleTemp:_.nozzle_temper??u.nozzleTemp,nozzleTargetTemp:_.nozzle_target_temper??u.nozzleTargetTemp,changedAt:l}),t.tempLastUpdatedAt=l}let i={status:_.gcode_state??t.status,stage:_.stg_cur??t.stage,percentDone:_.mc_percent??t.percentDone,layerNumber:_.layer_num??t.layerNumber,remainingTime:_.mc_remaining_time??t.remainingTime,skippedObjects:_.s_obj??t.skippedObjects,errorCodes:_.hms?.map((s,u)=>({attr:s.attr??t.errorCodes[u].attr,code:s.code??t.errorCodes[u].code,thrownAt:l??t.errorCodes[u].thrownAt,layerNumber:_.layer_num??t.errorCodes[u].layerNumber}))??t.errorCodes};this._jobData={...this._jobData,...i}}get data(){return this._jobData}};r(R,"Job");var w=R;import*as ce from"mqtt";import*as ye from"eventemitter3";import{createId as be}from"@paralleldrive/cuid2";function I(e){return new Promise(_=>setTimeout(_,e))}r(I,"sleep");function O(e,_){let t={},l=r((a,o,m)=>{for(let i in o){let s=o[i],u=a[i];if(!(i in a&&JSON.stringify(a[i])===JSON.stringify(o[i]))){if(typeof s=="object"&&s!==null&&u!==null&&!Array.isArray(s)){m[i]=m[i]||{},l(a[i],o[i],m[i]);continue}m[i]=o[i]}}},"checkDifferences");return l(e,_,t),t}r(O,"subtractObject");var D=class D extends ye.EventEmitter{constructor(t){super();n(this,"clientOptions");n(this,"id");n(this,"mqttClient");n(this,"config");n(this,"_printerData");n(this,"_printerStatus");n(this,"jobHistory");n(this,"currentJob");this.clientOptions=t,this.id=be(),this.mqttClient=void 0,this._printerData={modules:[],model:void 0},this._printerStatus="OFFLINE",this.jobHistory=[],this.currentJob=null,this.config={host:t.host,port:t.port??8883,accessToken:t.accessToken,serialNumber:t.serialNumber,throwOnOfflineCommands:t.throwOnOfflineCommands??!1,reconnectInterval:t.reconnectInterval??1e3,connectTimeout:t.connectTimeout??2e3,keepAlive:t.keepAlive??20},this.jobHistory=t.jobHistory??[],this.currentJob=t.currentJob??null}emit(t,...l){return super.emit(t,...l)}off(t,l){return super.off(t,l),this}once(t,l){return super.once(t,l),this}on(t,l){return super.on(t,l),this}async connectMQTT(){return new Promise((t,l)=>{if(this.mqttClient)throw new Error("Can't establish a new connection while running another one!");this.mqttClient=ce.connect(`mqtts://${this.config.host}:${this.config.port}`,{username:"bblp",password:this.config.accessToken,reconnectPeriod:this.clientOptions.reconnectInterval,connectTimeout:this.clientOptions.connectTimeout,keepalive:this.clientOptions.keepAlive,resubscribe:!0,rejectUnauthorized:!1}),this.mqttClient.on("connect",async(...a)=>{await this.onConnect(...a),this.emit("client:connect"),t()}),this.mqttClient.on("disconnect",()=>{this.emit("client:disconnect",!1),this.emit("printer:statusUpdate",this._printerStatus,"OFFLINE"),this._printerStatus="OFFLINE",this.currentJob&&this.emit("job:pause",this.currentJob,!0)}),this.mqttClient.on("offline",()=>{this.emit("client:disconnect",!0),this.emit("printer:statusUpdate",this._printerStatus,"OFFLINE"),this._printerStatus="OFFLINE",this.currentJob&&this.emit("job:pause",this.currentJob,!0)}),this.mqttClient.on("message",(a,o)=>this.emit("rawMessage",a,o)),this.mqttClient.on("error",a=>{this.emit("client:error",a),l(a)})})}async connect(){return await Promise.all([this.connectMQTT()])}async disconnect(t=!1,l){return new Promise(a=>this.mqttClient?.end(t,l,a))}subscribe(t){if(this._printerStatus!=="OFFLINE"&&this.config.throwOnOfflineCommands)throw new Error(`Unable to subscribe to topic "${t}" while disconnected from printer!`);return new Promise((l,a)=>{if(!this.mqttClient)return a("Client not connected.");this.mqttClient.subscribe(t,m=>{if(m)return a(`Error subscribing to topic '${t}': ${m.message}`)});let o=r(async(m,i)=>{m===t&&await this.onMessage(i.toString(),t)},"listener");this.mqttClient.on("message",o),l()})}async executeCommand(t){if(this._printerStatus!=="OFFLINE"&&this.config.throwOnOfflineCommands)throw new Error("Unable to send commands while disconnected from printer!");return new Promise((l,a)=>{t.invoke(this).catch(m=>a(m));let o=setTimeout(()=>{a(new Error("Command execution timed out after 5 seconds."))},5e3);this.on("rawMessage",(m,i)=>{let s=JSON.parse(i.toString()),u=Object.keys(s)[0];if(!(z(s)||g(s)||y(s)))return;let P=s[u];P?.command&&t.ownsResponse(P)&&(clearTimeout(o),l(P))})})}async onConnect(t){await this.subscribe(`device/${this.config.serialNumber}/report`),await I(1e3),await this.executeCommand(new b),(this._printerData.model===p.P1P||this._printerData.model===p.P1S||this._printerData.model===p.P2S||this._printerData.model===p.A1||this._printerData.model===p.A1M)&&(await this.executeCommand(new c),setInterval(async()=>{await this.executeCommand(new c)},5*60*1e3))}async onMessage(t,l){let a=JSON.parse(t),o=Object.keys(a)[0];if(z(a)||g(a)||y(a)){if(y(a)&&(this.emit("message",l,o,a.print),(h(a.print)||J(a.print))&&(this._printerData={...this._printerData,...a.print},this.emit("printer:dataUpdate",this._printerData,a.print),a.print.gcode_state&&a.print.gcode_state!==this._printerStatus))){let m=this._printerStatus,i=a.print.gcode_state;if(this.emit("printer:statusUpdate",m,i),(i==="PREPARE"||i==="SLICING")&&(i="IDLE"),(m==="PREPARE"||m==="SLICING")&&(m="RUNNING"),(m==="IDLE"||m==="FINISH"||m==="FAILED")&&i==="RUNNING")this.currentJob!==null&&this.jobHistory.push(this.currentJob),this.currentJob=new w(this._printerData,this.id,this._printerData.model),this.emit("job:start",this.currentJob);else if(m==="RUNNING"&&(i==="FINISH"||i==="FAILED"||i==="IDLE"))i==="FINISH"?this.currentJob!==null&&this.emit("job:finish",this.currentJob,"SUCCESS"):i==="FAILED"?this.currentJob!==null&&this.emit("job:finish",this.currentJob,"FAILED"):i==="IDLE"&&this.currentJob!==null&&this.emit("job:finish",this.currentJob,"UNEXPECTED"),this.currentJob!==null&&this.jobHistory.push(this.currentJob),this.currentJob=null;else if(m==="RUNNING"&&i==="PAUSE")this.currentJob&&this.emit("job:pause",this.currentJob,!1);else if(m==="PAUSE"&&i==="RUNNING")this.currentJob&&this.emit("job:unpause",this.currentJob);else if(m==="OFFLINE"&&i==="PAUSE")this.emit("job:offlineRecovery",this.currentJob);else if(!(m==="OFFLINE"&&i==="RUNNING")){if(!((m==="IDLE"||m==="OFFLINE")&&(i==="FINISH"||i==="FAILED"||i==="IDLE"))){if(!((m==="FINISH"||i==="FAILED")&&i==="IDLE")){if(m!==i)void 0}}}if(this._printerStatus=a.print.gcode_state,this.currentJob){let s=this.currentJob?.data;this.currentJob.update(a.print),this.emit("job:update",this.currentJob,O(s,this.currentJob.data))}}if(g(a)&&this.emit("message",l,o,a.print),z(a)&&(this.emit("message",l,o,a.info),x(a.info))){let m=[...new Set([...this._printerData.modules,...a.info.module])];this._printerData.modules=m,this.emit("printer:dataUpdate",this._printerData,{modules:m});let i=this._printerData.modules.find(s=>s.name==="ota");if(!i)return;if(i.sn.startsWith("00M"))this._printerData.model=p.X1C;else if(i.sn.startsWith("00W"))this._printerData.model=p.X1;else if(i.sn.startsWith("03W"))this._printerData.model=p.X1E;else if(i.sn.startsWith("01S"))this._printerData.model=p.P1P;else if(i.sn.startsWith("01P"))this._printerData.model=p.P1S;else if(i.sn.startsWith("22E"))this._printerData.model=p.P2S;else if(i.sn.startsWith("039"))this._printerData.model=p.A1;else if(i.sn.startsWith("030"))this._printerData.model=p.A1M;else if(i.sn.startsWith("093"))this._printerData.model=p.H2S;else if(i.sn.startsWith("094"))this._printerData.model=p.H2D;else if(i.sn.startsWith("239"))this._printerData.model=p.H2C;else if(i.sn.startsWith("31B"))this._printerData.model=p.H2DPRO;this.emit("printer:dataUpdate",this._printerData,{model:this._printerData.model})}}}publish(t){return new Promise((l,a)=>{if(!this.mqttClient)return a("Client not connected.");let o=typeof t=="string"?t:JSON.stringify(t),m=`device/${this.config.serialNumber}/request`;this.mqttClient.publish(m,o,i=>{if(i)return a(`Error publishing to topic '${m}': ${i.message}`);l()})})}get data(){return this._printerData}get status(){return this._printerStatus}};r(D,"BambuClient");var de=D;export{f as AbstractCommand,V as AmsRFIDStatus,de as BambuClient,le as Fan,Q as FilamentType,d as GCodeCommand,ie as GCodeFileCommand,ne as GCodeLineCommand,b as GetVersionCommand,w as Job,W as PrintStage,p as PrinterModel,c as PushAllCommand,X as SpeedLevel,me as UpdateFanCommand,oe as UpdateLightCommand,pe as UpdateSpeedCommand,se as UpdateStateCommand,ue as UpdateTempCommand,K as UpgradeDisplayState,Y as UpgradeErrorCode,Z as UpgradeStatusFinishState,$ as UpgradeStatusProgressState,gt as getCleanPushInfoCommand,Je as getFilamentConfig,j as isGCodeLineCommand,q as isGcodeFileCommand,x as isGetVersionCommand,z as isInfoMessage,g as isMCPrintMessage,y as isPrintMessage,Oe as isProjectFileCommand,h as isPushAllCommand,ht as isPushInfoCommand,J as isPushStatusCommand,ee as isUpdateFanCommand,te as isUpdateLightCommand,_e as isUpdateSpeedCommand,re as isUpdateStateCommand,ae as isUpdateTempCommand};
32
+ +{endif}`],filament_id:"GFL01",filament_flow_ratio:["0.98"],filament_vendor:["Polymaker"]}};var W;(function(e){e[e.PRINTING=0]="PRINTING",e[e.BED_LEVELING=1]="BED_LEVELING",e[e.HEATBED_PREHEATING=2]="HEATBED_PREHEATING",e[e.XY_MECH_MODE_SWEEP=3]="XY_MECH_MODE_SWEEP",e[e.CHANGE_MATERIAL=4]="CHANGE_MATERIAL",e[e.M400_PAUSE=5]="M400_PAUSE",e[e.FILAMENT_RUNOUT_PAUSE=6]="FILAMENT_RUNOUT_PAUSE",e[e.HOTEND_HEATING=7]="HOTEND_HEATING",e[e.EXTRUDE_COMPENSATION_SCAN=8]="EXTRUDE_COMPENSATION_SCAN",e[e.BED_SCAN=9]="BED_SCAN",e[e.FIRST_LAYER_SCAN=10]="FIRST_LAYER_SCAN",e[e.BE_SURFACE_TYPT_IDENTIFICATION=11]="BE_SURFACE_TYPT_IDENTIFICATION",e[e.SCANNER_EXTRINSIC_PARA_CALI=12]="SCANNER_EXTRINSIC_PARA_CALI",e[e.TOOLHEAD_HOMING=13]="TOOLHEAD_HOMING",e[e.NOZZLE_TIP_CLEANING=14]="NOZZLE_TIP_CLEANING",e[e.EXTRUDER_TEMP_PROTECT_CALI=15]="EXTRUDER_TEMP_PROTECT_CALI",e[e.USER_PAUSE=16]="USER_PAUSE",e[e.TOOLHEAD_SHELL_OFF_PAUSE=17]="TOOLHEAD_SHELL_OFF_PAUSE",e[e.SCANNER_LASER_PARA_CALI=18]="SCANNER_LASER_PARA_CALI",e[e.EXTRUDER_ABSOLUTE_FLOW_CALI=19]="EXTRUDER_ABSOLUTE_FLOW_CALI",e[e.HOTEND_TEMPERATURE_ERROR_PAUSE=20]="HOTEND_TEMPERATURE_ERROR_PAUSE",e[e.HEATED_BED_TEMPERATURE_ERROR_PAUSE=21]="HEATED_BED_TEMPERATURE_ERROR_PAUSE",e[e.FILAMENT_UNLOADING=22]="FILAMENT_UNLOADING",e[e.SKIP_STEP_PAUSE=23]="SKIP_STEP_PAUSE",e[e.FILAMENT_LOADING=24]="FILAMENT_LOADING",e[e.MOTOR_NOISE_CALIBRATION=25]="MOTOR_NOISE_CALIBRATION",e[e.AMS_LOST_PAUSE=26]="AMS_LOST_PAUSE",e[e.HEAT_BREAK_FAN_PAUSE=27]="HEAT_BREAK_FAN_PAUSE",e[e.CHAMBER_TEMPERATURE_CONTROL_ERROR_PAUSE=28]="CHAMBER_TEMPERATURE_CONTROL_ERROR_PAUSE",e[e.CHAMBER_COOLING=29]="CHAMBER_COOLING",e[e.USER_INSERT_GCODE_PAUSE=30]="USER_INSERT_GCODE_PAUSE",e[e.MOTOR_NOISE_SHOWOFF=31]="MOTOR_NOISE_SHOWOFF",e[e.NOZZLE_FILAMENT_COVERED_DETECTED_PAUSE=32]="NOZZLE_FILAMENT_COVERED_DETECTED_PAUSE",e[e.CUTTER_ERROR_PAUSE=33]="CUTTER_ERROR_PAUSE",e[e.FIRST_LAYER_ERROR_PAUSE=34]="FIRST_LAYER_ERROR_PAUSE",e[e.NOZZLE_CLOG_PAUSE=35]="NOZZLE_CLOG_PAUSE"})(W||(W={}));var X;(function(e){e.SILENT="1",e.NORMAL="2",e.SPORT="3",e.LUDICROUS="4"})(X||(X={}));var V;(function(e){e[e.IDLE=0]="IDLE",e[e.READING=1]="READING",e[e.GCODE_TRANS=2]="GCODE_TRANS",e[e.GCODE_RUNNING=3]="GCODE_RUNNING",e[e.ASSISTANT=4]="ASSISTANT",e[e.SWITCH_FILAMENT=5]="SWITCH_FILAMENT",e[e.HAS_FILAMENT=6]="HAS_FILAMENT"})(V||(V={}));var $;(function(e){e.DOWNLOADING="DOWNLOADING",e.UPGRADE_REQUEST="UPGRADE_REQUEST",e.PRE_FLASH_START="PRE_FLASH_START",e.PRE_FLASH_SUCCESS="PRE_FLASH_SUCCESS"})($||($={}));var Z;(function(e){e.UPGRADE_SUCCESS="UPGRADE_SUCCESS",e.DOWNLOAD_FAIL="DOWNLOAD_FAIL",e.FLASH_FAIL="FLASH_FAIL",e.PRE_FLASH_FAIL="PRE_FLASH_FAIL",e.UPGRADE_FAIL="UPGRADE_FAIL"})(Z||(Z={}));var Y;(function(e){e[e.UpgradeNoError=0]="UpgradeNoError",e[e.UpgradeDownloadFailed=1]="UpgradeDownloadFailed",e[e.UpgradeVerifyFailed=2]="UpgradeVerifyFailed",e[e.UpgradeFlashFailed=3]="UpgradeFlashFailed",e[e.UpgradePrinting=4]="UpgradePrinting"})(Y||(Y={}));var K;(function(e){e[e.UNAVAILABLE=0]="UNAVAILABLE",e[e.IDLE=1]="IDLE",e[e.UPGRADING=2]="UPGRADING",e[e.UPGRADE_FINISHED=3]="UPGRADE_FINISHED"})(K||(K={}));var Q;(function(e){e.PLA="PLA",e.ABS="ABS",e.TPU="TPU",e.PC="PC",e.ASA="ASA",e.PA_CF="PA-CF",e.PA6_CF="PA6-CF",e.PET_CF="PET-CF",e.PETG="PETG",e.PETG_CF="PETG-CF",e.PLA_AERO="PLA-AERO",e.PLA_CF="PLA-CF",e.PPA_CF="PPA-CF",e.PPA_GF="PPA-GF",e.PA="PA",e.HIPS="HIPS",e.PPS="PPS",e.PPS_CF="PPS-CF",e.PVA="PVA",e.PLA_S="PLA-S",e.PA_S="PLA-S"})(Q||(Q={}));function Je(e){return k[e]??null}r(Je,"getFilamentConfig");function h(e){return e.command==="push_status"&&Object.keys(e).length>=40}r(h,"isPushAllCommand");function ee(e){return e.command==="gcode_line"&&!!e?.param?.startsWith("M106")}r(ee,"isUpdateFanCommand");function te(e){return e.command==="push_status"&&e.hasOwnProperty("lights_report")}r(te,"isUpdateLightCommand");function _e(e){return e.command==="push_status"&&(e.hasOwnProperty("spd_lvl")||e.hasOwnProperty("spd_mag"))}r(_e,"isUpdateSpeedCommand");function re(e){return e.command===e.command}r(re,"isUpdateStateCommand");function ae(e){return e.command==="gcode_line"&&!!(e?.param?.startsWith("M104")||e?.param?.startsWith("M140"))}r(ae,"isUpdateTempCommand");function g(e){return!!e?.mc_print&&!!e?.mc_print?.command&&["push_info"].includes(e.mc_print.command)}r(g,"isMCPrintMessage");function ht(e){return e.command==="push_info"}r(ht,"isPushInfoCommand");function gt(e){let[,_,t,l]=e.param.match(/^\[(.+?)](?:\[(.+?)])?\s*(.+)$/)??[];return{sequenceId:Number(e.sequence_id),category:_,subcategory:t,content:l,rawParam:e.param}}r(gt,"getCleanPushInfoCommand");function x(e){return e.command==="get_version"}r(x,"isGetVersionCommand");function z(e){return!!e?.info&&!!e?.info?.command&&["get_version"].includes(e.info.command)}r(z,"isInfoMessage");var E=class E extends f{constructor({fileName:t}){super({param:t});n(this,"category","print");n(this,"command","gcode_file");n(this,"ownsResponse",q)}};r(E,"GCodeFileCommand");var ie=E;var S=class S extends d{constructor({gcodes:t}){super(t);n(this,"command","gcode_line");n(this,"ownsResponse",j)}};r(S,"GCodeLineCommand");var ne=S;var G=class G extends f{constructor(){super();n(this,"category","info");n(this,"command","get_version");n(this,"ownsResponse",x)}};r(G,"GetVersionCommand");var b=G;var M=class M extends f{constructor(){super();n(this,"category","pushing");n(this,"command","pushall");n(this,"ownsResponse",h)}};r(M,"PushAllCommand");var c=M;var le;(function(e){e[e.PART_COOLING_FAN=1]="PART_COOLING_FAN",e[e.AUXILIARY_FAN=2]="AUXILIARY_FAN",e[e.CHAMBER_FAN=3]="CHAMBER_FAN"})(le||(le={}));var L=class L extends d{constructor({fan:t,speed:l}){super([`M106 P${t.toString()} S${255*l/100}`]);n(this,"command","gcode_line");n(this,"ownsResponse",ee)}};r(L,"UpdateFanCommand");var me=L;var F=class F extends f{constructor({light:t,mode:l,loopOptions:a={led_on_time:0,led_off_time:0,loop_times:0,interval_time:0}}){super({led_node:t,led_mode:l,...a});n(this,"category","system");n(this,"command","ledctrl");n(this,"ownsResponse",te)}};r(F,"UpdateLightCommand");var oe=F;var N=class N extends f{constructor({speed:t}){super({param:t.toString()});n(this,"category","print");n(this,"command","print_speed");n(this,"ownsResponse",_e)}};r(N,"UpdateSpeedCommand");var pe=N;var C=class C extends f{constructor({state:t}){super();n(this,"category","print");n(this,"command");n(this,"ownsResponse",re);this.command=t}};r(C,"UpdateStateCommand");var se=C;var fe;(function(e){e.BED="M140",e.EXTRUDER="M104"})(fe||(fe={}));var T=class T extends d{constructor({part:t,temperature:l}){super([`${t} S${l}`]);n(this,"command","gcode_line");n(this,"ownsResponse",ae)}};r(T,"UpdateTempCommand");var ue=T;var p;(function(e){e.X1C="X1C",e.X1="X1",e.X1E="X1E",e.P1P="P1P",e.P1S="P1S",e.P2S="P2S",e.H2S="H2S",e.H2D="H2D",e.H2C="H2C",e.H2DPRO="H2DPRO",e.A1="A1",e.A1M="A1M"})(p||(p={}));import{createId as xe}from"@paralleldrive/cuid2";var ze=new RegExp(/\.[^/.]+$/),R=class R{constructor(_,t,l,a=new Date){n(this,"_jobData");let o=[p.P1P,p.P1S].includes(l);this._jobData={id:xe(),printerId:t,printerModel:l,name:_.subtask_name.replace(ze,""),fileName:_.subtask_name,gcodeName:_.gcode_file,jobOrigin:_.print_type,totalLayerNumber:_.total_layer_num,totalPrintTime:_.mc_remaining_time,status:_.gcode_state,stage:_.stg_cur,percentDone:_.mc_percent,layerNumber:_.layer_num,remainingTime:_.mc_remaining_time,skippedObjects:_.s_obj,statusHistory:[{status:_.gcode_state,changedAt:a}],speedLevelHistory:[{speedLevel:_.spd_lvl,speedMagnitude:_.spd_mag,changedAt:a}],printingStageHistory:[{stage:_.stg_cur,changedAt:a}],temperatureHistory:[{bedTemp:_.bed_temper,bedTargetTemp:_.bed_target_temper,interiorTemp:o?_.frame_temper:_.chamber_temper,nozzleTemp:_.nozzle_temper,nozzleTargetTemp:_.nozzle_target_temper,changedAt:a}],tempLastUpdatedAt:new Date,errorCodes:_.hms.map(m=>({attr:m.attr,code:m.code,thrownAt:a,layerNumber:_.layer_num})),createdAt:a}}update(_){if(!this._jobData)return;let t=this._jobData,l=new Date,a=[p.P1P,p.P1S].includes(this._jobData.printerModel);_.gcode_state&&t.statusHistory.at(-1)?.status!==_.gcode_state&&t.statusHistory.push({status:_.gcode_state,changedAt:l}),_.spd_lvl&&_.spd_mag&&t.speedLevelHistory.at(-1)?.speedLevel!==_.spd_lvl&&t.speedLevelHistory.push({speedLevel:_.spd_lvl,speedMagnitude:_.spd_mag,changedAt:l}),_.stg_cur&&t.printingStageHistory.at(-1)?.stage!==_.stg_cur&&t.printingStageHistory.push({stage:_.stg_cur,changedAt:l});let o=!!(_.bed_temper||_.bed_target_temper||(a?_.frame_temper:_.chamber_temper)||_.nozzle_temper||_.nozzle_target_temper),m=t.tempLastUpdatedAt.getTime()+5e3<l.getTime();if(o&&m){let s=a?_.frame_temper:_.chamber_temper,u=t.temperatureHistory.at(-1);t.temperatureHistory.push({bedTemp:_.bed_temper??u.bedTemp,bedTargetTemp:_.bed_target_temper??u.bedTargetTemp,interiorTemp:s??u.interiorTemp,nozzleTemp:_.nozzle_temper??u.nozzleTemp,nozzleTargetTemp:_.nozzle_target_temper??u.nozzleTargetTemp,changedAt:l}),t.tempLastUpdatedAt=l}let i={status:_.gcode_state??t.status,stage:_.stg_cur??t.stage,percentDone:_.mc_percent??t.percentDone,layerNumber:_.layer_num??t.layerNumber,remainingTime:_.mc_remaining_time??t.remainingTime,skippedObjects:_.s_obj??t.skippedObjects,errorCodes:_.hms?.map((s,u)=>({attr:s.attr??t.errorCodes[u].attr,code:s.code??t.errorCodes[u].code,thrownAt:l??t.errorCodes[u].thrownAt,layerNumber:_.layer_num??t.errorCodes[u].layerNumber}))??t.errorCodes};this._jobData={...this._jobData,...i}}get data(){return this._jobData}};r(R,"Job");var w=R;import*as ce from"mqtt";import*as ye from"eventemitter3";import{createId as be}from"@paralleldrive/cuid2";function I(e){return new Promise(_=>setTimeout(_,e))}r(I,"sleep");function O(e,_){let t={},l=r((a,o,m)=>{for(let i in o){let s=o[i],u=a[i];if(!(i in a&&JSON.stringify(a[i])===JSON.stringify(o[i]))){if(typeof s=="object"&&s!==null&&u!==null&&!Array.isArray(s)){m[i]=m[i]||{},l(a[i],o[i],m[i]);continue}m[i]=o[i]}}},"checkDifferences");return l(e,_,t),t}r(O,"subtractObject");var D=class D extends ye.EventEmitter{constructor(t){super();n(this,"clientOptions");n(this,"id");n(this,"mqttClient");n(this,"config");n(this,"_printerData");n(this,"_printerStatus");n(this,"jobHistory");n(this,"currentJob");this.clientOptions=t,this.id=be(),this.mqttClient=void 0,this._printerData={modules:[],model:void 0},this._printerStatus="OFFLINE",this.jobHistory=[],this.currentJob=null,this.config={host:t.host,port:t.port??8883,accessToken:t.accessToken,serialNumber:t.serialNumber,throwOnOfflineCommands:t.throwOnOfflineCommands??!1,reconnectInterval:t.reconnectInterval??1e3,connectTimeout:t.connectTimeout??2e3,keepAlive:t.keepAlive??20},this.jobHistory=t.jobHistory??[],this.currentJob=t.currentJob??null}emit(t,...l){return super.emit(t,...l)}off(t,l){return super.off(t,l),this}once(t,l){return super.once(t,l),this}on(t,l){return super.on(t,l),this}async connectMQTT(){return new Promise((t,l)=>{if(this.mqttClient)throw new Error("Can't establish a new connection while running another one!");this.mqttClient=ce.connect(`mqtts://${this.config.host}:${this.config.port}`,{username:"bblp",password:this.config.accessToken,reconnectPeriod:this.clientOptions.reconnectInterval,connectTimeout:this.clientOptions.connectTimeout,keepalive:this.clientOptions.keepAlive,resubscribe:!0,rejectUnauthorized:!1}),this.mqttClient.on("connect",async(...a)=>{await this.onConnect(...a),this.emit("client:connect"),t()}),this.mqttClient.on("disconnect",()=>{this.emit("client:disconnect",!1),this.emit("printer:statusUpdate",this._printerStatus,"OFFLINE"),this._printerStatus="OFFLINE",this.currentJob&&this.emit("job:pause",this.currentJob,!0)}),this.mqttClient.on("offline",()=>{this.emit("client:disconnect",!0),this.emit("printer:statusUpdate",this._printerStatus,"OFFLINE"),this._printerStatus="OFFLINE",this.currentJob&&this.emit("job:pause",this.currentJob,!0)}),this.mqttClient.on("message",(a,o)=>this.emit("rawMessage",a,o)),this.mqttClient.on("error",a=>{this.emit("client:error",a),l(a)})})}async connect(){return await Promise.all([this.connectMQTT()])}async disconnect(t=!1,l){return new Promise(a=>this.mqttClient?.end(t,l,a))}subscribe(t){if(this._printerStatus!=="OFFLINE"&&this.config.throwOnOfflineCommands)throw new Error(`Unable to subscribe to topic "${t}" while disconnected from printer!`);return new Promise((l,a)=>{if(!this.mqttClient)return a("Client not connected.");this.mqttClient.subscribe(t,m=>{if(m)return a(`Error subscribing to topic '${t}': ${m.message}`)});let o=r(async(m,i)=>{m===t&&await this.onMessage(i.toString(),t)},"listener");this.mqttClient.on("message",o),l()})}async executeCommand(t){if(this._printerStatus!=="OFFLINE"&&this.config.throwOnOfflineCommands)throw new Error("Unable to send commands while disconnected from printer!");return new Promise((l,a)=>{t.invoke(this).catch(m=>a(m));let o=setTimeout(()=>{a(new Error("Command execution timed out after 5 seconds."))},5e3);this.on("rawMessage",(m,i)=>{let s=JSON.parse(i.toString()),u=Object.keys(s)[0];if(!(z(s)||g(s)||y(s)))return;let P=s[u];P?.command&&t.ownsResponse(P)&&(clearTimeout(o),l(P))})})}async onConnect(t){await this.subscribe(`device/${this.config.serialNumber}/report`),await I(1e3),await this.executeCommand(new b),(this._printerData.model===p.P1P||this._printerData.model===p.P1S||this._printerData.model===p.P2S||this._printerData.model===p.A1||this._printerData.model===p.A1M)&&(await this.executeCommand(new c),setInterval(async()=>{await this.executeCommand(new c)},5*60*1e3))}async onMessage(t,l){let a=JSON.parse(t),o=Object.keys(a)[0];if(z(a)||g(a)||y(a)){if(y(a)&&(this.emit("message",l,o,a.print),(h(a.print)||J(a.print))&&(this._printerData={...this._printerData,...a.print},this.emit("printer:dataUpdate",this._printerData,a.print),a.print.gcode_state&&a.print.gcode_state!==this._printerStatus))){let m=this._printerStatus,i=a.print.gcode_state;if(this.emit("printer:statusUpdate",m,i),(i==="PREPARE"||i==="SLICING")&&(i="IDLE"),(m==="PREPARE"||m==="SLICING")&&(m="RUNNING"),(m==="IDLE"||m==="FINISH"||m==="FAILED")&&i==="RUNNING")this.currentJob!==null&&this.jobHistory.push(this.currentJob),this.currentJob=new w(this._printerData,this.id,this._printerData.model),this.emit("job:start",this.currentJob);else if(m==="RUNNING"&&(i==="FINISH"||i==="FAILED"||i==="IDLE"))i==="FINISH"?this.currentJob!==null&&this.emit("job:finish",this.currentJob,"SUCCESS"):i==="FAILED"?this.currentJob!==null&&this.emit("job:finish",this.currentJob,"FAILED"):i==="IDLE"&&this.currentJob!==null&&this.emit("job:finish",this.currentJob,"UNEXPECTED"),this.currentJob!==null&&this.jobHistory.push(this.currentJob),this.currentJob=null;else if(m==="RUNNING"&&i==="PAUSE")this.currentJob&&this.emit("job:pause",this.currentJob,!1);else if(m==="PAUSE"&&i==="RUNNING")this.currentJob&&this.emit("job:unpause",this.currentJob);else if(m==="OFFLINE"&&i==="PAUSE")this.emit("job:offlineRecovery",this.currentJob);else if(!(m==="OFFLINE"&&i==="RUNNING")){if(!((m==="IDLE"||m==="OFFLINE")&&(i==="FINISH"||i==="FAILED"||i==="IDLE"))){if(!((m==="FINISH"||i==="FAILED")&&i==="IDLE")){if(m!==i)void 0}}}if(this._printerStatus=a.print.gcode_state,this.currentJob){let s=this.currentJob?.data;this.currentJob.update(a.print),this.emit("job:update",this.currentJob,O(s,this.currentJob.data))}}if(g(a)&&this.emit("message",l,o,a.print),z(a)&&(this.emit("message",l,o,a.info),x(a.info))){let m=[...new Set([...this._printerData.modules,...a.info.module])];this._printerData.modules=m,this.emit("printer:dataUpdate",this._printerData,{modules:m});let i=this._printerData.modules.find(s=>s.name==="ota");if(!i)return;if(i.sn.startsWith("00M"))this._printerData.model=p.X1C;else if(i.sn.startsWith("00W"))this._printerData.model=p.X1;else if(i.sn.startsWith("03W"))this._printerData.model=p.X1E;else if(i.sn.startsWith("01S"))this._printerData.model=p.P1P;else if(i.sn.startsWith("01P"))this._printerData.model=p.P1S;else if(i.sn.startsWith("22E"))this._printerData.model=p.P2S;else if(i.sn.startsWith("039"))this._printerData.model=p.A1;else if(i.sn.startsWith("030"))this._printerData.model=p.A1M;else if(i.sn.startsWith("093"))this._printerData.model=p.H2S;else if(i.sn.startsWith("094"))this._printerData.model=p.H2D;else if(i.sn.startsWith("31B"))this._printerData.model=p.H2C;else if(i.sn.startsWith("239"))this._printerData.model=p.H2DPRO;this.emit("printer:dataUpdate",this._printerData,{model:this._printerData.model})}}}publish(t){return new Promise((l,a)=>{if(!this.mqttClient)return a("Client not connected.");let o=typeof t=="string"?t:JSON.stringify(t),m=`device/${this.config.serialNumber}/request`;this.mqttClient.publish(m,o,i=>{if(i)return a(`Error publishing to topic '${m}': ${i.message}`);l()})})}get data(){return this._printerData}get status(){return this._printerStatus}};r(D,"BambuClient");var de=D;export{f as AbstractCommand,V as AmsRFIDStatus,de as BambuClient,le as Fan,Q as FilamentType,d as GCodeCommand,ie as GCodeFileCommand,ne as GCodeLineCommand,b as GetVersionCommand,w as Job,W as PrintStage,p as PrinterModel,c as PushAllCommand,X as SpeedLevel,me as UpdateFanCommand,oe as UpdateLightCommand,pe as UpdateSpeedCommand,se as UpdateStateCommand,ue as UpdateTempCommand,K as UpgradeDisplayState,Y as UpgradeErrorCode,Z as UpgradeStatusFinishState,$ as UpgradeStatusProgressState,gt as getCleanPushInfoCommand,Je as getFilamentConfig,j as isGCodeLineCommand,q as isGcodeFileCommand,x as isGetVersionCommand,z as isInfoMessage,g as isMCPrintMessage,y as isPrintMessage,Oe as isProjectFileCommand,h as isPushAllCommand,ht as isPushInfoCommand,J as isPushStatusCommand,ee as isUpdateFanCommand,te as isUpdateLightCommand,_e as isUpdateSpeedCommand,re as isUpdateStateCommand,ae as isUpdateTempCommand};
@@ -37,6 +37,8 @@ export type BambuNativeControlOptions = {
37
37
 
38
38
  type NativeHelperResult = {
39
39
  resultCode: number;
40
+ signal: NodeJS.Signals | null;
41
+ authorizedAttempts: number;
40
42
  updates: string[];
41
43
  stderr: string;
42
44
  };
@@ -244,7 +246,7 @@ function runNativeHelper(
244
246
  settled = true;
245
247
  cleanup();
246
248
  if (failure) { reject(failure); return; }
247
- if (needsAuthorization && code === 0 && (authorizing || authorizedAttempts === 0)) {
249
+ if (needsAuthorization && (code === 0 || updates.includes("native_print result=0") || stdout.trim() === "native_print result=0") && (authorizing || authorizedAttempts === 0)) {
248
250
  reject(new Error("Native helper exited without completed dispatch authorization."));
249
251
  return;
250
252
  }
@@ -253,7 +255,7 @@ function runNativeHelper(
253
255
  updates.push(trailing);
254
256
  onUpdate?.(trailing);
255
257
  }
256
- resolve({ resultCode: code ?? (signal ? 1 : 0), updates, stderr });
258
+ resolve({ resultCode: code ?? (signal ? 1 : 0), signal, authorizedAttempts, updates, stderr });
257
259
  });
258
260
  });
259
261
  }
@@ -653,6 +655,10 @@ export async function printWithBambuNative(
653
655
  execution
654
656
  );
655
657
  if (result.resultCode !== 0) {
658
+ const explicitRejection = result.updates.some(line => /^native_print result=-?[1-9]\d*$/.test(line));
659
+ if (result.authorizedAttempts > 0 && (result.updates.includes("native_print result=0") || (result.signal && !explicitRejection))) {
660
+ throw new Error("Bambu native print dispatch is uncertain: the printer may have accepted this job before the helper exited. Check printer status and the job name before retrying; do not automatically resend it.");
661
+ }
656
662
  const detail = [
657
663
  ...result.updates,
658
664
  ...(result.stderr ? [result.stderr.trim()] : []),
package/src/index.ts CHANGED
@@ -2900,6 +2900,23 @@ class BambuPrinterMCPServer {
2900
2900
  required: ["action", "ams_id"]
2901
2901
  }
2902
2902
  },
2903
+ {
2904
+ name: "inspect_print_file",
2905
+ description: "Inspect an exact private snapshot of a prepared sliced 3MF or textual G-code file and selected plate without contacting a printer. Returns versioned file-only evidence; no printer readiness or permission to dispatch is established. Does not auto-slice.",
2906
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
2907
+ inputSchema: {
2908
+ type: "object",
2909
+ additionalProperties: false,
2910
+ properties: {
2911
+ file_path: { type: "string", minLength: 1, description: "Local prepared .3mf or textual .gcode artifact to inspect; checksum-only and unsliced projects reject." },
2912
+ bambu_model: { type: "string", enum: [...VALID_BAMBU_MODELS], description: "Explicit file-inspection target model; never inferred from environment or live state." },
2913
+ plate_index: { type: "integer", minimum: 0, description: "Zero-based selected plate; defaults to 0. Raw G-code only supports 0." },
2914
+ nozzle_diameters: { type: "array", minItems: 1, maxItems: 2, items: { type: "number", enum: [0.2, 0.4, 0.6, 0.8] }, description: "Optional requested nozzle diameters to compare with file declarations; this does not observe installed hardware." },
2915
+ bed_type: { type: "string", enum: [...VALID_BED_TYPES], description: "Optional declared bed type to compare with file metadata." },
2916
+ },
2917
+ required: ["file_path", "bambu_model"],
2918
+ },
2919
+ },
2903
2920
  {
2904
2921
  name: "print_3mf",
2905
2922
  description: "Print a 3MF file on a Bambu Lab printer. Auto-slices if the 3MF has no gcode. IMPORTANT: bambu_model must be specified to ensure safe printer operation.",
@@ -3147,6 +3164,34 @@ class BambuPrinterMCPServer {
3147
3164
  let result;
3148
3165
 
3149
3166
  switch (name) {
3167
+ case "inspect_print_file": {
3168
+ const allowed = new Set(["file_path", "bambu_model", "plate_index", "nozzle_diameters", "bed_type"]);
3169
+ if (!args || Object.keys(args).some(key => !allowed.has(key))) throw new Error("inspect_print_file accepts only file/model/plate/nozzle/bed arguments.");
3170
+ if (typeof args.file_path !== "string" || !args.file_path.trim() || args.file_path.includes("\0")) throw new Error("file_path must name a local prepared artifact.");
3171
+ if (typeof args.bambu_model !== "string" || !args.bambu_model.trim()) throw new Error("An explicit bambu_model is required for file inspection.");
3172
+ const model = validateBambuModel(args.bambu_model);
3173
+ const plateIndex = args.plate_index === undefined ? 0 : args.plate_index;
3174
+ if (typeof plateIndex !== "number" || !Number.isSafeInteger(plateIndex) || plateIndex < 0) throw new Error("plate_index must be a nonnegative safe integer.");
3175
+ if (args.nozzle_diameters !== undefined && (!Array.isArray(args.nozzle_diameters) || args.nozzle_diameters.some(value => typeof value !== "number"))) throw new Error("nozzle_diameters must be numeric sizes.");
3176
+ const nozzles = requestedPrintNozzles(args);
3177
+ if (args.bed_type !== undefined && (typeof args.bed_type !== "string" || !args.bed_type.trim())) throw new Error("bed_type must be a supported plate type.");
3178
+ const bedType = args.bed_type === undefined ? undefined : resolveBedType(args.bed_type);
3179
+ if (extra.signal.aborted) throw new Error("File inspection cancelled.");
3180
+ result = await withPrintSnapshot(args.file_path, async snapshot => {
3181
+ const inspection = await inspectPrintFile(snapshot, { model, nozzleDiameters: nozzles, plateIndex, bedType });
3182
+ if (extra.signal.aborted) throw new Error("File inspection cancelled.");
3183
+ const byteLength = (await fs.promises.stat(snapshot)).size;
3184
+ return {
3185
+ receipt_version: "1",
3186
+ evidence: "file-only",
3187
+ dispatch_authorized: false,
3188
+ artifact: { sha256: inspection.sha256, byte_length: byteLength, file_format: path.extname(snapshot).toLowerCase() === ".3mf" ? "3mf" : "gcode" },
3189
+ selection: { bambu_model: model, plate_index: plateIndex, ...(nozzles === undefined ? {} : { nozzle_diameters: nozzles }), ...(bedType === undefined ? {} : { bed_type: bedType }) },
3190
+ inspection,
3191
+ };
3192
+ });
3193
+ break;
3194
+ }
3150
3195
  case "get_printer_status":
3151
3196
  result = await this.bambu.getStatus(host, bambuSerial, bambuToken);
3152
3197
  break;
@@ -183,8 +183,8 @@ class TolerantBambuClient extends BambuClient {
183
183
  const sn = this.config.serialNumber;
184
184
  if (sn.startsWith("093")) return "H2S";
185
185
  if (sn.startsWith("094")) return "H2D";
186
- if (sn.startsWith("239")) return "H2C";
187
- if (sn.startsWith("31B")) return "H2DPRO";
186
+ if (sn.startsWith("31B")) return "H2C";
187
+ if (sn.startsWith("239")) return "H2DPRO";
188
188
  if (sn.startsWith("20P")) return "X2D";
189
189
  if (sn.startsWith("00M")) return "X1C";
190
190
  if (sn.startsWith("00W")) return "X1";
@@ -70,7 +70,7 @@ function parameters(text: string, flags = ''): Map<string,number> {
70
70
  return result;
71
71
  }
72
72
  // Layer comments only close a startup exception; they can never enable one.
73
- const isLayerMarker=(line:string):boolean=>/^\s*;\s*(?:LAYER_CHANGE|CHANGE_LAYER|LAYER\s*:|layer num\/total_layer_count\s*:)/i.test(line);
73
+ const isLayerMarker=(line:string):boolean=>/^\s*;\s*(?:(?:LAYER_CHANGE|CHANGE_LAYER)(?:\s|$)|LAYER\s*:|layer num\/total_layer_count\s*:)/i.test(line);
74
74
  function boundedX1ePurge(lines:string[],index:number):boolean {
75
75
  // Official X1E common flush: 50 mm at 200 mm/min, followed immediately by
76
76
  // lowering the target. No dwell, XY extrusion, repeated block or open-ended
@@ -249,6 +249,10 @@ export async function inspectPrintFile(filePath: string, options: {model:string;
249
249
  let nozzleTarget=0;let selectsAms=false;let pending: number|undefined;let previous: number|undefined;
250
250
  let maxNozzleTemperature=0,maxBedTemperature=0,maxChamberTemperature=0,commandCount=0;
251
251
  let depositionStarted=false,commonFlushUsed=false;
252
+ // Bound the H2C vendor forms to its one explicit executable startup block.
253
+ const startupStarts=lines.flatMap((line,index)=>/^\s*;\s*EXECUTABLE_BLOCK_START\s*$/i.test(line)?[index]:[]);
254
+ const startupEnds=lines.flatMap((line,index)=>/^\s*;\s*MACHINE_START_GCODE_END\s*$/i.test(line)?[index]:[]);
255
+ const h2cStartup=(index:number)=>model==='h2c' && !depositionStarted && startupStarts.length===1 && startupEnds.length===1 && index>startupStarts[0] && index<startupEnds[0];
252
256
  const heat=(component:'nozzle'|'bed'|'chamber',value:number,position?:number,allMaterials=false,startupPurge=false)=>{
253
257
  const affected=allMaterials || position===undefined ? materials.map((_,index)=>index) : [requirePosition(position)];
254
258
  if(component==='nozzle' && startupPurge) validateStartupPurgeTemperature(value,model,affected.map(index=>materials[index]));
@@ -267,15 +271,20 @@ export async function inspectPrintFile(filePath: string, options: {model:string;
267
271
  };
268
272
  for(let index=0;index<lines.length;index++) {
269
273
  try {
270
- if(isLayerMarker(lines[index])) closeStartupWindow();
274
+ if(isLayerMarker(lines[index]) || /^\s*;\s*MACHINE_START_GCODE_END\s*$/i.test(lines[index])) closeStartupWindow();
271
275
  let line=stripComments(lines[index]);if(!line||line==='%') continue;
276
+ // The official H2C template emits this literal separator without a semicolon.
277
+ if(line==='========== record data ==========' && h2cStartup(index)) continue;
272
278
  line=line.replace(/^N\d+\s*/i,'').replace(/\*\d+\s*$/,'');
273
279
  if(/[{}\[\]#]/.test(line)) fail('unresolved dynamic command syntax');
274
280
  if(/^SYNC(?=$|\s)/i.test(line)) {
275
- // H2D change_filament_gcode emits SYNC T{ceil(flush_length / 125) * 5}.
276
- // T is a synchronization duration here, not a nozzle temperature. The
277
- // vendor template establishes no independent maximum duration.
281
+ // Vendor SYNC T is a duration, not a heater target. X2D and P2S
282
+ // startup templates additionally emit R0; no other R form is supported.
278
283
  const args=parameters(line.slice(4));
284
+ if(args.has('R')) {
285
+ if(!['x2d','p2s'].includes(model) || args.get('R')!==0) fail('unsupported SYNC reset parameters');
286
+ args.delete('R');
287
+ }
279
288
  if(args.size!==1 || !args.has('T') || !Number.isFinite(args.get('T')) || args.get('T')!<0) fail('unsupported SYNC duration parameters');
280
289
  commandCount++;continue;
281
290
  }
@@ -299,6 +308,27 @@ export async function inspectPrintFile(filePath: string, options: {model:string;
299
308
  // Official A1 and A1 mini start G-code waits with M109 S<target> H<limit>
300
309
  // (S25 H140, S100 H170, S220 H300). H is not a heater target: S stays the
301
310
  // checked target, so H is accepted only within those vendor bounds.
311
+ if(args.has('O')) {
312
+ // H2C's exact all-hotend preheat offset is relative to the declared
313
+ // first-layer targets. Check every base and derived target, never use
314
+ // editable maxima or an unchecked offset as thermal evidence.
315
+ if(code!=='M104' || !h2cStartup(index) || args.size!==2 || args.get('O')!==-80 || !Number.isNaN(args.get('A')!)) fail('unsupported H2C startup offset parameters');
316
+ const targets=consistent(metadataValues(metadata,['nozzle_temperature_initial_layer']),value=>{
317
+ const values=list(value);return values.length===materials.length && values.every(v=>NUMBER.test(v) && Number.isFinite(Number(v)))?values.map(Number):undefined;
318
+ },'first-layer nozzle temperature');
319
+ for(let position=0;position<targets.length;position++) {
320
+ validateTemperature('nozzle',targets[position],model,[materials[position]]);
321
+ const preheat=Math.max(0,targets[position]-80);
322
+ validateTemperature('nozzle',preheat,model,[materials[position]]);
323
+ nozzleTarget=Math.max(nozzleTarget,preheat);
324
+ maxNozzleTemperature=Math.max(maxNozzleTemperature,preheat);
325
+ }
326
+ continue;
327
+ }
328
+ if(args.has('D')) {
329
+ if(!['M140','M190'].includes(code) || !h2cStartup(index) || args.size!==1) fail('unsupported H2C startup bed parameters');
330
+ heat('bed',args.get('D')!,undefined,true);continue;
331
+ }
302
332
  const vendorWait=code==='M109' && ['a1','a1mini'].includes(model);
303
333
  for(const key of args.keys()) if(!'SRTA'.includes(key) && !(key==='H' && vendorWait)) fail(`unsupported ${code} temperature parameter ${key}`);
304
334
  if(args.has('H') && (!Number.isFinite(args.get('H')) || args.get('H')!<0 || args.get('H')!>300)) fail(`unsupported ${code} wait parameter H`);
@@ -312,7 +342,8 @@ export async function inspectPrintFile(filePath: string, options: {model:string;
312
342
  if(commonFlush) commonFlushUsed=true;
313
343
  for(const key of ['S','R']) if(args.has(key)) heat(component,args.get(key)!,position,args.has('T') || args.has('A'),commonFlush);
314
344
  } else if(code==='M620' || code==='M621') {
315
- const args=parameters(argumentsText,model==='x2d'?'MAB':'MA');
345
+ const args=parameters(argumentsText,model==='x2d'?'MAB':model==='h2c'?'MAN':'MA');
346
+ if(args.has('N') && (code!=='M620' || !h2cStartup(index) || args.size!==1 || !Number.isNaN(args.get('N')!))) fail('unsupported H2C startup hotend-remap flag');
316
347
  // X2D templates use B as a bare material-switch flag, never a target.
317
348
  if(args.has('B') && (model!=='x2d' || !Number.isNaN(args.get('B')!))) fail('unsupported material-switch B flag');
318
349
  if(args.has('S')) {
@@ -338,10 +369,22 @@ export async function inspectPrintFile(filePath: string, options: {model:string;
338
369
  if(args.has('T')) heat('nozzle',args.get('T')!,active);
339
370
  } else if(/^G150\./.test(code) && /T/i.test(argumentsText)) {
340
371
  fail(`unsupported thermal-affecting wipe command ${code}`);
372
+ } else if(code==='M620.14') {
373
+ const args=parameters(argumentsText);
374
+ // H2C tower coordinates include the rear wipe area outside object bounds.
375
+ 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)) fail('unsupported H2C startup tower parameters');
376
+ } else if(model==='h2c' && ['G383','G383.3','G383.7'].includes(code) && /U/i.test(argumentsText)) {
377
+ const args=parameters(argumentsText);
378
+ const expected=code==='G383.7'?'UJ':code==='G383.3'?'UL':'OUL';
379
+ if(!h2cStartup(index) || args.size!==expected.length || [...args.keys()].some(key=>!expected.includes(key)) || args.get('U')!==140 ||
380
+ (code==='G383.7' && args.get('J')!==0) || (code==='G383' && args.get('O')!==2)) fail('unsupported H2C startup probing parameters');
381
+ const position=args.has('L')?requirePosition(args.get('L')!):active;
382
+ heat('nozzle',args.get('U')!,position);
341
383
  } else if(code==='G383' || code==='G383.3') {
342
384
  // Bambu probing routines carry nozzle temperature in T and optionally
343
385
  // the project filament position in L (including the H2D G383.3 form).
344
386
  const args=parameters(argumentsText);
387
+ if(args.has('U')) fail('unsupported G383 temperature parameter U outside H2C startup');
345
388
  if(code==='G383.3' && (!args.has('T') || [...args.keys()].some(key=>!'TL'.includes(key)))) fail('unsupported G383.3 temperature parameters');
346
389
  const position=args.has('L')?requirePosition(args.get('L')!):active;
347
390
  if(args.has('T')) heat('nozzle',args.get('T')!,position);
@@ -7,7 +7,7 @@ const MODEL_IDS: Record<string, string> = {
7
7
  N2S: "A1", A1M: "A1 Mini", C11: "P1P", C12: "P1S", C13: "X1E", "BL-P001": "X1C", "BL-P002": "X1",
8
8
  };
9
9
  const SERIAL_MODELS: Record<string, string> = {
10
- "093": "H2S", "094": "H2D", "239": "H2C", "31B": "H2D Pro", "20P": "X2D",
10
+ "093": "H2S", "094": "H2D", "31B": "H2C", "239": "H2D Pro", "20P": "X2D",
11
11
  "00M": "X1C", "00W": "X1", "03W": "X1E", "01S": "P1P", "01P": "P1S", "22E": "P2S", "039": "A1", "030": "A1 Mini",
12
12
  };
13
13
 
@@ -0,0 +1,21 @@
1
+ import { lstatSync, readdirSync } from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ /** Discover vendor subfolders without following links into another profile tree. */
5
+ export function profileJsonFiles(directory: string): string[] {
6
+ let entries;
7
+ try {
8
+ if (lstatSync(directory).isSymbolicLink()) return [];
9
+ entries = readdirSync(directory, { withFileTypes: true });
10
+ } catch (error) {
11
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') return [];
12
+ throw error;
13
+ }
14
+ entries.sort((a, b) => a.name.localeCompare(b.name));
15
+ const files = entries.filter(entry => entry.isFile() && entry.name.endsWith('.json'))
16
+ .map(entry => path.join(directory, entry.name));
17
+ for (const entry of entries) {
18
+ if (entry.isDirectory()) files.push(...profileJsonFiles(path.join(directory, entry.name)));
19
+ }
20
+ return files;
21
+ }
@@ -28,6 +28,7 @@ import fs from "node:fs/promises";
28
28
  import { existsSync, realpathSync } from "node:fs";
29
29
  import path from "node:path";
30
30
  import crypto from "node:crypto";
31
+ import { profileJsonFiles } from './profile-files.js';
31
32
 
32
33
  export type ProfileKind = "machine" | "process" | "filament";
33
34
 
@@ -125,17 +126,7 @@ async function buildNameIndex(
125
126
  ...userProfileRoots.flatMap(root => ['process', 'filament'].map(sub => path.join(root, sub))),
126
127
  ];
127
128
  for (const dir of directories) {
128
- let entries: string[];
129
- try {
130
- entries = await fs.readdir(dir);
131
- } catch {
132
- // Vendor or subdir missing -- skip silently; flatten will fail later
133
- // with a precise "name not found" error.
134
- continue;
135
- }
136
- for (const entry of entries) {
137
- if (!entry.endsWith(".json")) continue;
138
- const filePath = path.join(dir, entry);
129
+ for (const filePath of profileJsonFiles(dir)) {
139
130
  let raw: string;
140
131
  try {
141
132
  raw = await fs.readFile(filePath, "utf8");
@@ -10,6 +10,7 @@ import { EventEmitter } from 'events';
10
10
  import * as crypto from 'crypto';
11
11
  import { execFile } from 'child_process';
12
12
  import { flattenForCli, detectProfilesRoot, resolveBblMachineProfile } from '../slicer/profile-flatten.js';
13
+ import { profileJsonFiles } from '../slicer/profile-files.js';
13
14
 
14
15
  const readFileAsync = promisify(fs.readFile);
15
16
  const writeFileAsync = promisify(fs.writeFile);
@@ -232,10 +233,9 @@ export class STLManipulator extends EventEmitter {
232
233
  }
233
234
 
234
235
  for (const root of roots) {
235
- const candidate = path.join(root, category, `${profileName}.json`);
236
- if (fs.existsSync(candidate)) {
237
- return candidate;
238
- }
236
+ const candidate = profileJsonFiles(path.join(root, category))
237
+ .find(file => path.basename(file) === `${profileName}.json`);
238
+ if (candidate) return candidate;
239
239
  }
240
240
 
241
241
  return undefined;
@@ -250,12 +250,7 @@ export class STLManipulator extends EventEmitter {
250
250
  continue;
251
251
  }
252
252
 
253
- for (const entry of fs.readdirSync(filamentDir)) {
254
- if (!entry.endsWith('.json')) {
255
- continue;
256
- }
257
-
258
- const filePath = path.join(filamentDir, entry);
253
+ for (const filePath of profileJsonFiles(filamentDir)) {
259
254
  try {
260
255
  const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8'));
261
256
  const filamentId = typeof parsed?.filament_id === 'string' ? parsed.filament_id.trim() : '';