bambu-printer-mcp 1.1.24 → 1.1.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CONTRIBUTORS.md CHANGED
@@ -6,6 +6,9 @@ Thank you to everyone who builds, tests, reports problems, and shares real print
6
6
 
7
7
  | Contributor | Contribution |
8
8
  | --- | --- |
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
+ | [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. |
9
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. |
10
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. |
11
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
@@ -191,7 +191,7 @@ The optional FULU **BambuNetwork bridge** exposes `bambu_network_bridge_status`,
191
191
  ## Features
192
192
 
193
193
  - Get detailed printer status: temperatures (nozzle, bed, chamber), print progress, current layer, time remaining, and live AMS slot data
194
- - Query live AMS inventory with resolved Bambu/Orca filament profile paths via `get_printer_filaments`. Includes per-tray display names, match confidence (`high`/`medium`/`low`/`none`), resolution tier (`exact-model-nozzle`/`model`/`generic`/`unresolved`), and a summary with recommended auto-slice filament. Retries automatically when AMS data hasn't arrived yet (common on first MQTT push from idle printers).
194
+ - Query live AMS inventory with resolved Bambu/Orca filament profile paths via `get_printer_filaments`. Profile suggestions use the selected slicer installation on Windows, macOS and Linux, including an explicit `BAMBU_PROFILES_ROOT`; unavailable profiles leave live inventory readable. Includes per-tray display names, match confidence (`high`/`medium`/`low`/`none`), resolution tier (`exact-model-nozzle`/`model`/`generic`/`unresolved`), and a summary with recommended auto-slice filament. Retries automatically when AMS data hasn't arrived yet (common on first MQTT push from idle printers).
195
195
  - List, upload, and delete files on the printer's SD card via FTPS
196
196
  - Capture a JPEG snapshot from the chamber camera. Supports A1, A1 mini, P1S, P1P (TCP-on-6000), and X1, X1C, X1E, P2S, H2, H2S, H2D, H2C, H2D Pro, X2D (RTSP via ffmpeg). Requires ffmpeg in PATH for the RTSP path.
197
197
  - Upload and print pre-sliced `.3mf` projects with checked plate selection and calibration flags. Legacy `.gcode.3mf` routes require a single external-spool-only plate; see the [slicing guide](https://github.com/DMontgomery40/bambu-printer-mcp/blob/main/docs/SLICING.md).
@@ -568,6 +568,8 @@ Retrieve current printer state including temperatures, print progress, layer cou
568
568
  }
569
569
  ```
570
570
 
571
+ Chamber status decodes the current temperature from newer firmware's packed CTC reports, including P2S, while retaining legacy chamber/frame fields.
572
+
571
573
  Returns a structured object with fields including `status` (gcode_state string), `temperatures.nozzle`, `temperatures.bed`, `temperatures.chamber`, `print.progress`, `print.currentLayer`, `print.totalLayers`, `print.timeRemaining`, and `ams` (raw AMS data from the printer).
572
574
 
573
575
  #### get_printer_filaments
@@ -930,7 +932,7 @@ The npm package and desktop bundle include `native/bambu-native-print.cpp` and `
930
932
 
931
933
  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.
932
934
 
933
- 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.
935
+ 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.
934
936
 
935
937
  #### x2d_native_control
936
938
 
@@ -90,7 +90,7 @@ async function parseBambuConfig(zip) {
90
90
  if (file) {
91
91
  configFile = file;
92
92
  configContent = await file.async('string');
93
- console.log(`Found Bambu config file: ${configFile.name}`);
93
+ console.error(`Found Bambu config file: ${configFile.name}`);
94
94
  break; // Use the first one found (project usually has the most)
95
95
  }
96
96
  }
@@ -99,7 +99,7 @@ async function parseBambuConfig(zip) {
99
99
  // Attempt to parse as JSON
100
100
  const jsonData = JSON.parse(configContent);
101
101
  const parsedConfig = parseBambuJSONConfig(jsonData);
102
- console.log('Bambu config parsed successfully as JSON.');
102
+ console.error('Bambu config parsed successfully as JSON.');
103
103
  return parsedConfig;
104
104
  }
105
105
  catch (jsonError) {
@@ -107,7 +107,7 @@ async function parseBambuConfig(zip) {
107
107
  // Fallback: Try parsing as INI if JSON fails (though unlikely based on sample)
108
108
  try {
109
109
  const parsedConfig = parseFallbackINIConfig(configContent); // Keep a simple INI parser as fallback
110
- console.log('Bambu config parsed successfully as INI (fallback).');
110
+ console.error('Bambu config parsed successfully as INI (fallback).');
111
111
  return parsedConfig;
112
112
  }
113
113
  catch (iniError) {
@@ -117,7 +117,7 @@ async function parseBambuConfig(zip) {
117
117
  }
118
118
  }
119
119
  else {
120
- console.log('No Bambu-specific config file found in Metadata directory.');
120
+ console.error('No Bambu-specific config file found in Metadata directory.');
121
121
  return {};
122
122
  }
123
123
  }
@@ -186,7 +186,7 @@ async function parse3DModelConfig(zip) {
186
186
  });
187
187
  });
188
188
  }
189
- console.log('3dmodel.model parsed successfully.');
189
+ console.error('3dmodel.model parsed successfully.');
190
190
  return { metadata, objects, build: { items: buildItems } };
191
191
  }
192
192
  catch (error) {
@@ -195,13 +195,13 @@ async function parse3DModelConfig(zip) {
195
195
  }
196
196
  }
