bambu-printer-mcp 1.1.6 → 1.1.9

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.
@@ -47,6 +47,7 @@ const MODEL_ID_TO_NAME: Record<string, string> = {
47
47
  O1D: "H2D",
48
48
  O1E: "H2D Pro",
49
49
  O1S: "H2S",
50
+ N6: "X2D",
50
51
  N2S: "A1",
51
52
  A1M: "A1 Mini",
52
53
  C11: "P1P",
@@ -58,10 +59,23 @@ const MODEL_ID_TO_NAME: Record<string, string> = {
58
59
 
59
60
  const H2_MODEL_NAMES = new Set(["h2", "h2c", "h2d", "h2dpro", "h2d pro", "h2s"]);
60
61
 
62
+ export function assertDirectPrintSupported(model: string | undefined, serial?: string): void {
63
+ if (model?.trim().toLowerCase() === "x2d" || serial?.trim().toUpperCase().startsWith("20P")) {
64
+ throw new Error(
65
+ "X2D direct printing is not supported pending native eMMC transport support. " +
66
+ "X2D status and slicing remain available; use a supported slicer to print."
67
+ );
68
+ }
69
+ }
70
+
61
71
  function isH2ModelName(model: unknown): boolean {
62
72
  return H2_MODEL_NAMES.has(String(model ?? "").trim().toLowerCase().replace(/\s+/g, " "));
63
73
  }
64
74
 
75
+ function isP2SModelName(model: unknown): boolean {
76
+ return String(model ?? "").trim().toLowerCase() === "p2s";
77
+ }
78
+
65
79
  interface BambuPrintOptionsInternal {
66
80
  projectName: string;
67
81
  filePath: string;
@@ -145,11 +159,15 @@ class TolerantBambuClient extends BambuClient {
145
159
  const sn = this.config.serialNumber;
146
160
  if (sn.startsWith("093")) return "H2S";
147
161
  if (sn.startsWith("094")) return "H2D";
162
+ if (sn.startsWith("239")) return "H2C";
163
+ if (sn.startsWith("31B")) return "H2DPRO";
164
+ if (sn.startsWith("20P")) return "X2D";
148
165
  if (sn.startsWith("00M")) return "X1C";
149
166
  if (sn.startsWith("00W")) return "X1";
150
167
  if (sn.startsWith("03W")) return "X1E";
151
168
  if (sn.startsWith("01S")) return "P1P";
152
169
  if (sn.startsWith("01P")) return "P1S";
170
+ if (sn.startsWith("22E")) return "P2S";
153
171
  if (sn.startsWith("030")) return "A1";
154
172
  if (sn.startsWith("039")) return "A1M";
155
173
  return undefined;
@@ -561,6 +579,7 @@ export class BambuImplementation {
561
579
  token: string,
562
580
  options: BambuPrintOptionsInternal
563
581
  ): Promise<any> {
582
+ assertDirectPrintSupported(options.bambuModel, serial);
564
583
  if (!options.filePath.toLowerCase().endsWith(".3mf")) {
565
584
  throw new Error("print3mf requires a .3mf input file.");
566
585
  }
@@ -572,27 +591,36 @@ export class BambuImplementation {
572
591
  remoteFileName = remoteFileName.replace(/\.gcode\.3mf\.gcode\.3mf$/i, ".gcode.3mf");
573
592
 
574
593
  // H2-series printers land files at the FTP root and reference them via ftp:///<name>.
575
- // P1/A1/X1 use /cache/<name> and file:///sdcard/cache/<name>.
594
+ // Full-size A1 uses SD root/project_file (reported on firmware 01.08.01.00).
595
+ // P1/X1/A1 mini retain their legacy cache/gcode_file route.
596
+ // P2S keeps the /cache/<name> upload but only accepts the H2-style
597
+ // project_file command, referenced via ftp:///cache/<name> (verified on
598
+ // P2S firmware 01.02.00.00; file:///sdcard/... fails with ERROR STATE).
576
599
  const isH2 =
577
600
  serial.startsWith("093") ||
578
601
  serial.startsWith("094") ||
579
602
  isH2ModelName(options.bambuModel);
580
- const remoteProjectPath = isH2 ? remoteFileName : `cache/${remoteFileName}`;
581
- const remoteUploadPath = isH2 ? `/${remoteFileName}` : `/cache/${remoteFileName}`;
582
- const projectUrl = isH2
583
- ? `ftp:///${remoteFileName}`
603
+ const isP2S = serial.startsWith("22E") || isP2SModelName(options.bambuModel);
604
+ const isA1 = String(options.bambuModel ?? "").trim().toLowerCase() === "a1" ||
605
+ (!options.bambuModel && serial.startsWith("030"));
606
+ const usesH2ProjectFile = isH2 || isP2S;
607
+ const remoteProjectPath = isH2 || isA1 ? remoteFileName : `cache/${remoteFileName}`;
608
+ const remoteUploadPath = `/${remoteProjectPath}`;
609
+ const projectUrl = usesH2ProjectFile
610
+ ? `ftp:///${remoteProjectPath}`
584
611
  : `file:///sdcard/${remoteProjectPath}`;
585
612
 
586
613
  // Upload via basic-ftp directly (bypasses bambu-js double-path bug)
587
614
  await this.ftpUpload(host, token, options.filePath, remoteUploadPath);
588
615
 
589
616
  // Pre-sliced .gcode.3mf files: routing depends on firmware generation.
590
- // P1/A1/X1 series: project_file returns 405004002 for .gcode.3mf (firmware
617
+ // Legacy P1/X1/A1 mini: project_file returns 405004002 for .gcode.3mf (firmware
591
618
  // doesn't recognise the container), so use gcode_file instead.
592
- // H2-series: gcode_file is not supported; project_file works because the
619
+ // H2-series and P2S: gcode_file is not supported (P2S answers 0500-4002
620
+ // "Unsupported file path or name"); project_file works because the
593
621
  // firmware can open the zip and find Metadata/plate_<n>.gcode directly.
594
622
  if (options.filePath.toLowerCase().endsWith(".gcode.3mf")) {
595
- if (!isH2) {
623
+ if (!usesH2ProjectFile && !isA1) {
596
624
  const printer = await this.getPrinter(host, serial, token);
597
625
  await invokeWithoutAck(printer, new GCodeFileCommand({ fileName: remoteProjectPath }));
598
626
  return {
@@ -601,7 +629,7 @@ export class BambuImplementation {
601
629
  remoteProjectPath,
602
630
  };
603
631
  }
604
- // H2-series: fall through to project_file path below
632
+ // H2-series and P2S: fall through to project_file path below
605
633
  }
606
634
 
607
635
  const projectMetadata = await this.resolveProjectFileMetadata(
@@ -687,7 +715,7 @@ export class BambuImplementation {
687
715
 
688
716
  let amsMapping: number[];
689
717
  let amsMapping2: Array<{ ams_id: number; slot_id: number }>;
690
- if (isH2) {
718
+ if (usesH2ProjectFile) {
691
719
  const projLen = Math.max(projectMetadata.projectFilamentCount, baseMapping.length, 1);
692
720
  amsMapping = Array.from({ length: projLen }, (_, i) =>
693
721
  i < baseMapping.length ? baseMapping[i] : -1
@@ -699,7 +727,7 @@ export class BambuImplementation {
699
727
  return { ams_id: Math.floor(v / 4), slot_id: v % 4 };
700
728
  });
701
729
  } else {
702
- amsMapping = Array.from({ length: 5 }, (_, i) =>
730
+ amsMapping = Array.from({ length: Math.max(5, baseMapping.length, projectMetadata.projectFilamentCount) }, (_, i) =>
703
731
  i < baseMapping.length ? baseMapping[i] : -1
704
732
  );
705
733
  amsMapping2 = [];
@@ -707,7 +735,7 @@ export class BambuImplementation {
707
735
 
708
736
  const b = (v: any) => (v ? 1 : 0);
709
737
  let projectFileCmd: Record<string, any>;
710
- if (isH2) {
738
+ if (usesH2ProjectFile) {
711
739
  const submissionId = String(Date.now() & 0x7fffffff);
712
740
  projectFileCmd = {
713
741
  print: {
@@ -1143,8 +1171,10 @@ export class BambuImplementation {
1143
1171
  token: string,
1144
1172
  filePath: string,
1145
1173
  filename: string,
1146
- print: boolean
1174
+ print: boolean,
1175
+ bambuModel?: string
1147
1176
  ) {
1177
+ if (print) assertDirectPrintSupported(bambuModel, serial);
1148
1178
  await fs.access(filePath);
1149
1179
 
1150
1180
  const normalizedFileName = filename.replace(/^\/+/, "");
@@ -1164,7 +1194,7 @@ export class BambuImplementation {
1164
1194
 
1165
1195
  if (print) {
1166
1196
  if (remotePath.toLowerCase().endsWith(".gcode")) {
1167
- response.startResult = await this.startJob(host, serial, token, remotePath);
1197
+ response.startResult = await this.startJob(host, serial, token, remotePath, bambuModel);
1168
1198
  } else if (remotePath.toLowerCase().endsWith(".3mf")) {
1169
1199
  response.note =
1170
1200
  "3MF upload complete. Use print_3mf to start a project print with plate and metadata options.";
@@ -1178,7 +1208,8 @@ export class BambuImplementation {
1178
1208
  return response;
1179
1209
  }
1180
1210
 
1181
- async startJob(host: string, serial: string, token: string, filename: string) {
1211
+ async startJob(host: string, serial: string, token: string, filename: string, bambuModel?: string) {
1212
+ assertDirectPrintSupported(bambuModel, serial);
1182
1213
  const lower = filename.toLowerCase();
1183
1214
  if (lower.endsWith(".3mf") && !lower.endsWith(".gcode.3mf")) {
1184
1215
  throw new Error("Use print_3mf for .3mf project files.");
@@ -1265,15 +1296,15 @@ export class BambuImplementation {
1265
1296
  // P1/A1 series still use the proprietary TCP-on-6000 framed JPEG path
1266
1297
  // (per https://github.com/Doridian/OpenBambuAPI/blob/main/video.md).
1267
1298
  const TCP_CAMERA_MODELS = new Set(["a1", "a1mini", "p1s", "p1p"]);
1268
- // X1, P2S, AND H2 (H2S/H2D/H2C) all use RTSP on port 322. The
1299
+ // X1, P2S, H2 (H2S/H2D/H2C), AND X2D all use RTSP on port 322. The
1269
1300
  // OpenBambuAPI doc only mentions X1/P2S, but the HA bambulab
1270
1301
  // integration's models.py shows the printer reports its own
1271
1302
  // `ipcam.rtsp_url` for these models, and Parker (H2S) rejects the
1272
1303
  // A1/P1 80-byte auth packet on port 6000 (verified 2026-04-27 --
1273
- // see PROGRESS.md "H2 probe results").
1304
+ // confirmed by local H2 camera transport probes).
1274
1305
  const RTSP_MODELS = new Set([
1275
1306
  "x1", "x1c", "x1carbon", "x1e", "p2s",
1276
- "h2", "h2s", "h2d", "h2c", "h2dpro",
1307
+ "h2", "h2s", "h2d", "h2c", "h2dpro", "x2d",
1277
1308
  ]);
1278
1309
 
1279
1310
  if (!model) {