bambu-printer-mcp 1.1.31 → 1.1.32

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,7 +6,8 @@ Thank you to everyone who builds, tests, reports problems, and shares real print
6
6
 
7
7
  | Contributor | Contribution |
8
8
  | --- | --- |
9
- | [mygrexit](https://github.com/mygrexit) | Contributes pinned x86-64 Linux X2D native setup, emergency routing and one-shot cleanup in [#56](https://github.com/DMontgomery40/bambu-printer-mcp/pull/56), checked Orca/time-lapse G-code forms in [#57](https://github.com/DMontgomery40/bambu-printer-mcp/pull/57), and model-specific hidden-HMS notices in [#58](https://github.com/DMontgomery40/bambu-printer-mcp/pull/58). Supplies regressions, review corrections, and contributor-reported Debian/X2D read-only, heater-off and print-start evidence in [#55](https://github.com/DMontgomery40/bambu-printer-mcp/issues/55); maintainer validation is offline and mocked. Also reports and fixes hanging Linux X2D stop/heater-off calls in [#61](https://github.com/DMontgomery40/bambu-printer-mcp/pull/61), with never-connecting transport and helper-exit regressions; the release adds protection against expired commands dispatching after reconnection. |
9
+ | [mygrexit](https://github.com/mygrexit) | Contributes pinned x86-64 Linux X2D native setup, emergency routing and one-shot cleanup in [#56](https://github.com/DMontgomery40/bambu-printer-mcp/pull/56), checked Orca/time-lapse G-code forms in [#57](https://github.com/DMontgomery40/bambu-printer-mcp/pull/57), and model-specific hidden-HMS notices in [#58](https://github.com/DMontgomery40/bambu-printer-mcp/pull/58). Supplies regressions, review corrections, and contributor-reported Debian/X2D read-only, heater-off and print-start evidence in [#55](https://github.com/DMontgomery40/bambu-printer-mcp/issues/55); maintainer validation is offline and mocked. Also reports and fixes hanging Linux X2D stop/heater-off calls in [#61](https://github.com/DMontgomery40/bambu-printer-mcp/pull/61), with never-connecting transport and helper-exit regressions; the release adds protection against expired commands dispatching after reconnection. In [#64](https://github.com/DMontgomery40/bambu-printer-mcp/pull/64), contributes ready-card native job storage, fresh dispatch rechecks and tests, and reports Linux X2D firmware 01.01.01.00 printing to external storage with plug-in 02.08.02.54 and a Bambuddy thumbnail; maintainer verification uses source and mocked transports. |
10
+ | [maziggy](https://github.com/maziggy) | Proposes choosing ready external storage by default in [Bambu Studio #12146](https://github.com/bambulab/BambuStudio/pull/12146), a related upstream proposal supporting the storage choice in [#64](https://github.com/DMontgomery40/bambu-printer-mcp/pull/64). The upstream proposal remains unmerged; this credit does not claim it shipped in Bambu Studio. |
10
11
  | [patrikmadoran-cyber](https://github.com/patrikmadoran-cyber) | Adds explicit Standard/High Flow nozzle selection across slicing and print auto-slicing in [#54](https://github.com/DMontgomery40/bambu-printer-mcp/pull/54), with unsupported-variant regressions and real Windows BambuStudio 02.06.00.51 H2S 0.6 slicing evidence. No physical print is claimed. |
11
12
  | [Patrick Juchli (@patrickjuchli)](https://github.com/patrickjuchli) | Maintains basic-ftp and supplies the 6.2.3 fixes for rejected-command recovery, asynchronous credential-error crashes, and password redaction used by direct FTPS transfers. |
12
13
  | [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. |
package/README.md CHANGED
@@ -196,7 +196,7 @@ The optional FULU **BambuNetwork bridge** exposes `bambu_network_bridge_status`,
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).
198
198
  - Slice through BambuStudio CLI with automatic BBL inheritance/include resolution, per-slot filament colours, and fallback prime-tower placement for multi-nozzle printers. Missing dependencies stop the slice; custom settings and saved project tower positions are preserved. Multi-colour slicing is verified by the contributor on BambuStudio 02.08.02.60 for Windows; older CLI versions have separate limitations. See [slicing guide](https://github.com/DMontgomery40/bambu-printer-mcp/blob/main/docs/SLICING.md).
199
- - Recognize X2D status and slice with its own installed BambuStudio preset (`BAMBU_MODEL=x2d`). On macOS and x86-64 Linux, `print_3mf` selects the native eMMC route after resolving the model, including an elicited model. Install the optional helper as described below; Windows native print requests, and Linux requests without the helper, fail before slicing or contacting the printer. Legacy FTPS and remote G-code starts remain unsupported for X2D.
199
+ - Recognize X2D status and slice with its own installed BambuStudio preset (`BAMBU_MODEL=x2d`). On macOS and x86-64 Linux, `print_3mf` selects the native route after resolving the model, including an elicited model. Install the optional helper as described below; Windows native print requests, and Linux requests without the helper, fail before slicing or contacting the printer. Legacy FTPS and remote G-code starts remain unsupported for X2D.
200
200
  - Parse AMS mapping from the 3MF's embedded slicer metadata (`Metadata/plate_<n>.json` + gcode filament header) and send it correctly formatted per the OpenBambuAPI spec, with correct H2S/H2D/H2C `ams_mapping2` parallel array format
201
201
  - **Auto-match AMS slots by RFID** (`auto_match_ams` flag on `print_3mf`). Resolves required `tray_info_idx` from the sliced 3MF against live AMS inventory. Handles same-SKU different-color filaments by matching on `(tray_info_idx, tray_color)` and tracking already-claimed slots. Dry-run with `resolve_3mf_ams_slots` before printing.
202
202
  - Cancel, pause, and resume in-progress print jobs via MQTT
@@ -906,7 +906,7 @@ The primary tool for starting a Bambu print. **Recommended input: a pre-sliced `
906
906
  2. If no G-code is found, attempts to auto-slice via the configured slicer. Profile preparation or slicing failures stop the operation before upload. See the slicing guide for tested CLI versions and combinations.
907
907
  3. Parses the sliced 3MF to extract the correct plate file and compute its MD5 hash.
908
908
  4. Reads slicer metadata and any explicit AMS selection to build the filament mapping.
909
- 5. Uploads via `basic-ftp` to the model-specific location: SD root for H2/full-size A1, `cache/` for P1/X1/A1 mini/P2S. X2D `print_3mf` instead uses its checked native eMMC route on macOS or Linux.
909
+ 5. Uploads via `basic-ftp` to the model-specific location: SD root for H2/full-size A1, `cache/` for P1/X1/A1 mini/P2S. X2D `print_3mf` instead uses its checked native route on macOS or Linux, which stores the job on a ready SD card or USB stick and otherwise on internal storage (eMMC).
910
910
  6. Sends the correct MQTT print command for the target printer family. For H2S/H2D/H2C that means `project_file` with project-length `ams_mapping`, parallel `ams_mapping2`, and H2-compatible calibration flags.
911
911
 
912
912
  ```json
@@ -966,7 +966,7 @@ Use the same package version as the server, for example `bambu-printer-mcp@1.2.3
966
966
 
967
967
  `--check-update` asks Bambu's plug-in service for the newest Linux plug-in and compares the `PrintParams` layout in the matching Bambu Studio header with the helper. It reports only: the setup never installs an unpinned plug-in, because a changed layout would send wrong print parameters. A pin is raised by a package release after the check passes and a hardware test.
968
968
 
969
- Once the helper, plug-in and certificates are installed, X2D printing, upload and controls use the same routing, safety checks and authorization handshake described for macOS above. On Linux, stop and heater-off go out over the native route and MQTT at the same time, so a stuck helper cannot delay them. MQTT is sent again once every older print or heating helper for that printer has exited and the immediate MQTT attempt has settled, so none can dispatch after it; either the native route or that later MQTT send succeeding counts, and the result lists all sends under `routes`. Each MQTT send gives up after 15 seconds (`BAMBU_EMERGENCY_MQTT_TIMEOUT_MS`), so an unreachable MQTT connection cannot hold back a stop the native route already sent. A connection completing after that deadline cannot dispatch its expired command; an already queued MQTT publish is discarded by force-closing and invalidating that client. A repeated stop or heater-off is harmless. macOS keeps its native-only route. The server accepts only the pinned plug-in checksums; `BAMBU_NATIVE_ALLOW_UNPINNED_PLUGIN=1` is a trusted override for testing another build at your own risk. Without a complete installation, Linux keeps its previous behavior: X2D status and slicing work, stop, pause and heater-off use MQTT, and native print requests name the setup command before contacting the printer. Bambu publishes the Linux plug-in for x86-64 only; other architectures keep the MQTT behavior.
969
+ Once the helper, plug-in and certificates are installed, X2D printing, upload and controls use the same routing, safety checks and authorization handshake described for macOS above. On Linux, stop and heater-off go out over the native route and MQTT at the same time, so a stuck helper cannot delay them. MQTT is sent again once every older print or heating helper for that printer has exited and the immediate MQTT attempt has settled, so none can dispatch after it; either the native route or that later MQTT send succeeding counts, and the result lists all sends under `routes`. Each MQTT send gives up after 15 seconds (`BAMBU_EMERGENCY_MQTT_TIMEOUT_MS`), so an unreachable MQTT connection cannot hold back a stop the native route already sent. A connection completing after that deadline cannot dispatch its expired command; an already queued MQTT publish is discarded by force-closing and invalidating that client. A repeated stop or heater-off is harmless. macOS keeps its native-only route. On both systems a print goes to the printer's SD card or USB stick when the printer reports one ready, so it can be reprinted from the printer and archived by tools that read the card; otherwise it goes to internal storage (eMMC), as Bambu Studio's Print button always does. `BAMBU_NATIVE_TRY_EMMC_PRINT=1` (or `0`) forces the choice. The server accepts only the pinned plug-in checksums; `BAMBU_NATIVE_ALLOW_UNPINNED_PLUGIN=1` is a trusted override for testing another build at your own risk. Without a complete installation, Linux keeps its previous behavior: X2D status and slicing work, stop, pause and heater-off use MQTT, and native print requests name the setup command before contacting the printer. Bambu publishes the Linux plug-in for x86-64 only; other architectures keep the MQTT behavior.
970
970
 
971
971
  #### x2d_native_control
972
972
 
@@ -19,6 +19,8 @@ export type BambuNativePrintOptions = {
19
19
  vibrationCalibration?: boolean;
20
20
  layerInspect?: boolean;
21
21
  timelapse?: boolean;
22
+ /** true: the SD card or USB stick; false: internal storage (eMMC); unset: BAMBU_NATIVE_TRY_EMMC_PRINT or internal. Reported as the requested storage. */
23
+ externalStorage?: boolean;
22
24
  };
23
25
  export type BambuNativeControlOptions = {
24
26
  host: string;
@@ -748,6 +748,7 @@ export async function printWithBambuNative(options, onUpdate, execution) {
748
748
  BAMBU_NATIVE_VIBRATION_CALIBRATION: boolEnv(options.vibrationCalibration, true),
749
749
  BAMBU_NATIVE_LAYER_INSPECT: boolEnv(options.layerInspect, false),
750
750
  BAMBU_NATIVE_TIMELAPSE: boolEnv(options.timelapse, false),
751
+ ...(options.externalStorage === undefined ? {} : { BAMBU_NATIVE_TRY_EMMC_PRINT: String(!options.externalStorage) }),
751
752
  }, 300000, onUpdate, execution);
752
753
  if (result.resultCode !== 0) {
753
754
  const explicitRejection = result.updates.some(line => /^native_print result=-?[1-9]\d*$/.test(line));
@@ -765,6 +766,7 @@ export async function printWithBambuNative(options, onUpdate, execution) {
765
766
  return {
766
767
  status: "success",
767
768
  route: "bambu:///local",
769
+ ...(options.externalStorage === undefined ? {} : { storage: options.externalStorage ? "external" : "internal" }),
768
770
  updates: result.updates,
769
771
  };
770
772
  }
package/dist/index.js CHANGED
@@ -18,7 +18,7 @@ import { bridgeAmsMapping2Entry, bridgeAmsMappingValue, externalTrayForNozzle, h
18
18
  import { analyze3MFAmsRequirements, analyze3MFPlateObjects, analyzeCollarCharm3MF, extractBambuTemplateSettings, getCollarCharmRolePolicy, parse3MF } from './3mf_parser.js';
19
19
  import { BambuImplementation, assertDirectPrintSupported } from "./printers/bambu.js";
20
20
  import { inspectPrintFile } from "./safety/print-file.js";
21
- import { validatePrinterState } from "./safety/printer-state.js";
21
+ import { externalStorageState, validatePrinterState } from "./safety/printer-state.js";
22
22
  import { isDualNozzleModel } from "./safety/limits.js";
23
23
  import { withPrinterOperation, withPrintSnapshot, uniquePrintName } from "./safety/artifact.js";
24
24
  import { detectProfilesRoot } from "./slicer/profile-flatten.js";
@@ -67,6 +67,21 @@ function withEmergencyMqttTimeout(send) {
67
67
  });
68
68
  return Promise.race([Promise.resolve().then(() => send(controller.signal)), timeout]).finally(() => clearTimeout(timer));
69
69
  }
70
+ /**
71
+ * Whether an X2D native print stores its job on the SD card or USB stick
72
+ * (true) or internal storage (eMMC). An explicit try_emmc_print wins, then
73
+ * BAMBU_NATIVE_TRY_EMMC_PRINT read as the helper reads it; otherwise a ready
74
+ * card is used, as OrcaSlicer does, so the job can be reprinted from the
75
+ * printer and read over FTPS.
76
+ */
77
+ function nativeExternalStorage(tryEmmcPrint, raw) {
78
+ if (tryEmmcPrint !== undefined)
79
+ return !tryEmmcPrint;
80
+ const configured = process.env.BAMBU_NATIVE_TRY_EMMC_PRINT?.trim();
81
+ if (configured)
82
+ return !["1", "true", "TRUE", "yes"].includes(configured);
83
+ return externalStorageState(raw) === "normal";
84
+ }
70
85
  function requestedPrintNozzles(args) {
71
86
  if (args.nozzle_diameters !== undefined && args.nozzle_diameter !== undefined) {
72
87
  throw new Error("Supply nozzle_diameters or nozzle_diameter, not both.");
@@ -1136,6 +1151,9 @@ class BambuPrinterMCPServer {
1136
1151
  if (!args?.three_mf_path) {
1137
1152
  throw new Error("Missing required parameter: three_mf_path");
1138
1153
  }
1154
+ if (args?.try_emmc_print !== undefined && typeof args.try_emmc_print !== "boolean") {
1155
+ throw new Error("try_emmc_print must be true or false.");
1156
+ }
1139
1157
  const printModel = await this.resolveBambuModel(args?.bambu_model);
1140
1158
  const printBedType = resolveBedType(args?.bed_type);
1141
1159
  const printNozzles = requestedPrintNozzles(args);
@@ -1259,6 +1277,10 @@ class BambuPrinterMCPServer {
1259
1277
  };
1260
1278
  assertActive();
1261
1279
  if (native) {
1280
+ const externalStorage = nativeExternalStorage(args?.try_emmc_print, confirmedStatus.raw);
1281
+ if (externalStorage && externalStorageState(confirmedStatus.raw) !== "normal") {
1282
+ throw new Error("External storage was requested, but the printer reports no ready SD card or USB stick.");
1283
+ }
1262
1284
  // Release the telemetry client before the installed plug-in opens its session.
1263
1285
  await this.bambu.disconnectAll();
1264
1286
  assertActive();
@@ -1272,11 +1294,15 @@ class BambuPrinterMCPServer {
1272
1294
  vibrationCalibration: params.task_vibration_cali,
1273
1295
  layerInspect: params.task_layer_inspect,
1274
1296
  timelapse: params.task_record_timelapse,
1297
+ externalStorage,
1275
1298
  }, native.onUpdate, { signal: native.signal, assertActive, beforeDispatch: async () => {
1276
1299
  assertActive();
1277
1300
  const dispatchStatus = await this.bambu.getSafetyStatus(devIp, devId, password || bambuToken);
1278
1301
  validatePrinterState(dispatchStatus, requirements);
1279
1302
  this.bambu.assertBedClearance(dispatchStatus, bedClearance);
1303
+ if (externalStorage && externalStorageState(dispatchStatus.raw) !== "normal") {
1304
+ throw new Error("The printer's SD card or USB stick is no longer ready. Check it and start the print again.");
1305
+ }
1280
1306
  assertActive();
1281
1307
  } });
1282
1308
  assertActive();
@@ -2082,7 +2108,7 @@ class BambuPrinterMCPServer {
2082
2108
  use_ssl_for_ftp: { type: "boolean", description: "Whether FULU local print should use SSL for FTP (default: true)." },
2083
2109
  use_ssl_for_mqtt: { type: "boolean", description: "Whether FULU local print should use SSL for MQTT (default: true)." },
2084
2110
  external_change_assist: { type: "boolean", description: "Enable FULU PrintParams task_ext_change_assist for external filament change assistance." },
2085
- try_emmc_print: { type: "boolean", description: "Enable FULU PrintParams try_emmc_print for printers that support internal storage printing." },
2111
+ try_emmc_print: { type: "boolean", description: "Store the job on internal storage (eMMC) instead of the SD card or USB stick. FULU bridge: default false. X2D native printing: omit to use a ready card, else eMMC." },
2086
2112
  extra_options: { type: "string", description: "Raw JSON string or text for FULU PrintParams extra_options." },
2087
2113
  client_job_id: { type: "number", description: "Optional client job id sent to the bridge; defaults to the current timestamp." }
2088
2114
  },
@@ -2458,8 +2484,9 @@ class BambuPrinterMCPServer {
2458
2484
  connection_mode: {
2459
2485
  type: "string",
2460
2486
  enum: ["lan_mqtt_ftps", "bambu_network", "bambu_native", "bambu_connect"],
2461
- description: "Print path: bambu_connect hands a printable file to the signed-in Bambu Connect app for cloud-mode review; X2D otherwise defaults to bambu_native (Bambu Studio's installed local networking plug-in and emmc tunnel); lan_mqtt_ftps is the legacy direct MQTT/FTPS path; bambu_network uses the FULU bridge."
2487
+ description: "Print path: bambu_connect hands a printable file to the signed-in Bambu Connect app for cloud-mode review; X2D otherwise defaults to bambu_native (Bambu Studio's installed local networking plug-in, storing the job on a ready SD card or USB stick, otherwise internal eMMC); lan_mqtt_ftps is the legacy direct MQTT/FTPS path; bambu_network uses the FULU bridge."
2462
2488
  },
2489
+ try_emmc_print: { type: "boolean", description: "X2D native printing: true stores the job on internal storage (eMMC), false on the SD card or USB stick (refused unless the printer reports one ready). Omit to use the card when ready, else eMMC; BAMBU_NATIVE_TRY_EMMC_PRINT sets a server default." },
2463
2490
  connection_type: { type: "string", enum: ["cloud", "lan"], description: "BambuNetwork connection type when connection_mode is bambu_network; cloud uses restored internet printing, lan uses local bridge printing." },
2464
2491
  bambu_network_method: { type: "string", enum: BAMBU_NETWORK_PRINT_METHODS, description: "FULU print method when connection_mode is bambu_network; defaults to start_print for cloud and start_local_print for lan." },
2465
2492
  dev_id: { type: "string", description: "Bambu device id for FULU BambuNetwork printing; defaults to BAMBU_DEV_ID or BAMBU_SERIAL." },
@@ -51,4 +51,12 @@ export declare function hiddenHmsKeys(raw: unknown, serial: string, model: strin
51
51
  export declare function validatePrinterState(status: any, requirements: PrinterStateRequirements): ValidatedPrinterState;
52
52
  /** Manual M104 cannot remap filament slots. Validate the material already at the nozzle. */
53
53
  export declare function manualHeatingRequirements(status: any, model: string, nozzleDiameter: number, material: string): PrinterStateRequirements;
54
+ export type ExternalStorageState = "none" | "normal" | "abnormal" | "readonly";
55
+ /**
56
+ * The SD card or USB stick state, read as Bambu Studio does: the `sdcard`
57
+ * flag, then `home_flag` bits 8-9, then on firmware that reports cfg/fun/aux/stat
58
+ * the `aux` bits 12-13, each later source overriding the earlier one
59
+ * (DevStorage.cpp, DeviceManager.cpp parse_home_flag and parse_new_info).
60
+ */
61
+ export declare function externalStorageState(raw: unknown): ExternalStorageState;
54
62
  export {};
@@ -451,3 +451,21 @@ export function manualHeatingRequirements(status, model, nozzleDiameter, materia
451
451
  }
452
452
  return requirements;
453
453
  }
454
+ const EXTERNAL_STORAGE_STATES = ["none", "normal", "abnormal", "readonly"];
455
+ /**
456
+ * The SD card or USB stick state, read as Bambu Studio does: the `sdcard`
457
+ * flag, then `home_flag` bits 8-9, then on firmware that reports cfg/fun/aux/stat
458
+ * the `aux` bits 12-13, each later source overriding the earlier one
459
+ * (DevStorage.cpp, DeviceManager.cpp parse_home_flag and parse_new_info).
460
+ */
461
+ export function externalStorageState(raw) {
462
+ if (!object(raw))
463
+ return "none";
464
+ let state = raw.sdcard === true ? "normal" : "none";
465
+ if (Number.isInteger(raw.home_flag))
466
+ state = EXTERNAL_STORAGE_STATES[(raw.home_flag >> 8) & 3];
467
+ if (["cfg", "fun", "aux", "stat"].every(key => key in raw) && typeof raw.aux === "string" && /^[0-9a-f]+$/i.test(raw.aux)) {
468
+ state = EXTERNAL_STORAGE_STATES[Number((BigInt(`0x${raw.aux}`) >> 12n) & 3n)];
469
+ }
470
+ return state;
471
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bambu-printer-mcp",
3
- "version": "1.1.31",
3
+ "version": "1.1.32",
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": {
@@ -26,6 +26,8 @@ export type BambuNativePrintOptions = {
26
26
  vibrationCalibration?: boolean;
27
27
  layerInspect?: boolean;
28
28
  timelapse?: boolean;
29
+ /** true: the SD card or USB stick; false: internal storage (eMMC); unset: BAMBU_NATIVE_TRY_EMMC_PRINT or internal. Reported as the requested storage. */
30
+ externalStorage?: boolean;
29
31
  };
30
32
 
31
33
  export type BambuNativeControlOptions = {
@@ -864,6 +866,7 @@ export async function printWithBambuNative(
864
866
  BAMBU_NATIVE_VIBRATION_CALIBRATION: boolEnv(options.vibrationCalibration, true),
865
867
  BAMBU_NATIVE_LAYER_INSPECT: boolEnv(options.layerInspect, false),
866
868
  BAMBU_NATIVE_TIMELAPSE: boolEnv(options.timelapse, false),
869
+ ...(options.externalStorage === undefined ? {} : { BAMBU_NATIVE_TRY_EMMC_PRINT: String(!options.externalStorage) }),
867
870
  },
868
871
  300_000,
869
872
  onUpdate,
@@ -885,6 +888,7 @@ export async function printWithBambuNative(
885
888
  return {
886
889
  status: "success",
887
890
  route: "bambu:///local",
891
+ ...(options.externalStorage === undefined ? {} : { storage: options.externalStorage ? "external" : "internal" }),
888
892
  updates: result.updates,
889
893
  };
890
894
  }
package/src/index.ts CHANGED
@@ -45,7 +45,7 @@ import { analyze3MFAmsRequirements, analyze3MFPlateObjects, analyzeCollarCharm3M
45
45
  import type { ThreeMFAmsRequirements } from "./types.js";
46
46
  import { BambuImplementation, assertDirectPrintSupported } from "./printers/bambu.js";
47
47
  import { inspectPrintFile } from "./safety/print-file.js";
48
- import { validatePrinterState } from "./safety/printer-state.js";
48
+ import { externalStorageState, validatePrinterState } from "./safety/printer-state.js";
49
49
  import { isDualNozzleModel } from "./safety/limits.js";
50
50
  import { withPrinterOperation, withPrintSnapshot, uniquePrintName } from "./safety/artifact.js";
51
51
  import { detectProfilesRoot } from "./slicer/profile-flatten.js";
@@ -101,6 +101,20 @@ function withEmergencyMqttTimeout<T>(send: (signal: AbortSignal) => Promise<T>):
101
101
  return Promise.race([Promise.resolve().then(() => send(controller.signal)), timeout]).finally(() => clearTimeout(timer));
102
102
  }
103
103
 
104
+ /**
105
+ * Whether an X2D native print stores its job on the SD card or USB stick
106
+ * (true) or internal storage (eMMC). An explicit try_emmc_print wins, then
107
+ * BAMBU_NATIVE_TRY_EMMC_PRINT read as the helper reads it; otherwise a ready
108
+ * card is used, as OrcaSlicer does, so the job can be reprinted from the
109
+ * printer and read over FTPS.
110
+ */
111
+ function nativeExternalStorage(tryEmmcPrint: boolean | undefined, raw: unknown): boolean {
112
+ if (tryEmmcPrint !== undefined) return !tryEmmcPrint;
113
+ const configured = process.env.BAMBU_NATIVE_TRY_EMMC_PRINT?.trim();
114
+ if (configured) return !["1", "true", "TRUE", "yes"].includes(configured);
115
+ return externalStorageState(raw) === "normal";
116
+ }
117
+
104
118
  function requestedPrintNozzles(args: Record<string, any>): number[] | undefined {
105
119
  if (args.nozzle_diameters !== undefined && args.nozzle_diameter !== undefined) {
106
120
  throw new Error("Supply nozzle_diameters or nozzle_diameter, not both.");
@@ -1527,6 +1541,9 @@ class BambuPrinterMCPServer {
1527
1541
  if (!args?.three_mf_path) {
1528
1542
  throw new Error("Missing required parameter: three_mf_path");
1529
1543
  }
1544
+ if (args?.try_emmc_print !== undefined && typeof args.try_emmc_print !== "boolean") {
1545
+ throw new Error("try_emmc_print must be true or false.");
1546
+ }
1530
1547
 
1531
1548
  const printModel = await this.resolveBambuModel(args?.bambu_model as string | undefined);
1532
1549
  const printBedType = resolveBedType(args?.bed_type as string | undefined);
@@ -1674,6 +1691,10 @@ class BambuPrinterMCPServer {
1674
1691
 
1675
1692
  assertActive();
1676
1693
  if (native) {
1694
+ const externalStorage = nativeExternalStorage(args?.try_emmc_print, confirmedStatus.raw);
1695
+ if (externalStorage && externalStorageState(confirmedStatus.raw) !== "normal") {
1696
+ throw new Error("External storage was requested, but the printer reports no ready SD card or USB stick.");
1697
+ }
1677
1698
  // Release the telemetry client before the installed plug-in opens its session.
1678
1699
  await this.bambu.disconnectAll();
1679
1700
  assertActive();
@@ -1687,11 +1708,15 @@ class BambuPrinterMCPServer {
1687
1708
  vibrationCalibration: params.task_vibration_cali as boolean,
1688
1709
  layerInspect: params.task_layer_inspect as boolean,
1689
1710
  timelapse: params.task_record_timelapse as boolean,
1711
+ externalStorage,
1690
1712
  }, native.onUpdate, { signal: native.signal, assertActive, beforeDispatch: async () => {
1691
1713
  assertActive();
1692
1714
  const dispatchStatus = await this.bambu.getSafetyStatus(devIp, devId, password || bambuToken);
1693
1715
  validatePrinterState(dispatchStatus, requirements);
1694
1716
  this.bambu.assertBedClearance(dispatchStatus, bedClearance);
1717
+ if (externalStorage && externalStorageState(dispatchStatus.raw) !== "normal") {
1718
+ throw new Error("The printer's SD card or USB stick is no longer ready. Check it and start the print again.");
1719
+ }
1695
1720
  assertActive();
1696
1721
  } });
1697
1722
  assertActive();
@@ -2611,7 +2636,7 @@ class BambuPrinterMCPServer {
2611
2636
  use_ssl_for_ftp: { type: "boolean", description: "Whether FULU local print should use SSL for FTP (default: true)." },
2612
2637
  use_ssl_for_mqtt: { type: "boolean", description: "Whether FULU local print should use SSL for MQTT (default: true)." },
2613
2638
  external_change_assist: { type: "boolean", description: "Enable FULU PrintParams task_ext_change_assist for external filament change assistance." },
2614
- try_emmc_print: { type: "boolean", description: "Enable FULU PrintParams try_emmc_print for printers that support internal storage printing." },
2639
+ try_emmc_print: { type: "boolean", description: "Store the job on internal storage (eMMC) instead of the SD card or USB stick. FULU bridge: default false. X2D native printing: omit to use a ready card, else eMMC." },
2615
2640
  extra_options: { type: "string", description: "Raw JSON string or text for FULU PrintParams extra_options." },
2616
2641
  client_job_id: { type: "number", description: "Optional client job id sent to the bridge; defaults to the current timestamp." }
2617
2642
  },
@@ -2987,8 +3012,9 @@ class BambuPrinterMCPServer {
2987
3012
  connection_mode: {
2988
3013
  type: "string",
2989
3014
  enum: ["lan_mqtt_ftps", "bambu_network", "bambu_native", "bambu_connect"],
2990
- description: "Print path: bambu_connect hands a printable file to the signed-in Bambu Connect app for cloud-mode review; X2D otherwise defaults to bambu_native (Bambu Studio's installed local networking plug-in and emmc tunnel); lan_mqtt_ftps is the legacy direct MQTT/FTPS path; bambu_network uses the FULU bridge."
3015
+ description: "Print path: bambu_connect hands a printable file to the signed-in Bambu Connect app for cloud-mode review; X2D otherwise defaults to bambu_native (Bambu Studio's installed local networking plug-in, storing the job on a ready SD card or USB stick, otherwise internal eMMC); lan_mqtt_ftps is the legacy direct MQTT/FTPS path; bambu_network uses the FULU bridge."
2991
3016
  },
3017
+ try_emmc_print: { type: "boolean", description: "X2D native printing: true stores the job on internal storage (eMMC), false on the SD card or USB stick (refused unless the printer reports one ready). Omit to use the card when ready, else eMMC; BAMBU_NATIVE_TRY_EMMC_PRINT sets a server default." },
2992
3018
  connection_type: { type: "string", enum: ["cloud", "lan"], description: "BambuNetwork connection type when connection_mode is bambu_network; cloud uses restored internet printing, lan uses local bridge printing." },
2993
3019
  bambu_network_method: { type: "string", enum: BAMBU_NETWORK_PRINT_METHODS, description: "FULU print method when connection_mode is bambu_network; defaults to start_print for cloud and start_local_print for lan." },
2994
3020
  dev_id: { type: "string", description: "Bambu device id for FULU BambuNetwork printing; defaults to BAMBU_DEV_ID or BAMBU_SERIAL." },
@@ -424,3 +424,22 @@ export function manualHeatingRequirements(status: any, model: string, nozzleDiam
424
424
  if (tray < 254) { requirements.useAMS = true; requirements.amsMapping = [tray]; }
425
425
  return requirements;
426
426
  }
427
+
428
+ export type ExternalStorageState = "none" | "normal" | "abnormal" | "readonly";
429
+ const EXTERNAL_STORAGE_STATES: readonly ExternalStorageState[] = ["none", "normal", "abnormal", "readonly"];
430
+
431
+ /**
432
+ * The SD card or USB stick state, read as Bambu Studio does: the `sdcard`
433
+ * flag, then `home_flag` bits 8-9, then on firmware that reports cfg/fun/aux/stat
434
+ * the `aux` bits 12-13, each later source overriding the earlier one
435
+ * (DevStorage.cpp, DeviceManager.cpp parse_home_flag and parse_new_info).
436
+ */
437
+ export function externalStorageState(raw: unknown): ExternalStorageState {
438
+ if (!object(raw)) return "none";
439
+ let state: ExternalStorageState = raw.sdcard === true ? "normal" : "none";
440
+ if (Number.isInteger(raw.home_flag)) state = EXTERNAL_STORAGE_STATES[(raw.home_flag >> 8) & 3];
441
+ if (["cfg", "fun", "aux", "stat"].every(key => key in raw) && typeof raw.aux === "string" && /^[0-9a-f]+$/i.test(raw.aux)) {
442
+ state = EXTERNAL_STORAGE_STATES[Number((BigInt(`0x${raw.aux}`) >> 12n) & 3n)];
443
+ }
444
+ return state;
445
+ }