197
197
  export async function parse3MF(filePath) {
198
- console.log(`Parsing 3MF file: ${filePath}`);
198
+ console.error(`Parsing 3MF file: ${filePath}`);
199
199
  try {
200
200
  const data = await fs.readFile(filePath);
201
201
  const zip = await JSZip.loadAsync(data);
202
202
  // List files for debugging
203
203
  // const fileList = Object.keys(zip.files);
204
- // console.log('Files in 3MF:', fileList);
204
+ // console.error('Files in 3MF:', fileList);
205
205
  const { metadata, objects, build } = await parse3DModelConfig(zip);
206
206
  const bambuConfig = await parseBambuConfig(zip);
207
207
  // Combine data into the final structure
@@ -211,7 +211,7 @@ export async function parse3MF(filePath) {
211
211
  build,
212
212
  slicerConfig: bambuConfig
213
213
  };
214
- console.log('3MF parsing completed.');
214
+ console.error('3MF parsing completed.');
215
215
  return combinedData;
216
216
  }
217
217
  catch (error) {
@@ -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
@@ -20,6 +20,7 @@ import { BambuImplementation, assertDirectPrintSupported } from "./printers/bamb
20
20
  import { inspectPrintFile } from "./safety/print-file.js";
21
21
  import { validatePrinterState } from "./safety/printer-state.js";
22
22
  import { withPrinterOperation, withPrintSnapshot, uniquePrintName } from "./safety/artifact.js";
23
+ import { detectProfilesRoot } from "./slicer/profile-flatten.js";
23
24
  dotenv.config();
24
25
  const DEFAULT_HOST = process.env.BAMBU_PRINTER_HOST || process.env.PRINTER_HOST || "localhost";
25
26
  const DEFAULT_BAMBU_SERIAL = process.env.BAMBU_PRINTER_SERIAL || process.env.BAMBU_SERIAL || "";
@@ -104,7 +105,6 @@ const BAMBU_MODEL_PRESETS = {
104
105
  h2c: (n) => `Bambu Lab H2C ${n} nozzle`,
105
106
  x2d: (n) => `Bambu Lab X2D ${n} nozzle`,
106
107
  };
107
- const FILAMENT_PROFILE_DIR = "/Applications/BambuStudio.app/Contents/Resources/profiles/BBL/filament";
108
108
  const FILAMENT_MODEL_CODES = {
109
109
  p1s: "P1S",
110
110
  p1p: "P1P",
@@ -123,13 +123,27 @@ let filamentProfileIndexCache = null;
123
123
  function buildFilamentProfileIndex() {
124
124
  const byName = new Map();
125
125
  const baseNameByFilamentId = new Map();
126
- if (!fs.existsSync(FILAMENT_PROFILE_DIR)) {
126
+ let filamentProfileDir;
127
+ try {
128
+ const { slicerType, slicerPath } = resolveSlicerConfig(undefined);
129
+ filamentProfileDir = path.join(detectProfilesRoot(slicerPath, slicerType), "BBL", "filament");
130
+ }
131
+ catch {
132
+ // Profile suggestions are optional; invalid slicer configuration must not hide AMS inventory.
127
133
  return { byName, baseNameByFilamentId };
128
134
  }
129
- for (const entry of fs.readdirSync(FILAMENT_PROFILE_DIR)) {
135
+ let profileEntries;
136
+ try {
137
+ profileEntries = fs.readdirSync(filamentProfileDir);
138
+ }
139
+ catch {
140
+ // Ignore unavailable profile suggestions while keeping live inventory readable.
141
+ return { byName, baseNameByFilamentId };
142
+ }
143
+ for (const entry of profileEntries) {
130
144
  if (!entry.endsWith(".json"))
131
145
  continue;
132
- const filePath = path.join(FILAMENT_PROFILE_DIR, entry);
146
+ const filePath = path.join(filamentProfileDir, entry);
133
147
  try {
134
148
  const raw = fs.readFileSync(filePath, "utf8");
135
149
  const parsed = JSON.parse(raw);
@@ -880,6 +894,9 @@ class BambuPrinterMCPServer {
880
894
  required: ["confirmed"],
881
895
  },
882
896
  }, { timeout: this.confirmationTimeoutMs });
897
+ if (response.action === "accept" && response.content?.confirmed !== true) {
898
+ throw new Error("Hardware confirmation checkbox was not checked. Check it only after inspecting the printer, or cancel. No command was sent.");
899
+ }
883
900
  return response.action === "accept" && response.content?.confirmed === true;
884
901
  }
885
902
  catch (error) {
@@ -1015,7 +1032,7 @@ class BambuPrinterMCPServer {
1015
1032
  if (bedType === "supertack_plate") {
1016
1033
  throw new Error('BambuStudio CLI SuperTack bed type is not verified; use a pre-sliced 3MF for SuperTack or choose textured_plate, cool_plate, engineering_plate, or hot_plate.');
1017
1034
  }
1018
- console.log(`3MF has no gcode - auto-slicing with ${slicerType} for ${printModel}`);
1035
+ console.error(`3MF has no gcode - auto-slicing with ${slicerType} for ${printModel}`);
1019
1036
  const autoSliceOptions = {
1020
1037
  uptodate: true,
1021
1038
  ensureOnBed: true,
@@ -1025,7 +1042,7 @@ class BambuPrinterMCPServer {
1025
1042
  nozzleType: resolveNozzleType(args?.nozzle_type),
1026
1043
  };
1027
1044
  threeMFPath = await this.stlManipulator.sliceSTL(threeMFPath, slicerType, slicerPath, slicerProfile || undefined, undefined, printPreset, autoSliceOptions);
1028
- console.log("Auto-sliced to: " + threeMFPath);
1045
+ console.error("Auto-sliced to: " + threeMFPath);
1029
1046
  return { threeMFPath, autoSliced: true };
1030
1047
  }
1031
1048
  async resolveAmsPrintSettings(threeMFPath, args, host, bambuSerial, bambuToken, printModel, printNozzle) {
@@ -3269,7 +3286,7 @@ class BambuPrinterMCPServer {
3269
3286
  if (printBedType === "supertack_plate") {
3270
3287
  throw new Error('BambuStudio CLI SuperTack bed type is not verified; use a pre-sliced 3MF for SuperTack or choose textured_plate, cool_plate, engineering_plate, or hot_plate.');
3271
3288
  }
3272
- console.log(`3MF has no gcode — auto-slicing with ${slicerType} for ${printModel}`);
3289
+ console.error(`3MF has no gcode — auto-slicing with ${slicerType} for ${printModel}`);
3273
3290
  const autoSliceOptions = {
3274
3291
  uptodate: true,
3275
3292
  ensureOnBed: true,
@@ -3291,7 +3308,7 @@ class BambuPrinterMCPServer {
3291
3308
  }
3292
3309
  threeMFPath = await this.stlManipulator.sliceSTL(threeMFPath, slicerType, slicerPath, activeSlicerProfile, undefined, // progressCallback
3293
3310
  printPreset, autoSliceOptions);
3294
- console.log("Auto-sliced to: " + threeMFPath);
3311
+ console.error("Auto-sliced to: " + threeMFPath);
3295
3312
  }
3296
3313
  const parsed3MFData = await parse3MF(threeMFPath);
3297
3314
  const isH2Print = H2_BAMBU_MODELS.has(printModel);
@@ -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";
@@ -504,6 +504,11 @@ export class BambuImplementation {
504
504
  const data = cachedData && Object.keys(cachedData).length > 0
505
505
  ? cachedData
506
506
  : printer.data;
507
+ const ctcTemperature = data.device?.ctc?.info?.temp;
508
+ // CTC packs the current temperature in the low 16 bits and the target in the high 16 bits.
509
+ const chamberTemperature = typeof ctcTemperature === "number" && Number.isInteger(ctcTemperature) && ctcTemperature >= 0 && ctcTemperature <= 0xffffffff
510
+ ? ctcTemperature & 0xffff
511
+ : data.chamber_temper || data.frame_temper || 0;
507
512
  return {
508
513
  status: data.gcode_state || "UNKNOWN",
509
514
  connected: true,
@@ -516,7 +521,7 @@ export class BambuImplementation {
516
521
  actual: data.bed_temper || 0,
517
522
  target: data.bed_target_temper || 0,
518
523
  },
519
- chamber: data.chamber_temper || data.frame_temper || 0,
524
+ chamber: chamberTemperature,
520
525
  },
521
526
  print: {
522
527
  filename: data.subtask_name || data.gcode_file || "None",
@@ -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,11 +360,14 @@ 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');
@@ -408,6 +415,31 @@ export async function inspectPrintFile(filePath, options) {
408
415
  // Official A1 and A1 mini start G-code waits with M109 S<target> H<limit>
409
416
  // (S25 H140, S100 H170, S220 H300). H is not a heater target: S stays the
410
417
  // checked target, so H is accepted only within those vendor bounds.
418
+ if (args.has('O')) {
419
+ // H2C's exact all-hotend preheat offset is relative to the declared
420
+ // first-layer targets. Check every base and derived target, never use
421
+ // editable maxima or an unchecked offset as thermal evidence.
422
+ if (code !== 'M104' || !h2cStartup(index) || args.size !== 2 || args.get('O') !== -80 || !Number.isNaN(args.get('A')))
423
+ fail('unsupported H2C startup offset parameters');
424
+ const targets = consistent(metadataValues(metadata, ['nozzle_temperature_initial_layer']), value => {
425
+ const values = list(value);
426
+ return values.length === materials.length && values.every(v => NUMBER.test(v) && Number.isFinite(Number(v))) ? values.map(Number) : undefined;
427
+ }, 'first-layer nozzle temperature');
428
+ for (let position = 0; position < targets.length; position++) {
429
+ validateTemperature('nozzle', targets[position], model, [materials[position]]);
430
+ const preheat = Math.max(0, targets[position] - 80);
431
+ validateTemperature('nozzle', preheat, model, [materials[position]]);
432
+ nozzleTarget = Math.max(nozzleTarget, preheat);
433
+ maxNozzleTemperature = Math.max(maxNozzleTemperature, preheat);
434
+ }
435
+ continue;
436
+ }
437
+ if (args.has('D')) {
438
+ if (!['M140', 'M190'].includes(code) || !h2cStartup(index) || args.size !== 1)
439
+ fail('unsupported H2C startup bed parameters');
440
+ heat('bed', args.get('D'), undefined, true);
441
+ continue;
442
+ }
411
443
  const vendorWait = code === 'M109' && ['a1', 'a1mini'].includes(model);
412
444
  for (const key of args.keys())
413
445
  if (!'SRTA'.includes(key) && !(key === 'H' && vendorWait))
@@ -430,7 +462,12 @@ export async function inspectPrintFile(filePath, options) {
430
462
  heat(component, args.get(key), position, args.has('T') || args.has('A'), commonFlush);
431
463
  }
432
464
  else if (code === 'M620' || code === 'M621') {
433
- const args = parameters(argumentsText, 'MA');
465
+ const args = parameters(argumentsText, model === 'x2d' ? 'MAB' : model === 'h2c' ? 'MAN' : 'MA');
466
+ if (args.has('N') && (code !== 'M620' || !h2cStartup(index) || args.size !== 1 || !Number.isNaN(args.get('N'))))
467
+ fail('unsupported H2C startup hotend-remap flag');
468
+ // X2D templates use B as a bare material-switch flag, never a target.
469
+ if (args.has('B') && (model !== 'x2d' || !Number.isNaN(args.get('B'))))
470
+ fail('unsupported material-switch B flag');
434
471
  if (args.has('S')) {
435
472
  const position = args.get('S');
436
473
  if ([254, 255, 65279, 65535].includes(position)) {
@@ -454,6 +491,14 @@ export async function inspectPrintFile(filePath, options) {
454
491
  }
455
492
  }
456
493
  }
494
+ else if (code === 'M620.22') {
495
+ // Official X2D runout auto-purge enables the declared project filament.
496
+ // It supplies no heater target; all explicit heat still uses the shared gate.
497
+ const args = parameters(argumentsText);
498
+ if (model !== 'x2d' || args.size !== 2 || !args.has('I') || args.get('P') !== 1)
499
+ fail('unsupported M620.22 runout purge parameters');
500
+ requirePosition(args.get('I'));
501
+ }
457
502
  else if (code === 'M620.1' || code === 'M620.10') {
458
503
  const args = parameters(argumentsText, 'E');
459
504
  const position = code === 'M620.10' && args.get('A') === 1 ? pending ?? active : code === 'M620.10' && args.get('A') === 0 ? active : undefined;
@@ -472,10 +517,27 @@ export async function inspectPrintFile(filePath, options) {
472
517
  else if (/^G150\./.test(code) && /T/i.test(argumentsText)) {
473
518
  fail(`unsupported thermal-affecting wipe command ${code}`);
474
519
  }
520
+ else if (code === 'M620.14') {
521
+ const args = parameters(argumentsText);
522
+ // H2C tower coordinates include the rear wipe area outside object bounds.
523
+ if (!h2cStartup(index) || args.size !== 2 || !['X', 'Y'].every(key => args.has(key) && Number.isFinite(args.get(key)) && args.get(key) >= 0 && args.get(key) <= 350))
524
+ fail('unsupported H2C startup tower parameters');
525
+ }
526
+ else if (model === 'h2c' && ['G383', 'G383.3', 'G383.7'].includes(code) && /U/i.test(argumentsText)) {
527
+ const args = parameters(argumentsText);
528
+ const expected = code === 'G383.7' ? 'UJ' : code === 'G383.3' ? 'UL' : 'OUL';
529
+ if (!h2cStartup(index) || args.size !== expected.length || [...args.keys()].some(key => !expected.includes(key)) || args.get('U') !== 140 ||
530
+ (code === 'G383.7' && args.get('J') !== 0) || (code === 'G383' && args.get('O') !== 2))
531
+ fail('unsupported H2C startup probing parameters');
532
+ const position = args.has('L') ? requirePosition(args.get('L')) : active;
533
+ heat('nozzle', args.get('U'), position);
534
+ }
475
535
  else if (code === 'G383' || code === 'G383.3') {
476
536
  // Bambu probing routines carry nozzle temperature in T and optionally
477
537
  // the project filament position in L (including the H2D G383.3 form).
478
538
  const args = parameters(argumentsText);
539
+ if (args.has('U'))
540
+ fail('unsupported G383 temperature parameter U outside H2C startup');
479
541
  if (code === 'G383.3' && (!args.has('T') || [...args.keys()].some(key => !'TL'.includes(key))))
480
542
  fail('unsupported G383.3 temperature parameters');
481
543
  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) {
@@ -1144,7 +1144,7 @@ export class STLManipulator extends EventEmitter {
1144
1144
  try {
1145
1145
  if (progressCallback)
1146
1146
  progressCallback(0, "Starting base extension operation...");
1147
- console.log(`Extending base of ${stlFilePath} by ${extensionInches} inches`);
1147
+ console.error(`Extending base of ${stlFilePath} by ${extensionInches} inches`);
1148
1148
  // Load the STL file
1149
1149
  const { geometry, boundingBox } = await this.loadSTL(stlFilePath, progressCallback);
1150
1150
  if (!this.activeOperations.get(operationId)) {
@@ -1197,7 +1197,7 @@ export class STLManipulator extends EventEmitter {
1197
1197
  success: true,
1198
1198
  output: outputFilePath
1199
1199
  });
1200
- console.log(`Modified STL saved to ${outputFilePath}`);
1200
+ console.error(`Modified STL saved to ${outputFilePath}`);
1201
1201
  return outputFilePath;
1202
1202
  }
1203
1203
  catch (error) {
@@ -1364,7 +1364,7 @@ export class STLManipulator extends EventEmitter {
1364
1364
  }
1365
1365
  if (progressCallback)
1366
1366
  progressCallback(20, `Executing slicer: ${slicerPath} ${args.join(' ')}`);
1367
- console.log(`Executing: ${slicerPath} ${args.join(' ')}`);
1367
+ console.error(`Executing: ${slicerPath} ${args.join(' ')}`);
1368
1368
  // Execute the slicer
1369
1369
  await new Promise((resolve, reject) => {
1370
1370
  const process = execFile(slicerPath, args, (error, stdout, stderr) => {
@@ -1374,7 +1374,7 @@ export class STLManipulator extends EventEmitter {
1374
1374
  reject(new Error(`Slicer failed: ${error.message}. Stderr: ${stderr}`));
1375
1375
  }
1376
1376
  else {
1377
- console.log(`Slicer Stdout: ${stdout}`);
1377
+ console.error(`Slicer Stdout: ${stdout}`);
1378
1378
  if (stderr) {
1379
1379
  console.warn(`Slicer Stderr: ${stderr}`); // Log stderr even on success
1380
1380
  }
@@ -1382,8 +1382,8 @@ export class STLManipulator extends EventEmitter {
1382
1382
  }
1383
1383
  });
1384
1384
  // Optional: Add listeners for stdout/stderr for real-time progress if slicer provides it
1385
- // process.stdout?.on('data', (data) => { console.log(`Slicer stdout: ${data}`); });
1386
- // process.stderr?.on('data', (data) => { console.log(`Slicer stderr: ${data}`); });
1385
+ // process.stdout?.on('data', (data) => { console.error(`Slicer stdout: ${data}`); });
1386
+ // process.stderr?.on('data', (data) => { console.error(`Slicer stderr: ${data}`); });
1387
1387
  // Check for cancellation periodically
1388
1388
  const checkCancel = setInterval(() => {
1389
1389
  if (!this.activeOperations.get(operationId)) {
@@ -1534,7 +1534,7 @@ export class STLManipulator extends EventEmitter {
1534
1534
  const originalVertexCount = geometry.attributes.position.count;
1535
1535
  const mergedGeometry = BufferGeometryUtils.mergeVertices(geometry, tolerance);
1536
1536
  const newVertexCount = mergedGeometry.attributes.position.count;
1537
- console.log(`Merged vertices: ${originalVertexCount} -> ${newVertexCount} (Tolerance: ${tolerance}mm)`);
1537
+ console.error(`Merged vertices: ${originalVertexCount} -> ${newVertexCount} (Tolerance: ${tolerance}mm)`);
1538
1538
  if (progressCallback)
1539
1539
  progressCallback(70, `Vertices merged: ${originalVertexCount} -> ${newVertexCount}`);
1540
1540
  if (!this.activeOperations.get(operationId))
@@ -1572,7 +1572,7 @@ export class STLManipulator extends EventEmitter {
1572
1572
  if (center.lengthSq() < 0.0001) { // Already centered (or very close)
1573
1573
  if (progressCallback)
1574
1574
  progressCallback(100, "Model is already centered.");
1575
- console.log("Model already centered. No changes made.");
1575
+ console.error("Model already centered. No changes made.");
1576
1576
  this.emit('operationComplete', { operationId, type: 'centerModel', success: true, output: stlFilePath, message: "Model already centered." });
1577
1577
  return stlFilePath; // Return original path
1578
1578
  }
@@ -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.24",
3
+ "version": "1.1.26",
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": {
@@ -46,13 +46,13 @@
46
46
  "author": "David Montgomery",
47
47
  "license": "GPL-2.0",
48
48
  "dependencies": {
49
- "@modelcontextprotocol/sdk": "^1.27.0",
49
+ "@modelcontextprotocol/sdk": "^1.32.1",
50
50
  "bambu-js": "^2.2.2",
51
51
  "bambu-node": "3.22.21",
52
- "basic-ftp": "^5.0.5",
52
+ "basic-ftp": "^6.2.2",
53
53
  "dotenv": "^16.3.1",
54
- "jszip": "^3.10.1",
55
- "mqtt": "^5.15.1",
54
+ "jszip": "^3.10.2",
55
+ "mqtt": "^5.16.0",
56
56
  "patch-package": "8.0.1",
57
57
  "three": "^0.173.0",
58
58
  "xml2js": "^0.6.2"
@@ -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};
package/src/3mf_parser.ts CHANGED
@@ -86,7 +86,7 @@ async function parseBambuConfig(zip: JSZip): Promise<Partial<BambuSlicerConfig>>
86
86
  if (file) {
87
87
  configFile = file;
88
88
  configContent = await file.async('string');
89
- console.log(`Found Bambu config file: ${configFile.name}`);
89
+ console.error(`Found Bambu config file: ${configFile.name}`);
90
90
  break; // Use the first one found (project usually has the most)
91
91
  }
92
92
  }
@@ -96,14 +96,14 @@ async function parseBambuConfig(zip: JSZip): Promise<Partial<BambuSlicerConfig>>
96
96
  // Attempt to parse as JSON
97
97
  const jsonData = JSON.parse(configContent);
98
98
  const parsedConfig = parseBambuJSONConfig(jsonData);
99
- console.log('Bambu config parsed successfully as JSON.');
99
+ console.error('Bambu config parsed successfully as JSON.');
100
100
  return parsedConfig;
101
101
  } catch (jsonError: any) {
102
102
  console.warn(`Failed to parse ${configFile.name} as JSON: ${jsonError.message}. Attempting INI parse as fallback...`);
103
103
  // Fallback: Try parsing as INI if JSON fails (though unlikely based on sample)
104
104
  try {
105
105
  const parsedConfig = parseFallbackINIConfig(configContent); // Keep a simple INI parser as fallback
106
- console.log('Bambu config parsed successfully as INI (fallback).');
106
+ console.error('Bambu config parsed successfully as INI (fallback).');
107
107
  return parsedConfig;
108
108
  } catch (iniError: any) {
109
109
  console.error(`Error parsing Bambu config ${configFile.name} as INI (fallback):`, iniError);
@@ -111,7 +111,7 @@ async function parseBambuConfig(zip: JSZip): Promise<Partial<BambuSlicerConfig>>
111
111
  }
112
112
  }
113
113
  } else {
114
- console.log('No Bambu-specific config file found in Metadata directory.');
114
+ console.error('No Bambu-specific config file found in Metadata directory.');
115
115
  return {};
116
116
  }
117
117
  }
@@ -186,7 +186,7 @@ async function parse3DModelConfig(zip: JSZip): Promise<{ metadata: ThreeMFMetada
186
186
  });
187
187
  }
188
188
 
189
- console.log('3dmodel.model parsed successfully.');
189
+ console.error('3dmodel.model parsed successfully.');
190
190
  return { metadata, objects, build: { items: buildItems } };
191
191
 
192
192
  } catch (error: any) {
@@ -198,14 +198,14 @@ async function parse3DModelConfig(zip: JSZip): Promise<{ metadata: ThreeMFMetada
198
198
  export { ThreeMFData };
199
199
 
200
200
  export async function parse3MF(filePath: string): Promise<ThreeMFData> {
201
- console.log(`Parsing 3MF file: ${filePath}`);
201
+ console.error(`Parsing 3MF file: ${filePath}`);
202
202
  try {
203
203
  const data = await fs.readFile(filePath);
204
204
  const zip = await JSZip.loadAsync(data);
205
205
 
206
206
  // List files for debugging
207
207
  // const fileList = Object.keys(zip.files);
208
- // console.log('Files in 3MF:', fileList);
208
+ // console.error('Files in 3MF:', fileList);
209
209
 
210
210
  const { metadata, objects, build } = await parse3DModelConfig(zip);
211
211
  const bambuConfig = await parseBambuConfig(zip);
@@ -218,7 +218,7 @@ export async function parse3MF(filePath: string): Promise<ThreeMFData> {
218
218
  slicerConfig: bambuConfig
219
219
  };
220
220
 
221
- console.log('3MF parsing completed.');
221
+ console.error('3MF parsing completed.');
222
222
  return combinedData;
223
223
 
224
224
  } catch (error: any) {
@@ -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
@@ -44,6 +44,7 @@ import { BambuImplementation, assertDirectPrintSupported } from "./printers/bamb
44
44
  import { inspectPrintFile } from "./safety/print-file.js";
45
45
  import { validatePrinterState } from "./safety/printer-state.js";
46
46
  import { withPrinterOperation, withPrintSnapshot, uniquePrintName } from "./safety/artifact.js";
47
+ import { detectProfilesRoot } from "./slicer/profile-flatten.js";
47
48
 
48
49
  dotenv.config();
49
50
 
@@ -140,8 +141,6 @@ const BAMBU_MODEL_PRESETS: Record<string, (nozzle: string) => string> = {
140
141
  x2d: (n) => `Bambu Lab X2D ${n} nozzle`,
141
142
  };
142
143
 
143
- const FILAMENT_PROFILE_DIR =
144
- "/Applications/BambuStudio.app/Contents/Resources/profiles/BBL/filament";
145
144
  const FILAMENT_MODEL_CODES: Record<string, string> = {
146
145
  p1s: "P1S",
147
146
  p1p: "P1P",
@@ -212,15 +211,27 @@ let filamentProfileIndexCache: FilamentProfileIndex | null = null;
212
211
  function buildFilamentProfileIndex(): FilamentProfileIndex {
213
212
  const byName = new Map<string, string>();
214
213
  const baseNameByFilamentId = new Map<string, string>();
214
+ let filamentProfileDir: string;
215
+ try {
216
+ const { slicerType, slicerPath } = resolveSlicerConfig(undefined);
217
+ filamentProfileDir = path.join(detectProfilesRoot(slicerPath, slicerType), "BBL", "filament");
218
+ } catch {
219
+ // Profile suggestions are optional; invalid slicer configuration must not hide AMS inventory.
220
+ return { byName, baseNameByFilamentId };
221
+ }
215
222
 
216
- if (!fs.existsSync(FILAMENT_PROFILE_DIR)) {
223
+ let profileEntries: string[];
224
+ try {
225
+ profileEntries = fs.readdirSync(filamentProfileDir);
226
+ } catch {
227
+ // Ignore unavailable profile suggestions while keeping live inventory readable.
217
228
  return { byName, baseNameByFilamentId };
218
229
  }
219
230
 
220
- for (const entry of fs.readdirSync(FILAMENT_PROFILE_DIR)) {
231
+ for (const entry of profileEntries) {
221
232
  if (!entry.endsWith(".json")) continue;
222
233
 
223
- const filePath = path.join(FILAMENT_PROFILE_DIR, entry);
234
+ const filePath = path.join(filamentProfileDir, entry);
224
235
 
225
236
  try {
226
237
  const raw = fs.readFileSync(filePath, "utf8");
@@ -1174,6 +1185,9 @@ class BambuPrinterMCPServer {
1174
1185
  required: ["confirmed"],
1175
1186
  },
1176
1187
  }, { timeout: this.confirmationTimeoutMs });
1188
+ if (response.action === "accept" && response.content?.confirmed !== true) {
1189
+ throw new Error("Hardware confirmation checkbox was not checked. Check it only after inspecting the printer, or cancel. No command was sent.");
1190
+ }
1177
1191
  return response.action === "accept" && response.content?.confirmed === true;
1178
1192
  } catch (error: any) {
1179
1193
  if (isElicitationTimeout(error)) {
@@ -1363,7 +1377,7 @@ class BambuPrinterMCPServer {
1363
1377
  );
1364
1378
  }
1365
1379
 
1366
- console.log(`3MF has no gcode - auto-slicing with ${slicerType} for ${printModel}`);
1380
+ console.error(`3MF has no gcode - auto-slicing with ${slicerType} for ${printModel}`);
1367
1381
  const autoSliceOptions: BambuSliceOptions = {
1368
1382
  uptodate: true,
1369
1383
  ensureOnBed: true,
@@ -1378,7 +1392,7 @@ class BambuPrinterMCPServer {
1378
1392
  printPreset,
1379
1393
  autoSliceOptions
1380
1394
  );
1381
- console.log("Auto-sliced to: " + threeMFPath);
1395
+ console.error("Auto-sliced to: " + threeMFPath);
1382
1396
  return { threeMFPath, autoSliced: true };
1383
1397
  }
1384
1398
 
@@ -3956,7 +3970,7 @@ class BambuPrinterMCPServer {
3956
3970
  'BambuStudio CLI SuperTack bed type is not verified; use a pre-sliced 3MF for SuperTack or choose textured_plate, cool_plate, engineering_plate, or hot_plate.'
3957
3971
  );
3958
3972
  }
3959
- console.log(`3MF has no gcode — auto-slicing with ${slicerType} for ${printModel}`);
3973
+ console.error(`3MF has no gcode — auto-slicing with ${slicerType} for ${printModel}`);
3960
3974
  const autoSliceOptions: BambuSliceOptions = {
3961
3975
  uptodate: true,
3962
3976
  ensureOnBed: true,
@@ -3987,7 +4001,7 @@ class BambuPrinterMCPServer {
3987
4001
  printPreset,
3988
4002
  autoSliceOptions
3989
4003
  );
3990
- console.log("Auto-sliced to: " + threeMFPath);
4004
+ console.error("Auto-sliced to: " + threeMFPath);
3991
4005
  }
3992
4006
 
3993
4007
  const parsed3MFData = await parse3MF(threeMFPath);
@@ -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";
@@ -606,10 +606,16 @@ export class BambuImplementation {
606
606
  }
607
607
 
608
608
  const cachedData = await this.printerStore.waitForInitialReport(host, serial, token);
609
- const data =
609
+ const data: Record<string, any> =
610
610
  cachedData && Object.keys(cachedData).length > 0
611
611
  ? cachedData
612
612
  : printer.data;
613
+ const ctcTemperature = data.device?.ctc?.info?.temp;
614
+ // CTC packs the current temperature in the low 16 bits and the target in the high 16 bits.
615
+ const chamberTemperature =
616
+ typeof ctcTemperature === "number" && Number.isInteger(ctcTemperature) && ctcTemperature >= 0 && ctcTemperature <= 0xffffffff
617
+ ? ctcTemperature & 0xffff
618
+ : data.chamber_temper || data.frame_temper || 0;
613
619
 
614
620
  return {
615
621
  status: data.gcode_state || "UNKNOWN",
@@ -623,7 +629,7 @@ export class BambuImplementation {
623
629
  actual: data.bed_temper || 0,
624
630
  target: data.bed_target_temper || 0,
625
631
  },
626
- chamber: data.chamber_temper || data.frame_temper || 0,
632
+ chamber: chamberTemperature,
627
633
  },
628
634
  print: {
629
635
  filename: data.subtask_name || data.gcode_file || "None",
@@ -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,8 +271,10 @@ 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)) {
@@ -299,6 +305,27 @@ export async function inspectPrintFile(filePath: string, options: {model:string;
299
305
  // Official A1 and A1 mini start G-code waits with M109 S<target> H<limit>
300
306
  // (S25 H140, S100 H170, S220 H300). H is not a heater target: S stays the
301
307
  // checked target, so H is accepted only within those vendor bounds.
308
+ if(args.has('O')) {
309
+ // H2C's exact all-hotend preheat offset is relative to the declared
310
+ // first-layer targets. Check every base and derived target, never use
311
+ // editable maxima or an unchecked offset as thermal evidence.
312
+ if(code!=='M104' || !h2cStartup(index) || args.size!==2 || args.get('O')!==-80 || !Number.isNaN(args.get('A')!)) fail('unsupported H2C startup offset parameters');
313
+ const targets=consistent(metadataValues(metadata,['nozzle_temperature_initial_layer']),value=>{
314
+ const values=list(value);return values.length===materials.length && values.every(v=>NUMBER.test(v) && Number.isFinite(Number(v)))?values.map(Number):undefined;
315
+ },'first-layer nozzle temperature');
316
+ for(let position=0;position<targets.length;position++) {
317
+ validateTemperature('nozzle',targets[position],model,[materials[position]]);
318
+ const preheat=Math.max(0,targets[position]-80);
319
+ validateTemperature('nozzle',preheat,model,[materials[position]]);
320
+ nozzleTarget=Math.max(nozzleTarget,preheat);
321
+ maxNozzleTemperature=Math.max(maxNozzleTemperature,preheat);
322
+ }
323
+ continue;
324
+ }
325
+ if(args.has('D')) {
326
+ if(!['M140','M190'].includes(code) || !h2cStartup(index) || args.size!==1) fail('unsupported H2C startup bed parameters');
327
+ heat('bed',args.get('D')!,undefined,true);continue;
328
+ }
302
329
  const vendorWait=code==='M109' && ['a1','a1mini'].includes(model);
303
330
  for(const key of args.keys()) if(!'SRTA'.includes(key) && !(key==='H' && vendorWait)) fail(`unsupported ${code} temperature parameter ${key}`);
304
331
  if(args.has('H') && (!Number.isFinite(args.get('H')) || args.get('H')!<0 || args.get('H')!>300)) fail(`unsupported ${code} wait parameter H`);
@@ -312,12 +339,21 @@ export async function inspectPrintFile(filePath: string, options: {model:string;
312
339
  if(commonFlush) commonFlushUsed=true;
313
340
  for(const key of ['S','R']) if(args.has(key)) heat(component,args.get(key)!,position,args.has('T') || args.has('A'),commonFlush);
314
341
  } else if(code==='M620' || code==='M621') {
315
- const args=parameters(argumentsText,'MA');
342
+ const args=parameters(argumentsText,model==='x2d'?'MAB':model==='h2c'?'MAN':'MA');
343
+ if(args.has('N') && (code!=='M620' || !h2cStartup(index) || args.size!==1 || !Number.isNaN(args.get('N')!))) fail('unsupported H2C startup hotend-remap flag');
344
+ // X2D templates use B as a bare material-switch flag, never a target.
345
+ if(args.has('B') && (model!=='x2d' || !Number.isNaN(args.get('B')!))) fail('unsupported material-switch B flag');
316
346
  if(args.has('S')) {
317
347
  const position=args.get('S')!;
318
348
  if([254,255,65279,65535].includes(position)) {if(code==='M621') {previous=active;active=undefined;}pending=undefined;}
319
349
  else {selectsAms=true;requirePosition(position);if(code==='M620') pending=position;else {validateTemperature('nozzle',nozzleTarget,model,[materials[position]]);previous=active;active=position;pending=undefined;}}
320
350
  }
351
+ } else if(code==='M620.22') {
352
+ // Official X2D runout auto-purge enables the declared project filament.
353
+ // It supplies no heater target; all explicit heat still uses the shared gate.
354
+ const args=parameters(argumentsText);
355
+ if(model!=='x2d' || args.size!==2 || !args.has('I') || args.get('P')!==1) fail('unsupported M620.22 runout purge parameters');
356
+ requirePosition(args.get('I')!);
321
357
  } else if(code==='M620.1' || code==='M620.10') {
322
358
  const args=parameters(argumentsText,'E');
323
359
  const position=code==='M620.10' && args.get('A')===1 ? pending ?? active : code==='M620.10' && args.get('A')===0 ? active : undefined;
@@ -330,10 +366,22 @@ export async function inspectPrintFile(filePath: string, options: {model:string;
330
366
  if(args.has('T')) heat('nozzle',args.get('T')!,active);
331
367
  } else if(/^G150\./.test(code) && /T/i.test(argumentsText)) {
332
368
  fail(`unsupported thermal-affecting wipe command ${code}`);
369
+ } else if(code==='M620.14') {
370
+ const args=parameters(argumentsText);
371
+ // H2C tower coordinates include the rear wipe area outside object bounds.
372
+ 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');
373
+ } else if(model==='h2c' && ['G383','G383.3','G383.7'].includes(code) && /U/i.test(argumentsText)) {
374
+ const args=parameters(argumentsText);
375
+ const expected=code==='G383.7'?'UJ':code==='G383.3'?'UL':'OUL';
376
+ if(!h2cStartup(index) || args.size!==expected.length || [...args.keys()].some(key=>!expected.includes(key)) || args.get('U')!==140 ||
377
+ (code==='G383.7' && args.get('J')!==0) || (code==='G383' && args.get('O')!==2)) fail('unsupported H2C startup probing parameters');
378
+ const position=args.has('L')?requirePosition(args.get('L')!):active;
379
+ heat('nozzle',args.get('U')!,position);
333
380
  } else if(code==='G383' || code==='G383.3') {
334
381
  // Bambu probing routines carry nozzle temperature in T and optionally
335
382
  // the project filament position in L (including the H2D G383.3 form).
336
383
  const args=parameters(argumentsText);
384
+ if(args.has('U')) fail('unsupported G383 temperature parameter U outside H2C startup');
337
385
  if(code==='G383.3' && (!args.has('T') || [...args.keys()].some(key=>!'TL'.includes(key)))) fail('unsupported G383.3 temperature parameters');
338
386
  const position=args.has('L')?requirePosition(args.get('L')!):active;
339
387
  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
 
@@ -1477,7 +1477,7 @@ export class STLManipulator extends EventEmitter {
1477
1477
 
1478
1478
  try {
1479
1479
  if (progressCallback) progressCallback(0, "Starting base extension operation...");
1480
- console.log(`Extending base of ${stlFilePath} by ${extensionInches} inches`);
1480
+ console.error(`Extending base of ${stlFilePath} by ${extensionInches} inches`);
1481
1481
 
1482
1482
  // Load the STL file
1483
1483
  const { geometry, boundingBox } = await this.loadSTL(stlFilePath, progressCallback);
@@ -1554,7 +1554,7 @@ export class STLManipulator extends EventEmitter {
1554
1554
  output: outputFilePath
1555
1555
  });
1556
1556
 
1557
- console.log(`Modified STL saved to ${outputFilePath}`);
1557
+ console.error(`Modified STL saved to ${outputFilePath}`);
1558
1558
  return outputFilePath;
1559
1559
  } catch (error) {
1560
1560
  console.error("Error extending STL base:", error);
@@ -1733,7 +1733,7 @@ export class STLManipulator extends EventEmitter {
1733
1733
  }
1734
1734
 
1735
1735
  if (progressCallback) progressCallback(20, `Executing slicer: ${slicerPath} ${args.join(' ')}`);
1736
- console.log(`Executing: ${slicerPath} ${args.join(' ')}`);
1736
+ console.error(`Executing: ${slicerPath} ${args.join(' ')}`);
1737
1737
 
1738
1738
  // Execute the slicer
1739
1739
  await new Promise<void>((resolve, reject) => {
@@ -1743,7 +1743,7 @@ export class STLManipulator extends EventEmitter {
1743
1743
  console.error(`Slicer Stderr: ${stderr}`);
1744
1744
  reject(new Error(`Slicer failed: ${error.message}. Stderr: ${stderr}`));
1745
1745
  } else {
1746
- console.log(`Slicer Stdout: ${stdout}`);
1746
+ console.error(`Slicer Stdout: ${stdout}`);
1747
1747
  if (stderr) {
1748
1748
  console.warn(`Slicer Stderr: ${stderr}`); // Log stderr even on success
1749
1749
  }
@@ -1752,8 +1752,8 @@ export class STLManipulator extends EventEmitter {
1752
1752
  });
1753
1753
 
1754
1754
  // Optional: Add listeners for stdout/stderr for real-time progress if slicer provides it
1755
- // process.stdout?.on('data', (data) => { console.log(`Slicer stdout: ${data}`); });
1756
- // process.stderr?.on('data', (data) => { console.log(`Slicer stderr: ${data}`); });
1755
+ // process.stdout?.on('data', (data) => { console.error(`Slicer stdout: ${data}`); });
1756
+ // process.stderr?.on('data', (data) => { console.error(`Slicer stderr: ${data}`); });
1757
1757
 
1758
1758
  // Check for cancellation periodically
1759
1759
  const checkCancel = setInterval(() => {
@@ -1938,7 +1938,7 @@ export class STLManipulator extends EventEmitter {
1938
1938
  const mergedGeometry = BufferGeometryUtils.mergeVertices(geometry, tolerance);
1939
1939
  const newVertexCount = mergedGeometry.attributes.position.count;
1940
1940
 
1941
- console.log(`Merged vertices: ${originalVertexCount} -> ${newVertexCount} (Tolerance: ${tolerance}mm)`);
1941
+ console.error(`Merged vertices: ${originalVertexCount} -> ${newVertexCount} (Tolerance: ${tolerance}mm)`);
1942
1942
  if (progressCallback) progressCallback(70, `Vertices merged: ${originalVertexCount} -> ${newVertexCount}`);
1943
1943
 
1944
1944
  if (!this.activeOperations.get(operationId)) throw new Error("Operation cancelled");
@@ -1982,7 +1982,7 @@ export class STLManipulator extends EventEmitter {
1982
1982
 
1983
1983
  if (center.lengthSq() < 0.0001) { // Already centered (or very close)
1984
1984
  if (progressCallback) progressCallback(100, "Model is already centered.");
1985
- console.log("Model already centered. No changes made.");
1985
+ console.error("Model already centered. No changes made.");
1986
1986
  this.emit('operationComplete', { operationId, type: 'centerModel', success: true, output: stlFilePath, message: "Model already centered." });
1987
1987
  return stlFilePath; // Return original path
1988
1988
  }