bambu-printer-mcp 1.1.6 → 1.1.8
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 +26 -0
- package/README.md +98 -25
- package/dist/blender-mcp-bridge.d.ts +10 -0
- package/dist/blender-mcp-bridge.js +389 -0
- package/dist/index.js +116 -80
- package/dist/printers/bambu.js +31 -13
- package/dist/slicer/profile-flatten.d.ts +15 -4
- package/dist/slicer/profile-flatten.js +181 -74
- package/dist/stl/stl-manipulator.d.ts +3 -17
- package/dist/stl/stl-manipulator.js +142 -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 +133 -103
- package/src/printers/bambu.ts +29 -13
- package/src/slicer/profile-flatten.ts +202 -80
- package/src/stl/stl-manipulator.ts +161 -98
package/src/index.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
14
14
|
import dotenv from "dotenv";
|
|
15
15
|
import fs from "fs";
|
|
16
|
+
import os from "node:os";
|
|
16
17
|
import path from "path";
|
|
17
18
|
import { createServer as createHttpServer, type IncomingMessage, type Server as HttpServer, type ServerResponse } from "node:http";
|
|
18
19
|
import { randomUUID } from "node:crypto";
|
|
@@ -24,6 +25,7 @@ import {
|
|
|
24
25
|
type SlicerType,
|
|
25
26
|
} from "./stl/stl-manipulator.js";
|
|
26
27
|
import { BambuNetworkBridge, type BambuNetworkBridgeOptions } from "./bambu-network-bridge.js";
|
|
28
|
+
import { BlenderMcpBridge } from "./blender-mcp-bridge.js";
|
|
27
29
|
import { hasAmsMappingInput, normalizeAmsMappingObject, normalizeBridgeAmsTrayValue } from "./ams-mapping.js";
|
|
28
30
|
import { analyze3MFAmsRequirements, analyze3MFPlateObjects, analyzeCollarCharm3MF, extractBambuTemplateSettings, getCollarCharmRolePolicy, parse3MF } from './3mf_parser.js';
|
|
29
31
|
import type { ThreeMFAmsRequirements } from "./types.js";
|
|
@@ -36,7 +38,23 @@ const DEFAULT_BAMBU_SERIAL = process.env.BAMBU_PRINTER_SERIAL || process.env.BAM
|
|
|
36
38
|
const DEFAULT_BAMBU_TOKEN =
|
|
37
39
|
process.env.BAMBU_PRINTER_ACCESS_TOKEN || process.env.BAMBU_TOKEN || "";
|
|
38
40
|
const DEFAULT_BAMBU_DEV_ID = process.env.BAMBU_DEV_ID || DEFAULT_BAMBU_SERIAL;
|
|
39
|
-
|
|
41
|
+
// os.tmpdir(), not process.cwd() — a packaged Claude Desktop extension is
|
|
42
|
+
// spawned with a cwd we don't control (and may not be writable), so a
|
|
43
|
+
// cwd-relative default here can throw at module load via the mkdirSync
|
|
44
|
+
// below and kill the server before it ever opens the stdio transport.
|
|
45
|
+
const AUTOMATIC_TEMP_DIR = process.env.TEMP_DIR ? undefined : fs.mkdtempSync(path.join(os.tmpdir(), "bambu-printer-mcp-"));
|
|
46
|
+
const TEMP_DIR = process.env.TEMP_DIR || AUTOMATIC_TEMP_DIR!;
|
|
47
|
+
if (AUTOMATIC_TEMP_DIR) {
|
|
48
|
+
// The exit event also covers startup failures and normal event-loop exit.
|
|
49
|
+
// Only the directory created above belongs to us; explicit TEMP_DIR is retained.
|
|
50
|
+
process.once("exit", () => {
|
|
51
|
+
try {
|
|
52
|
+
fs.rmSync(AUTOMATIC_TEMP_DIR, { recursive: true, force: true });
|
|
53
|
+
} catch (error) {
|
|
54
|
+
console.error("Unable to remove the server's temporary directory:", error);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
}
|
|
40
58
|
|
|
41
59
|
// Printer model and bed type
|
|
42
60
|
const DEFAULT_BAMBU_MODEL =
|
|
@@ -1015,16 +1033,18 @@ class BambuPrinterMCPServer {
|
|
|
1015
1033
|
private server: Server;
|
|
1016
1034
|
private bambu: BambuImplementation;
|
|
1017
1035
|
private bambuNetwork: BambuNetworkBridge;
|
|
1036
|
+
private readonly blender = new BlenderMcpBridge();
|
|
1018
1037
|
private stlManipulator: STLManipulator;
|
|
1019
1038
|
private readonly runtimeConfig: RuntimeConfig;
|
|
1020
1039
|
private httpRuntime?: { transport: StreamableHTTPServerTransport; httpServer: HttpServer };
|
|
1040
|
+
private shuttingDown = false;
|
|
1021
1041
|
|
|
1022
1042
|
constructor() {
|
|
1023
1043
|
this.runtimeConfig = readRuntimeConfig();
|
|
1024
1044
|
this.server = new Server(
|
|
1025
1045
|
{
|
|
1026
1046
|
name: "bambu-printer-mcp",
|
|
1027
|
-
version: "
|
|
1047
|
+
version: JSON.parse(fs.readFileSync(new URL("../package.json", import.meta.url), "utf8")).version
|
|
1028
1048
|
},
|
|
1029
1049
|
{
|
|
1030
1050
|
capabilities: {
|
|
@@ -2637,29 +2657,63 @@ class BambuPrinterMCPServer {
|
|
|
2637
2657
|
required: ["stl_path"]
|
|
2638
2658
|
}
|
|
2639
2659
|
},
|
|
2660
|
+
{
|
|
2661
|
+
name: "blender_mcp_status",
|
|
2662
|
+
description: "Inspect Blender MCP configuration or connect and discover the remote server's tools and schemas. Connecting does not edit the scene; use get_scene_info through blender_mcp_call to check the Blender addon.",
|
|
2663
|
+
inputSchema: {
|
|
2664
|
+
type: "object",
|
|
2665
|
+
properties: {
|
|
2666
|
+
connect: { type: "boolean", description: "Initialize the configured stdio MCP server and discover its tools (default false)." },
|
|
2667
|
+
timeout_ms: { type: "integer", minimum: 100, maximum: 300000, description: "Total connection and discovery deadline in milliseconds; defaults to BLENDER_MCP_TIMEOUT_MS or 120000." }
|
|
2668
|
+
},
|
|
2669
|
+
additionalProperties: false
|
|
2670
|
+
}
|
|
2671
|
+
},
|
|
2672
|
+
{
|
|
2673
|
+
name: "blender_mcp_call",
|
|
2674
|
+
description: "Call a discovered tool on the configured Blender MCP server, preserving its full MCP content and errors. Discover tool schemas with blender_mcp_status first; execute_blender_code accepts Python code and user_prompt. Calls can modify the active Blender scene and are never automatically retried.",
|
|
2675
|
+
inputSchema: {
|
|
2676
|
+
type: "object",
|
|
2677
|
+
properties: {
|
|
2678
|
+
tool_name: { type: "string", description: "Exact name advertised by Blender MCP, such as get_scene_info or execute_blender_code." },
|
|
2679
|
+
arguments: { type: "object", description: "Arguments matching the remote tool's discovered input schema. Preserve the user's own words in user_prompt when the remote tool requests it." },
|
|
2680
|
+
timeout_ms: { type: "integer", minimum: 100, maximum: 300000, description: "Total connection, discovery, and tool deadline in milliseconds; defaults to BLENDER_MCP_TIMEOUT_MS or 120000." }
|
|
2681
|
+
},
|
|
2682
|
+
required: ["tool_name"],
|
|
2683
|
+
additionalProperties: false
|
|
2684
|
+
}
|
|
2685
|
+
},
|
|
2640
2686
|
{
|
|
2641
2687
|
name: "blender_mcp_edit_model",
|
|
2642
|
-
description: "
|
|
2688
|
+
description: "Import, edit, and export a local STL through standard Blender MCP with verified output and existing scene objects preserved. Requires a shared local filesystem and Blender Object Mode. Also supports a separately configured legacy executable bridge.",
|
|
2643
2689
|
inputSchema: {
|
|
2644
2690
|
type: "object",
|
|
2645
2691
|
properties: {
|
|
2646
2692
|
stl_path: { type: "string", description: "Path to the local STL file" },
|
|
2647
2693
|
operations: {
|
|
2648
2694
|
type: "array",
|
|
2649
|
-
description: "Ordered
|
|
2695
|
+
description: "Ordered operations: decimate:<ratio greater than 0 and at most 1>, remesh:<positive voxel size in STL units>, boolean_union:<STL path>. Legacy custom bridges define their own operations.",
|
|
2696
|
+
minItems: 1,
|
|
2697
|
+
maxItems: 64,
|
|
2650
2698
|
items: { type: "string" }
|
|
2651
2699
|
},
|
|
2652
|
-
|
|
2653
|
-
|
|
2700
|
+
output_path: { type: "string", description: "Required for standard MCP previews and execution: new local STL output path whose parent exists. Existing files are never overwritten. Optional for legacy-only bridge configuration." },
|
|
2701
|
+
user_prompt: { type: "string", description: "The user's own words describing the edit, passed unchanged to Blender MCP." },
|
|
2702
|
+
timeout_ms: { type: "integer", minimum: 100, maximum: 300000, description: "Total Blender request deadline in milliseconds; defaults to BLENDER_MCP_TIMEOUT_MS or 120000." },
|
|
2703
|
+
bridge_command: { type: "string", description: "Legacy custom bridge executable override, not a standard MCP command. Per-call overrides require MCP_ALLOW_EXECUTABLE_ARG=1." },
|
|
2704
|
+
execute: { type: "boolean", description: "Apply edits and export (true) or validate and return the prepared request without connecting (false, default)." }
|
|
2654
2705
|
},
|
|
2655
|
-
required:
|
|
2706
|
+
required: process.env.BLENDER_MCP_COMMAND?.trim()
|
|
2707
|
+
? ["stl_path", "operations", "output_path"]
|
|
2708
|
+
: ["stl_path", "operations"],
|
|
2709
|
+
additionalProperties: false
|
|
2656
2710
|
}
|
|
2657
2711
|
}
|
|
2658
2712
|
]
|
|
2659
2713
|
};
|
|
2660
2714
|
});
|
|
2661
2715
|
|
|
2662
|
-
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
2716
|
+
this.server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
|
|
2663
2717
|
const { name, arguments: args } = request.params;
|
|
2664
2718
|
|
|
2665
2719
|
const host = String(args?.host || DEFAULT_HOST);
|
|
@@ -3229,57 +3283,50 @@ class BambuPrinterMCPServer {
|
|
|
3229
3283
|
|
|
3230
3284
|
let threeMFPath = String(args.three_mf_path);
|
|
3231
3285
|
|
|
3232
|
-
//
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
if (
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3286
|
+
// Inspect and auto-slice before upload. Preserve any failure instead of sending an unsliced project.
|
|
3287
|
+
const JSZip = (await import('jszip')).default;
|
|
3288
|
+
const zipData = fs.readFileSync(threeMFPath);
|
|
3289
|
+
const zip = await JSZip.loadAsync(zipData);
|
|
3290
|
+
const hasGcode = Object.values(zip.files).some(
|
|
3291
|
+
entry => !entry.dir && /\.gcode$/i.test(entry.name)
|
|
3292
|
+
);
|
|
3293
|
+
if (!hasGcode) {
|
|
3294
|
+
if (printBedType === "supertack_plate") {
|
|
3295
|
+
throw new Error(
|
|
3296
|
+
'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.'
|
|
3297
|
+
);
|
|
3298
|
+
}
|
|
3299
|
+
console.log(`3MF has no gcode — auto-slicing with ${slicerType} for ${printModel}`);
|
|
3300
|
+
const autoSliceOptions: BambuSliceOptions = {
|
|
3301
|
+
uptodate: true,
|
|
3302
|
+
ensureOnBed: true,
|
|
3303
|
+
minSave: true,
|
|
3304
|
+
skipModifiedGcodes: true,
|
|
3305
|
+
bedType: printBedType,
|
|
3306
|
+
};
|
|
3307
|
+
if (!explicitSlicerProfile) {
|
|
3308
|
+
try {
|
|
3309
|
+
const liveFilaments = await this.getResolvedPrinterFilamentInventory(
|
|
3310
|
+
host,
|
|
3311
|
+
bambuSerial,
|
|
3312
|
+
bambuToken,
|
|
3313
|
+
printModel,
|
|
3314
|
+
printNozzle
|
|
3244
3315
|
);
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
const autoSliceOptions: BambuSliceOptions = {
|
|
3248
|
-
uptodate: true,
|
|
3249
|
-
ensureOnBed: true,
|
|
3250
|
-
minSave: true,
|
|
3251
|
-
skipModifiedGcodes: true,
|
|
3252
|
-
bedType: printBedType,
|
|
3253
|
-
};
|
|
3254
|
-
if (!explicitSlicerProfile) {
|
|
3255
|
-
try {
|
|
3256
|
-
const liveFilaments = await this.getResolvedPrinterFilamentInventory(
|
|
3257
|
-
host,
|
|
3258
|
-
bambuSerial,
|
|
3259
|
-
bambuToken,
|
|
3260
|
-
printModel,
|
|
3261
|
-
printNozzle
|
|
3262
|
-
);
|
|
3263
|
-
if (liveFilaments.recommended?.load_filaments) {
|
|
3264
|
-
autoSliceOptions.loadFilaments = liveFilaments.recommended.load_filaments;
|
|
3265
|
-
}
|
|
3266
|
-
} catch (filamentError) {
|
|
3267
|
-
console.warn("Could not resolve live printer filaments for auto-slicing:", filamentError);
|
|
3316
|
+
if (liveFilaments.recommended?.load_filaments) {
|
|
3317
|
+
autoSliceOptions.loadFilaments = liveFilaments.recommended.load_filaments;
|
|
3268
3318
|
}
|
|
3319
|
+
} catch (filamentError) {
|
|
3320
|
+
console.warn("Could not resolve live printer filaments for auto-slicing:", filamentError);
|
|
3269
3321
|
}
|
|
3270
|
-
threeMFPath = await this.stlManipulator.sliceSTL(
|
|
3271
|
-
threeMFPath, slicerType, slicerPath, activeSlicerProfile,
|
|
3272
|
-
undefined, // progressCallback
|
|
3273
|
-
printPreset,
|
|
3274
|
-
autoSliceOptions
|
|
3275
|
-
);
|
|
3276
|
-
console.log("Auto-sliced to: " + threeMFPath);
|
|
3277
|
-
}
|
|
3278
|
-
} catch (sliceCheckErr: any) {
|
|
3279
|
-
if (String(sliceCheckErr?.message || "").includes("SuperTack")) {
|
|
3280
|
-
throw sliceCheckErr;
|
|
3281
3322
|
}
|
|
3282
|
-
|
|
3323
|
+
threeMFPath = await this.stlManipulator.sliceSTL(
|
|
3324
|
+
threeMFPath, slicerType, slicerPath, activeSlicerProfile,
|
|
3325
|
+
undefined, // progressCallback
|
|
3326
|
+
printPreset,
|
|
3327
|
+
autoSliceOptions
|
|
3328
|
+
);
|
|
3329
|
+
console.log("Auto-sliced to: " + threeMFPath);
|
|
3283
3330
|
}
|
|
3284
3331
|
|
|
3285
3332
|
const parsed3MFData = await parse3MF(threeMFPath);
|
|
@@ -3496,21 +3543,24 @@ class BambuPrinterMCPServer {
|
|
|
3496
3543
|
result = await this.stlManipulator.layFlat(String(args.stl_path));
|
|
3497
3544
|
break;
|
|
3498
3545
|
|
|
3546
|
+
case "blender_mcp_status":
|
|
3547
|
+
result = await this.blender.status(args ?? {}, extra.signal);
|
|
3548
|
+
break;
|
|
3549
|
+
|
|
3550
|
+
case "blender_mcp_call":
|
|
3551
|
+
return await this.blender.call(args ?? {}, extra.signal);
|
|
3552
|
+
|
|
3499
3553
|
case "blender_mcp_edit_model":
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
stlPath: String(args.stl_path),
|
|
3505
|
-
operations: args.operations.map((entry: any) => String(entry)),
|
|
3506
|
-
execute: Boolean(args.execute ?? false),
|
|
3507
|
-
bridgeCommand: this.resolveExecutableSelectorArg(
|
|
3508
|
-
args.bridge_command,
|
|
3554
|
+
result = await this.blender.edit(
|
|
3555
|
+
args ?? {},
|
|
3556
|
+
this.resolveExecutableSelectorArg(
|
|
3557
|
+
args?.bridge_command,
|
|
3509
3558
|
"blender_mcp_edit_model",
|
|
3510
3559
|
"bridge_command",
|
|
3511
3560
|
true
|
|
3512
|
-
) ?? this.runtimeConfig.blenderBridgeCommand,
|
|
3513
|
-
|
|
3561
|
+
) ?? (!process.env.BLENDER_MCP_COMMAND?.trim() ? this.runtimeConfig.blenderBridgeCommand : undefined),
|
|
3562
|
+
extra.signal
|
|
3563
|
+
);
|
|
3514
3564
|
break;
|
|
3515
3565
|
|
|
3516
3566
|
default:
|
|
@@ -3547,49 +3597,27 @@ class BambuPrinterMCPServer {
|
|
|
3547
3597
|
});
|
|
3548
3598
|
}
|
|
3549
3599
|
|
|
3550
|
-
private async invokeBlenderBridge(params: {
|
|
3551
|
-
stlPath: string;
|
|
3552
|
-
operations: string[];
|
|
3553
|
-
execute: boolean;
|
|
3554
|
-
bridgeCommand?: string;
|
|
3555
|
-
}): Promise<any> {
|
|
3556
|
-
const payload = {
|
|
3557
|
-
stlPath: params.stlPath,
|
|
3558
|
-
operations: params.operations,
|
|
3559
|
-
};
|
|
3560
|
-
|
|
3561
|
-
if (!params.execute || !params.bridgeCommand) {
|
|
3562
|
-
return {
|
|
3563
|
-
status: "prepared",
|
|
3564
|
-
payload,
|
|
3565
|
-
note: params.bridgeCommand
|
|
3566
|
-
? "Set execute=true to run the Blender bridge command."
|
|
3567
|
-
: "No BLENDER_MCP_BRIDGE_COMMAND configured. Set the env var or pass bridge_command.",
|
|
3568
|
-
};
|
|
3569
|
-
}
|
|
3570
|
-
|
|
3571
|
-
const { execFile } = await import("node:child_process");
|
|
3572
|
-
const { promisify } = await import("node:util");
|
|
3573
|
-
const execFileAsync = promisify(execFile);
|
|
3574
|
-
|
|
3575
|
-
const { stdout, stderr } = await execFileAsync(params.bridgeCommand, [], {
|
|
3576
|
-
env: { ...process.env, MCP_BLENDER_PAYLOAD: JSON.stringify(payload) },
|
|
3577
|
-
timeout: 120_000,
|
|
3578
|
-
});
|
|
3579
|
-
|
|
3580
|
-
return {
|
|
3581
|
-
status: "executed",
|
|
3582
|
-
stdout: stdout.trim(),
|
|
3583
|
-
stderr: stderr.trim(),
|
|
3584
|
-
};
|
|
3585
|
-
}
|
|
3586
|
-
|
|
3587
3600
|
async startStdio() {
|
|
3588
3601
|
const transport = new StdioServerTransport();
|
|
3589
3602
|
await this.server.connect(transport);
|
|
3603
|
+
process.stdin.once("end", () => { void this.shutdown(); });
|
|
3590
3604
|
console.error("Bambu Printer MCP server running on stdio");
|
|
3591
3605
|
}
|
|
3592
3606
|
|
|
3607
|
+
async shutdown(exitCode = 0) {
|
|
3608
|
+
if (this.shuttingDown) return;
|
|
3609
|
+
this.shuttingDown = true;
|
|
3610
|
+
try {
|
|
3611
|
+
// Closing the MCP transport aborts its active requests before exit cleanup.
|
|
3612
|
+
await this.server.close();
|
|
3613
|
+
this.httpRuntime?.httpServer.close();
|
|
3614
|
+
} catch (error) {
|
|
3615
|
+
console.error("[MCP Shutdown]", error);
|
|
3616
|
+
} finally {
|
|
3617
|
+
process.exit(exitCode);
|
|
3618
|
+
}
|
|
3619
|
+
}
|
|
3620
|
+
|
|
3593
3621
|
async startHttp() {
|
|
3594
3622
|
const { httpHost, httpPort, httpPath, statefulSession, enableJsonResponse, allowedOrigins } =
|
|
3595
3623
|
this.runtimeConfig;
|
|
@@ -3640,4 +3668,6 @@ class BambuPrinterMCPServer {
|
|
|
3640
3668
|
}
|
|
3641
3669
|
|
|
3642
3670
|
const server = new BambuPrinterMCPServer();
|
|
3671
|
+
process.once("SIGINT", () => { void server.shutdown(130); });
|
|
3672
|
+
process.once("SIGTERM", () => { void server.shutdown(143); });
|
|
3643
3673
|
server.run().catch(console.error);
|
package/src/printers/bambu.ts
CHANGED
|
@@ -62,6 +62,10 @@ function isH2ModelName(model: unknown): boolean {
|
|
|
62
62
|
return H2_MODEL_NAMES.has(String(model ?? "").trim().toLowerCase().replace(/\s+/g, " "));
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
function isP2SModelName(model: unknown): boolean {
|
|
66
|
+
return String(model ?? "").trim().toLowerCase() === "p2s";
|
|
67
|
+
}
|
|
68
|
+
|
|
65
69
|
interface BambuPrintOptionsInternal {
|
|
66
70
|
projectName: string;
|
|
67
71
|
filePath: string;
|
|
@@ -145,11 +149,14 @@ class TolerantBambuClient extends BambuClient {
|
|
|
145
149
|
const sn = this.config.serialNumber;
|
|
146
150
|
if (sn.startsWith("093")) return "H2S";
|
|
147
151
|
if (sn.startsWith("094")) return "H2D";
|
|
152
|
+
if (sn.startsWith("239")) return "H2C";
|
|
153
|
+
if (sn.startsWith("31B")) return "H2DPRO";
|
|
148
154
|
if (sn.startsWith("00M")) return "X1C";
|
|
149
155
|
if (sn.startsWith("00W")) return "X1";
|
|
150
156
|
if (sn.startsWith("03W")) return "X1E";
|
|
151
157
|
if (sn.startsWith("01S")) return "P1P";
|
|
152
158
|
if (sn.startsWith("01P")) return "P1S";
|
|
159
|
+
if (sn.startsWith("22E")) return "P2S";
|
|
153
160
|
if (sn.startsWith("030")) return "A1";
|
|
154
161
|
if (sn.startsWith("039")) return "A1M";
|
|
155
162
|
return undefined;
|
|
@@ -572,27 +579,36 @@ export class BambuImplementation {
|
|
|
572
579
|
remoteFileName = remoteFileName.replace(/\.gcode\.3mf\.gcode\.3mf$/i, ".gcode.3mf");
|
|
573
580
|
|
|
574
581
|
// H2-series printers land files at the FTP root and reference them via ftp:///<name>.
|
|
575
|
-
//
|
|
582
|
+
// Full-size A1 uses SD root/project_file (reported on firmware 01.08.01.00).
|
|
583
|
+
// P1/X1/A1 mini retain their legacy cache/gcode_file route.
|
|
584
|
+
// P2S keeps the /cache/<name> upload but only accepts the H2-style
|
|
585
|
+
// project_file command, referenced via ftp:///cache/<name> (verified on
|
|
586
|
+
// P2S firmware 01.02.00.00; file:///sdcard/... fails with ERROR STATE).
|
|
576
587
|
const isH2 =
|
|
577
588
|
serial.startsWith("093") ||
|
|
578
589
|
serial.startsWith("094") ||
|
|
579
590
|
isH2ModelName(options.bambuModel);
|
|
580
|
-
const
|
|
581
|
-
const
|
|
582
|
-
|
|
583
|
-
|
|
591
|
+
const isP2S = serial.startsWith("22E") || isP2SModelName(options.bambuModel);
|
|
592
|
+
const isA1 = String(options.bambuModel ?? "").trim().toLowerCase() === "a1" ||
|
|
593
|
+
(!options.bambuModel && serial.startsWith("030"));
|
|
594
|
+
const usesH2ProjectFile = isH2 || isP2S;
|
|
595
|
+
const remoteProjectPath = isH2 || isA1 ? remoteFileName : `cache/${remoteFileName}`;
|
|
596
|
+
const remoteUploadPath = `/${remoteProjectPath}`;
|
|
597
|
+
const projectUrl = usesH2ProjectFile
|
|
598
|
+
? `ftp:///${remoteProjectPath}`
|
|
584
599
|
: `file:///sdcard/${remoteProjectPath}`;
|
|
585
600
|
|
|
586
601
|
// Upload via basic-ftp directly (bypasses bambu-js double-path bug)
|
|
587
602
|
await this.ftpUpload(host, token, options.filePath, remoteUploadPath);
|
|
588
603
|
|
|
589
604
|
// Pre-sliced .gcode.3mf files: routing depends on firmware generation.
|
|
590
|
-
// P1/A1
|
|
605
|
+
// Legacy P1/X1/A1 mini: project_file returns 405004002 for .gcode.3mf (firmware
|
|
591
606
|
// doesn't recognise the container), so use gcode_file instead.
|
|
592
|
-
// H2-series: gcode_file is not supported
|
|
607
|
+
// H2-series and P2S: gcode_file is not supported (P2S answers 0500-4002
|
|
608
|
+
// "Unsupported file path or name"); project_file works because the
|
|
593
609
|
// firmware can open the zip and find Metadata/plate_<n>.gcode directly.
|
|
594
610
|
if (options.filePath.toLowerCase().endsWith(".gcode.3mf")) {
|
|
595
|
-
if (!
|
|
611
|
+
if (!usesH2ProjectFile && !isA1) {
|
|
596
612
|
const printer = await this.getPrinter(host, serial, token);
|
|
597
613
|
await invokeWithoutAck(printer, new GCodeFileCommand({ fileName: remoteProjectPath }));
|
|
598
614
|
return {
|
|
@@ -601,7 +617,7 @@ export class BambuImplementation {
|
|
|
601
617
|
remoteProjectPath,
|
|
602
618
|
};
|
|
603
619
|
}
|
|
604
|
-
// H2-series: fall through to project_file path below
|
|
620
|
+
// H2-series and P2S: fall through to project_file path below
|
|
605
621
|
}
|
|
606
622
|
|
|
607
623
|
const projectMetadata = await this.resolveProjectFileMetadata(
|
|
@@ -687,7 +703,7 @@ export class BambuImplementation {
|
|
|
687
703
|
|
|
688
704
|
let amsMapping: number[];
|
|
689
705
|
let amsMapping2: Array<{ ams_id: number; slot_id: number }>;
|
|
690
|
-
if (
|
|
706
|
+
if (usesH2ProjectFile) {
|
|
691
707
|
const projLen = Math.max(projectMetadata.projectFilamentCount, baseMapping.length, 1);
|
|
692
708
|
amsMapping = Array.from({ length: projLen }, (_, i) =>
|
|
693
709
|
i < baseMapping.length ? baseMapping[i] : -1
|
|
@@ -699,7 +715,7 @@ export class BambuImplementation {
|
|
|
699
715
|
return { ams_id: Math.floor(v / 4), slot_id: v % 4 };
|
|
700
716
|
});
|
|
701
717
|
} else {
|
|
702
|
-
amsMapping = Array.from({ length: 5 }, (_, i) =>
|
|
718
|
+
amsMapping = Array.from({ length: Math.max(5, baseMapping.length, projectMetadata.projectFilamentCount) }, (_, i) =>
|
|
703
719
|
i < baseMapping.length ? baseMapping[i] : -1
|
|
704
720
|
);
|
|
705
721
|
amsMapping2 = [];
|
|
@@ -707,7 +723,7 @@ export class BambuImplementation {
|
|
|
707
723
|
|
|
708
724
|
const b = (v: any) => (v ? 1 : 0);
|
|
709
725
|
let projectFileCmd: Record<string, any>;
|
|
710
|
-
if (
|
|
726
|
+
if (usesH2ProjectFile) {
|
|
711
727
|
const submissionId = String(Date.now() & 0x7fffffff);
|
|
712
728
|
projectFileCmd = {
|
|
713
729
|
print: {
|
|
@@ -1270,7 +1286,7 @@ export class BambuImplementation {
|
|
|
1270
1286
|
// integration's models.py shows the printer reports its own
|
|
1271
1287
|
// `ipcam.rtsp_url` for these models, and Parker (H2S) rejects the
|
|
1272
1288
|
// A1/P1 80-byte auth packet on port 6000 (verified 2026-04-27 --
|
|
1273
|
-
//
|
|
1289
|
+
// confirmed by local H2 camera transport probes).
|
|
1274
1290
|
const RTSP_MODELS = new Set([
|
|
1275
1291
|
"x1", "x1c", "x1carbon", "x1e", "p2s",
|
|
1276
1292
|
"h2", "h2s", "h2d", "h2c", "h2dpro",
|