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/dist/index.js
CHANGED
|
@@ -5,11 +5,13 @@ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/
|
|
|
5
5
|
import { ListResourcesRequestSchema, ReadResourceRequestSchema, ListToolsRequestSchema, CallToolRequestSchema, ErrorCode, McpError } from "@modelcontextprotocol/sdk/types.js";
|
|
6
6
|
import dotenv from "dotenv";
|
|
7
7
|
import fs from "fs";
|
|
8
|
+
import os from "node:os";
|
|
8
9
|
import path from "path";
|
|
9
10
|
import { createServer as createHttpServer } from "node:http";
|
|
10
11
|
import { randomUUID } from "node:crypto";
|
|
11
12
|
import { STLManipulator, SLICER_TYPES, normalizeSlicerType, } from "./stl/stl-manipulator.js";
|
|
12
13
|
import { BambuNetworkBridge } from "./bambu-network-bridge.js";
|
|
14
|
+
import { BlenderMcpBridge } from "./blender-mcp-bridge.js";
|
|
13
15
|
import { hasAmsMappingInput, normalizeAmsMappingObject, normalizeBridgeAmsTrayValue } from "./ams-mapping.js";
|
|
14
16
|
import { analyze3MFAmsRequirements, analyze3MFPlateObjects, analyzeCollarCharm3MF, extractBambuTemplateSettings, getCollarCharmRolePolicy, parse3MF } from './3mf_parser.js';
|
|
15
17
|
import { BambuImplementation } from "./printers/bambu.js";
|
|
@@ -18,7 +20,24 @@ const DEFAULT_HOST = process.env.BAMBU_PRINTER_HOST || process.env.PRINTER_HOST
|
|
|
18
20
|
const DEFAULT_BAMBU_SERIAL = process.env.BAMBU_PRINTER_SERIAL || process.env.BAMBU_SERIAL || "";
|
|
19
21
|
const DEFAULT_BAMBU_TOKEN = process.env.BAMBU_PRINTER_ACCESS_TOKEN || process.env.BAMBU_TOKEN || "";
|
|
20
22
|
const DEFAULT_BAMBU_DEV_ID = process.env.BAMBU_DEV_ID || DEFAULT_BAMBU_SERIAL;
|
|
21
|
-
|
|
23
|
+
// os.tmpdir(), not process.cwd() — a packaged Claude Desktop extension is
|
|
24
|
+
// spawned with a cwd we don't control (and may not be writable), so a
|
|
25
|
+
// cwd-relative default here can throw at module load via the mkdirSync
|
|
26
|
+
// below and kill the server before it ever opens the stdio transport.
|
|
27
|
+
const AUTOMATIC_TEMP_DIR = process.env.TEMP_DIR ? undefined : fs.mkdtempSync(path.join(os.tmpdir(), "bambu-printer-mcp-"));
|
|
28
|
+
const TEMP_DIR = process.env.TEMP_DIR || AUTOMATIC_TEMP_DIR;
|
|
29
|
+
if (AUTOMATIC_TEMP_DIR) {
|
|
30
|
+
// The exit event also covers startup failures and normal event-loop exit.
|
|
31
|
+
// Only the directory created above belongs to us; explicit TEMP_DIR is retained.
|
|
32
|
+
process.once("exit", () => {
|
|
33
|
+
try {
|
|
34
|
+
fs.rmSync(AUTOMATIC_TEMP_DIR, { recursive: true, force: true });
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
console.error("Unable to remove the server's temporary directory:", error);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
}
|
|
22
41
|
// Printer model and bed type
|
|
23
42
|
const DEFAULT_BAMBU_MODEL = process.env.BAMBU_PRINTER_MODEL?.trim().toLowerCase() ||
|
|
24
43
|
process.env.BAMBU_MODEL?.trim().toLowerCase() ||
|
|
@@ -743,10 +762,12 @@ if (!fs.existsSync(TEMP_DIR)) {
|
|
|
743
762
|
}
|
|
744
763
|
class BambuPrinterMCPServer {
|
|
745
764
|
constructor() {
|
|
765
|
+
this.blender = new BlenderMcpBridge();
|
|
766
|
+
this.shuttingDown = false;
|
|
746
767
|
this.runtimeConfig = readRuntimeConfig();
|
|
747
768
|
this.server = new Server({
|
|
748
769
|
name: "bambu-printer-mcp",
|
|
749
|
-
version: "
|
|
770
|
+
version: JSON.parse(fs.readFileSync(new URL("../package.json", import.meta.url), "utf8")).version
|
|
750
771
|
}, {
|
|
751
772
|
capabilities: {
|
|
752
773
|
resources: {},
|
|
@@ -2140,28 +2161,62 @@ class BambuPrinterMCPServer {
|
|
|
2140
2161
|
required: ["stl_path"]
|
|
2141
2162
|
}
|
|
2142
2163
|
},
|
|
2164
|
+
{
|
|
2165
|
+
name: "blender_mcp_status",
|
|
2166
|
+
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.",
|
|
2167
|
+
inputSchema: {
|
|
2168
|
+
type: "object",
|
|
2169
|
+
properties: {
|
|
2170
|
+
connect: { type: "boolean", description: "Initialize the configured stdio MCP server and discover its tools (default false)." },
|
|
2171
|
+
timeout_ms: { type: "integer", minimum: 100, maximum: 300000, description: "Total connection and discovery deadline in milliseconds; defaults to BLENDER_MCP_TIMEOUT_MS or 120000." }
|
|
2172
|
+
},
|
|
2173
|
+
additionalProperties: false
|
|
2174
|
+
}
|
|
2175
|
+
},
|
|
2176
|
+
{
|
|
2177
|
+
name: "blender_mcp_call",
|
|
2178
|
+
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.",
|
|
2179
|
+
inputSchema: {
|
|
2180
|
+
type: "object",
|
|
2181
|
+
properties: {
|
|
2182
|
+
tool_name: { type: "string", description: "Exact name advertised by Blender MCP, such as get_scene_info or execute_blender_code." },
|
|
2183
|
+
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." },
|
|
2184
|
+
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." }
|
|
2185
|
+
},
|
|
2186
|
+
required: ["tool_name"],
|
|
2187
|
+
additionalProperties: false
|
|
2188
|
+
}
|
|
2189
|
+
},
|
|
2143
2190
|
{
|
|
2144
2191
|
name: "blender_mcp_edit_model",
|
|
2145
|
-
description: "
|
|
2192
|
+
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.",
|
|
2146
2193
|
inputSchema: {
|
|
2147
2194
|
type: "object",
|
|
2148
2195
|
properties: {
|
|
2149
2196
|
stl_path: { type: "string", description: "Path to the local STL file" },
|
|
2150
2197
|
operations: {
|
|
2151
2198
|
type: "array",
|
|
2152
|
-
description: "Ordered
|
|
2199
|
+
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.",
|
|
2200
|
+
minItems: 1,
|
|
2201
|
+
maxItems: 64,
|
|
2153
2202
|
items: { type: "string" }
|
|
2154
2203
|
},
|
|
2155
|
-
|
|
2156
|
-
|
|
2204
|
+
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." },
|
|
2205
|
+
user_prompt: { type: "string", description: "The user's own words describing the edit, passed unchanged to Blender MCP." },
|
|
2206
|
+
timeout_ms: { type: "integer", minimum: 100, maximum: 300000, description: "Total Blender request deadline in milliseconds; defaults to BLENDER_MCP_TIMEOUT_MS or 120000." },
|
|
2207
|
+
bridge_command: { type: "string", description: "Legacy custom bridge executable override, not a standard MCP command. Per-call overrides require MCP_ALLOW_EXECUTABLE_ARG=1." },
|
|
2208
|
+
execute: { type: "boolean", description: "Apply edits and export (true) or validate and return the prepared request without connecting (false, default)." }
|
|
2157
2209
|
},
|
|
2158
|
-
required:
|
|
2210
|
+
required: process.env.BLENDER_MCP_COMMAND?.trim()
|
|
2211
|
+
? ["stl_path", "operations", "output_path"]
|
|
2212
|
+
: ["stl_path", "operations"],
|
|
2213
|
+
additionalProperties: false
|
|
2159
2214
|
}
|
|
2160
2215
|
}
|
|
2161
2216
|
]
|
|
2162
2217
|
};
|
|
2163
2218
|
});
|
|
2164
|
-
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
2219
|
+
this.server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
|
|
2165
2220
|
const { name, arguments: args } = request.params;
|
|
2166
2221
|
const host = String(args?.host || DEFAULT_HOST);
|
|
2167
2222
|
const bambuSerial = String(args?.bambu_serial || DEFAULT_BAMBU_SERIAL);
|
|
@@ -2583,45 +2638,37 @@ class BambuPrinterMCPServer {
|
|
|
2583
2638
|
throw new Error("plate_index must be a non-negative integer.");
|
|
2584
2639
|
}
|
|
2585
2640
|
let threeMFPath = String(args.three_mf_path);
|
|
2586
|
-
//
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
if (
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
autoSliceOptions.loadFilaments = liveFilaments.recommended.load_filaments;
|
|
2609
|
-
}
|
|
2610
|
-
}
|
|
2611
|
-
catch (filamentError) {
|
|
2612
|
-
console.warn("Could not resolve live printer filaments for auto-slicing:", filamentError);
|
|
2641
|
+
// Inspect and auto-slice before upload. Preserve any failure instead of sending an unsliced project.
|
|
2642
|
+
const JSZip = (await import('jszip')).default;
|
|
2643
|
+
const zipData = fs.readFileSync(threeMFPath);
|
|
2644
|
+
const zip = await JSZip.loadAsync(zipData);
|
|
2645
|
+
const hasGcode = Object.values(zip.files).some(entry => !entry.dir && /\.gcode$/i.test(entry.name));
|
|
2646
|
+
if (!hasGcode) {
|
|
2647
|
+
if (printBedType === "supertack_plate") {
|
|
2648
|
+
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.');
|
|
2649
|
+
}
|
|
2650
|
+
console.log(`3MF has no gcode — auto-slicing with ${slicerType} for ${printModel}`);
|
|
2651
|
+
const autoSliceOptions = {
|
|
2652
|
+
uptodate: true,
|
|
2653
|
+
ensureOnBed: true,
|
|
2654
|
+
minSave: true,
|
|
2655
|
+
skipModifiedGcodes: true,
|
|
2656
|
+
bedType: printBedType,
|
|
2657
|
+
};
|
|
2658
|
+
if (!explicitSlicerProfile) {
|
|
2659
|
+
try {
|
|
2660
|
+
const liveFilaments = await this.getResolvedPrinterFilamentInventory(host, bambuSerial, bambuToken, printModel, printNozzle);
|
|
2661
|
+
if (liveFilaments.recommended?.load_filaments) {
|
|
2662
|
+
autoSliceOptions.loadFilaments = liveFilaments.recommended.load_filaments;
|
|
2613
2663
|
}
|
|
2614
2664
|
}
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
}
|
|
2619
|
-
}
|
|
2620
|
-
catch (sliceCheckErr) {
|
|
2621
|
-
if (String(sliceCheckErr?.message || "").includes("SuperTack")) {
|
|
2622
|
-
throw sliceCheckErr;
|
|
2665
|
+
catch (filamentError) {
|
|
2666
|
+
console.warn("Could not resolve live printer filaments for auto-slicing:", filamentError);
|
|
2667
|
+
}
|
|
2623
2668
|
}
|
|
2624
|
-
|
|
2669
|
+
threeMFPath = await this.stlManipulator.sliceSTL(threeMFPath, slicerType, slicerPath, activeSlicerProfile, undefined, // progressCallback
|
|
2670
|
+
printPreset, autoSliceOptions);
|
|
2671
|
+
console.log("Auto-sliced to: " + threeMFPath);
|
|
2625
2672
|
}
|
|
2626
2673
|
const parsed3MFData = await parse3MF(threeMFPath);
|
|
2627
2674
|
const isH2Print = H2_BAMBU_MODELS.has(printModel);
|
|
@@ -2783,16 +2830,13 @@ class BambuPrinterMCPServer {
|
|
|
2783
2830
|
throw new Error("Missing required parameter: stl_path");
|
|
2784
2831
|
result = await this.stlManipulator.layFlat(String(args.stl_path));
|
|
2785
2832
|
break;
|
|
2833
|
+
case "blender_mcp_status":
|
|
2834
|
+
result = await this.blender.status(args ?? {}, extra.signal);
|
|
2835
|
+
break;
|
|
2836
|
+
case "blender_mcp_call":
|
|
2837
|
+
return await this.blender.call(args ?? {}, extra.signal);
|
|
2786
2838
|
case "blender_mcp_edit_model":
|
|
2787
|
-
|
|
2788
|
-
throw new Error("Missing required parameters: stl_path and operations");
|
|
2789
|
-
}
|
|
2790
|
-
result = await this.invokeBlenderBridge({
|
|
2791
|
-
stlPath: String(args.stl_path),
|
|
2792
|
-
operations: args.operations.map((entry) => String(entry)),
|
|
2793
|
-
execute: Boolean(args.execute ?? false),
|
|
2794
|
-
bridgeCommand: this.resolveExecutableSelectorArg(args.bridge_command, "blender_mcp_edit_model", "bridge_command", true) ?? this.runtimeConfig.blenderBridgeCommand,
|
|
2795
|
-
});
|
|
2839
|
+
result = await this.blender.edit(args ?? {}, this.resolveExecutableSelectorArg(args?.bridge_command, "blender_mcp_edit_model", "bridge_command", true) ?? (!process.env.BLENDER_MCP_COMMAND?.trim() ? this.runtimeConfig.blenderBridgeCommand : undefined), extra.signal);
|
|
2796
2840
|
break;
|
|
2797
2841
|
default:
|
|
2798
2842
|
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
|
|
@@ -2823,38 +2867,28 @@ class BambuPrinterMCPServer {
|
|
|
2823
2867
|
}
|
|
2824
2868
|
});
|
|
2825
2869
|
}
|
|
2826
|
-
async invokeBlenderBridge(params) {
|
|
2827
|
-
const payload = {
|
|
2828
|
-
stlPath: params.stlPath,
|
|
2829
|
-
operations: params.operations,
|
|
2830
|
-
};
|
|
2831
|
-
if (!params.execute || !params.bridgeCommand) {
|
|
2832
|
-
return {
|
|
2833
|
-
status: "prepared",
|
|
2834
|
-
payload,
|
|
2835
|
-
note: params.bridgeCommand
|
|
2836
|
-
? "Set execute=true to run the Blender bridge command."
|
|
2837
|
-
: "No BLENDER_MCP_BRIDGE_COMMAND configured. Set the env var or pass bridge_command.",
|
|
2838
|
-
};
|
|
2839
|
-
}
|
|
2840
|
-
const { execFile } = await import("node:child_process");
|
|
2841
|
-
const { promisify } = await import("node:util");
|
|
2842
|
-
const execFileAsync = promisify(execFile);
|
|
2843
|
-
const { stdout, stderr } = await execFileAsync(params.bridgeCommand, [], {
|
|
2844
|
-
env: { ...process.env, MCP_BLENDER_PAYLOAD: JSON.stringify(payload) },
|
|
2845
|
-
timeout: 120000,
|
|
2846
|
-
});
|
|
2847
|
-
return {
|
|
2848
|
-
status: "executed",
|
|
2849
|
-
stdout: stdout.trim(),
|
|
2850
|
-
stderr: stderr.trim(),
|
|
2851
|
-
};
|
|
2852
|
-
}
|
|
2853
2870
|
async startStdio() {
|
|
2854
2871
|
const transport = new StdioServerTransport();
|
|
2855
2872
|
await this.server.connect(transport);
|
|
2873
|
+
process.stdin.once("end", () => { void this.shutdown(); });
|
|
2856
2874
|
console.error("Bambu Printer MCP server running on stdio");
|
|
2857
2875
|
}
|
|
2876
|
+
async shutdown(exitCode = 0) {
|
|
2877
|
+
if (this.shuttingDown)
|
|
2878
|
+
return;
|
|
2879
|
+
this.shuttingDown = true;
|
|
2880
|
+
try {
|
|
2881
|
+
// Closing the MCP transport aborts its active requests before exit cleanup.
|
|
2882
|
+
await this.server.close();
|
|
2883
|
+
this.httpRuntime?.httpServer.close();
|
|
2884
|
+
}
|
|
2885
|
+
catch (error) {
|
|
2886
|
+
console.error("[MCP Shutdown]", error);
|
|
2887
|
+
}
|
|
2888
|
+
finally {
|
|
2889
|
+
process.exit(exitCode);
|
|
2890
|
+
}
|
|
2891
|
+
}
|
|
2858
2892
|
async startHttp() {
|
|
2859
2893
|
const { httpHost, httpPort, httpPath, statefulSession, enableJsonResponse, allowedOrigins } = this.runtimeConfig;
|
|
2860
2894
|
const transport = new StreamableHTTPServerTransport({
|
|
@@ -2894,4 +2928,6 @@ class BambuPrinterMCPServer {
|
|
|
2894
2928
|
}
|
|
2895
2929
|
}
|
|
2896
2930
|
const server = new BambuPrinterMCPServer();
|
|
2931
|
+
process.once("SIGINT", () => { void server.shutdown(130); });
|
|
2932
|
+
process.once("SIGTERM", () => { void server.shutdown(143); });
|
|
2897
2933
|
server.run().catch(console.error);
|
package/dist/printers/bambu.js
CHANGED
|
@@ -49,6 +49,9 @@ const H2_MODEL_NAMES = new Set(["h2", "h2c", "h2d", "h2dpro", "h2d pro", "h2s"])
|
|
|
49
49
|
function isH2ModelName(model) {
|
|
50
50
|
return H2_MODEL_NAMES.has(String(model ?? "").trim().toLowerCase().replace(/\s+/g, " "));
|
|
51
51
|
}
|
|
52
|
+
function isP2SModelName(model) {
|
|
53
|
+
return String(model ?? "").trim().toLowerCase() === "p2s";
|
|
54
|
+
}
|
|
52
55
|
function sleep(ms) {
|
|
53
56
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
54
57
|
}
|
|
@@ -88,6 +91,10 @@ class TolerantBambuClient extends BambuClient {
|
|
|
88
91
|
return "H2S";
|
|
89
92
|
if (sn.startsWith("094"))
|
|
90
93
|
return "H2D";
|
|
94
|
+
if (sn.startsWith("239"))
|
|
95
|
+
return "H2C";
|
|
96
|
+
if (sn.startsWith("31B"))
|
|
97
|
+
return "H2DPRO";
|
|
91
98
|
if (sn.startsWith("00M"))
|
|
92
99
|
return "X1C";
|
|
93
100
|
if (sn.startsWith("00W"))
|
|
@@ -98,6 +105,8 @@ class TolerantBambuClient extends BambuClient {
|
|
|
98
105
|
return "P1P";
|
|
99
106
|
if (sn.startsWith("01P"))
|
|
100
107
|
return "P1S";
|
|
108
|
+
if (sn.startsWith("22E"))
|
|
109
|
+
return "P2S";
|
|
101
110
|
if (sn.startsWith("030"))
|
|
102
111
|
return "A1";
|
|
103
112
|
if (sn.startsWith("039"))
|
|
@@ -457,24 +466,33 @@ export class BambuImplementation {
|
|
|
457
466
|
let remoteFileName = path.basename(options.filePath);
|
|
458
467
|
remoteFileName = remoteFileName.replace(/\.gcode\.3mf\.gcode\.3mf$/i, ".gcode.3mf");
|
|
459
468
|
// H2-series printers land files at the FTP root and reference them via ftp:///<name>.
|
|
460
|
-
//
|
|
469
|
+
// Full-size A1 uses SD root/project_file (reported on firmware 01.08.01.00).
|
|
470
|
+
// P1/X1/A1 mini retain their legacy cache/gcode_file route.
|
|
471
|
+
// P2S keeps the /cache/<name> upload but only accepts the H2-style
|
|
472
|
+
// project_file command, referenced via ftp:///cache/<name> (verified on
|
|
473
|
+
// P2S firmware 01.02.00.00; file:///sdcard/... fails with ERROR STATE).
|
|
461
474
|
const isH2 = serial.startsWith("093") ||
|
|
462
475
|
serial.startsWith("094") ||
|
|
463
476
|
isH2ModelName(options.bambuModel);
|
|
464
|
-
const
|
|
465
|
-
const
|
|
466
|
-
|
|
467
|
-
|
|
477
|
+
const isP2S = serial.startsWith("22E") || isP2SModelName(options.bambuModel);
|
|
478
|
+
const isA1 = String(options.bambuModel ?? "").trim().toLowerCase() === "a1" ||
|
|
479
|
+
(!options.bambuModel && serial.startsWith("030"));
|
|
480
|
+
const usesH2ProjectFile = isH2 || isP2S;
|
|
481
|
+
const remoteProjectPath = isH2 || isA1 ? remoteFileName : `cache/${remoteFileName}`;
|
|
482
|
+
const remoteUploadPath = `/${remoteProjectPath}`;
|
|
483
|
+
const projectUrl = usesH2ProjectFile
|
|
484
|
+
? `ftp:///${remoteProjectPath}`
|
|
468
485
|
: `file:///sdcard/${remoteProjectPath}`;
|
|
469
486
|
// Upload via basic-ftp directly (bypasses bambu-js double-path bug)
|
|
470
487
|
await this.ftpUpload(host, token, options.filePath, remoteUploadPath);
|
|
471
488
|
// Pre-sliced .gcode.3mf files: routing depends on firmware generation.
|
|
472
|
-
// P1/A1
|
|
489
|
+
// Legacy P1/X1/A1 mini: project_file returns 405004002 for .gcode.3mf (firmware
|
|
473
490
|
// doesn't recognise the container), so use gcode_file instead.
|
|
474
|
-
// H2-series: gcode_file is not supported
|
|
491
|
+
// H2-series and P2S: gcode_file is not supported (P2S answers 0500-4002
|
|
492
|
+
// "Unsupported file path or name"); project_file works because the
|
|
475
493
|
// firmware can open the zip and find Metadata/plate_<n>.gcode directly.
|
|
476
494
|
if (options.filePath.toLowerCase().endsWith(".gcode.3mf")) {
|
|
477
|
-
if (!
|
|
495
|
+
if (!usesH2ProjectFile && !isA1) {
|
|
478
496
|
const printer = await this.getPrinter(host, serial, token);
|
|
479
497
|
await invokeWithoutAck(printer, new GCodeFileCommand({ fileName: remoteProjectPath }));
|
|
480
498
|
return {
|
|
@@ -483,7 +501,7 @@ export class BambuImplementation {
|
|
|
483
501
|
remoteProjectPath,
|
|
484
502
|
};
|
|
485
503
|
}
|
|
486
|
-
// H2-series: fall through to project_file path below
|
|
504
|
+
// H2-series and P2S: fall through to project_file path below
|
|
487
505
|
}
|
|
488
506
|
const projectMetadata = await this.resolveProjectFileMetadata(options.filePath, options.plateIndex);
|
|
489
507
|
// Send project_file command via bambu-node MQTT (bypasses bambu-js
|
|
@@ -550,7 +568,7 @@ export class BambuImplementation {
|
|
|
550
568
|
}
|
|
551
569
|
let amsMapping;
|
|
552
570
|
let amsMapping2;
|
|
553
|
-
if (
|
|
571
|
+
if (usesH2ProjectFile) {
|
|
554
572
|
const projLen = Math.max(projectMetadata.projectFilamentCount, baseMapping.length, 1);
|
|
555
573
|
amsMapping = Array.from({ length: projLen }, (_, i) => i < baseMapping.length ? baseMapping[i] : -1);
|
|
556
574
|
amsMapping2 = amsMapping.map((v) => {
|
|
@@ -564,12 +582,12 @@ export class BambuImplementation {
|
|
|
564
582
|
});
|
|
565
583
|
}
|
|
566
584
|
else {
|
|
567
|
-
amsMapping = Array.from({ length: 5 }, (_, i) => i < baseMapping.length ? baseMapping[i] : -1);
|
|
585
|
+
amsMapping = Array.from({ length: Math.max(5, baseMapping.length, projectMetadata.projectFilamentCount) }, (_, i) => i < baseMapping.length ? baseMapping[i] : -1);
|
|
568
586
|
amsMapping2 = [];
|
|
569
587
|
}
|
|
570
588
|
const b = (v) => (v ? 1 : 0);
|
|
571
589
|
let projectFileCmd;
|
|
572
|
-
if (
|
|
590
|
+
if (usesH2ProjectFile) {
|
|
573
591
|
const submissionId = String(Date.now() & 0x7fffffff);
|
|
574
592
|
projectFileCmd = {
|
|
575
593
|
print: {
|
|
@@ -994,7 +1012,7 @@ export class BambuImplementation {
|
|
|
994
1012
|
// integration's models.py shows the printer reports its own
|
|
995
1013
|
// `ipcam.rtsp_url` for these models, and Parker (H2S) rejects the
|
|
996
1014
|
// A1/P1 80-byte auth packet on port 6000 (verified 2026-04-27 --
|
|
997
|
-
//
|
|
1015
|
+
// confirmed by local H2 camera transport probes).
|
|
998
1016
|
const RTSP_MODELS = new Set([
|
|
999
1017
|
"x1", "x1c", "x1carbon", "x1e", "p2s",
|
|
1000
1018
|
"h2", "h2s", "h2d", "h2c", "h2dpro",
|
|
@@ -10,11 +10,13 @@
|
|
|
10
10
|
*
|
|
11
11
|
* This module:
|
|
12
12
|
* 1. Indexes every BBL profile JSON by its `name` field.
|
|
13
|
-
* 2. Recursively walks `inherits`, deep-merging parent into child
|
|
13
|
+
* 2. Recursively walks `inherits`, deep-merging parent into child, and
|
|
14
|
+
* applies each level's `include` templates (G-code templates for
|
|
15
|
+
* machines, per-variant defaults for filaments) the way the GUI does.
|
|
14
16
|
* 3. Derives `nozzle_volume_type` from `default_nozzle_volume_type[0]`
|
|
15
17
|
* (the GUI does this implicitly; the CLI doesn't).
|
|
16
|
-
* 4.
|
|
17
|
-
*
|
|
18
|
+
* 4. Validates the model in `BBL/cli_config.json` and merges its CLI-specific
|
|
19
|
+
* machine_limits where supplied for safe accelerations / jerks.
|
|
18
20
|
* 5. Writes the flattened JSON to a temp file the caller passes to
|
|
19
21
|
* BambuStudio CLI.
|
|
20
22
|
*
|
|
@@ -32,6 +34,9 @@ export interface FlattenedProfiles {
|
|
|
32
34
|
machineLeafName: string;
|
|
33
35
|
processLeafName: string;
|
|
34
36
|
filamentLeafNames: string[];
|
|
37
|
+
/** False only for an explicit standalone custom machine. */
|
|
38
|
+
cliConfigValidated: boolean;
|
|
39
|
+
/** Some validated official model configs intentionally have no limits. */
|
|
35
40
|
cliOverlayApplied: boolean;
|
|
36
41
|
};
|
|
37
42
|
}
|
|
@@ -59,6 +64,12 @@ export interface FlattenOptions {
|
|
|
59
64
|
nozzleVolumeType?: "Standard" | "High Flow";
|
|
60
65
|
/** BambuStudio display name, e.g. "Textured PEI Plate" or "Cool Plate". */
|
|
61
66
|
bedType?: string;
|
|
67
|
+
/** Actual input configs, including user overrides on top of BBL parents. */
|
|
68
|
+
sourceProfiles?: {
|
|
69
|
+
machine?: Record<string, unknown>;
|
|
70
|
+
process?: Record<string, unknown>;
|
|
71
|
+
filaments?: (Record<string, unknown> | undefined)[];
|
|
72
|
+
};
|
|
62
73
|
}
|
|
63
74
|
/**
|
|
64
75
|
* Flatten the leaf profiles, post-process for CLI, and write to temp files.
|
|
@@ -68,7 +79,7 @@ export interface FlattenOptions {
|
|
|
68
79
|
export declare function flattenForCli(opts: FlattenOptions): Promise<FlattenedProfiles>;
|
|
69
80
|
/**
|
|
70
81
|
* Given the SLICER_PATH (path to BambuStudio executable), walk up to the
|
|
71
|
-
*
|
|
82
|
+
* profile directory for that installation (macOS, Windows, or Linux prefix).
|
|
72
83
|
*
|
|
73
84
|
* Override via BAMBU_PROFILES_ROOT env.
|
|
74
85
|
*/
|