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.
- package/CONTRIBUTORS.md +27 -0
- package/README.md +103 -28
- package/dist/blender-mcp-bridge.d.ts +10 -0
- package/dist/blender-mcp-bridge.js +389 -0
- package/dist/index.js +155 -91
- package/dist/printers/bambu.d.ts +3 -2
- package/dist/printers/bambu.js +49 -18
- package/dist/slicer/profile-flatten.d.ts +29 -4
- package/dist/slicer/profile-flatten.js +269 -74
- package/dist/stl/stl-manipulator.d.ts +4 -17
- package/dist/stl/stl-manipulator.js +161 -85
- package/package.json +8 -4
- package/patches/bambu-node+3.22.21.patch +18 -2
- package/scripts/install-patches.mjs +64 -0
- package/src/blender-mcp-bridge.ts +344 -0
- package/src/index.ts +169 -114
- package/src/printers/bambu.ts +49 -18
- package/src/slicer/profile-flatten.ts +307 -82
- package/src/stl/stl-manipulator.ts +184 -98
package/src/printers/bambu.ts
CHANGED
|
@@ -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
|
-
//
|
|
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
|
|
581
|
-
const
|
|
582
|
-
|
|
583
|
-
|
|
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
|
|
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
|
|
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 (!
|
|
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 (
|
|
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 (
|
|
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,
|
|
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
|
-
//
|
|
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) {
|