bambu-printer-mcp 1.1.27 → 1.1.29
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 +7 -1
- package/README.md +29 -10
- package/dist/ams-mapping.d.ts +18 -2
- package/dist/ams-mapping.js +36 -5
- package/dist/bambu-native.d.ts +55 -0
- package/dist/bambu-native.js +212 -13
- package/dist/index.js +154 -71
- package/dist/printers/bambu.d.ts +29 -2
- package/dist/printers/bambu.js +59 -28
- package/dist/safety/hidden-hms.d.ts +32 -0
- package/dist/safety/hidden-hms.js +405 -0
- package/dist/safety/limits.d.ts +6 -0
- package/dist/safety/limits.js +9 -0
- package/dist/safety/print-file.d.ts +2 -0
- package/dist/safety/print-file.js +83 -7
- package/dist/safety/printer-state.d.ts +11 -0
- package/dist/safety/printer-state.js +115 -22
- package/dist/slicer/profile-flatten.js +25 -0
- package/dist/slicer/remote-slicer.d.ts +12 -0
- package/dist/slicer/remote-slicer.js +240 -0
- package/dist/stl/stl-manipulator.d.ts +2 -1
- package/dist/stl/stl-manipulator.js +71 -42
- package/native/bambu-native-print.cpp +100 -26
- package/package.json +7 -5
- package/scripts/build-bambu-native.sh +53 -0
- package/scripts/setup-bambu-native.mjs +372 -0
- package/src/ams-mapping.ts +38 -5
- package/src/bambu-native.ts +229 -14
- package/src/index.ts +156 -67
- package/src/printers/bambu.ts +65 -25
- package/src/safety/hidden-hms.ts +406 -0
- package/src/safety/limits.ts +9 -0
- package/src/safety/print-file.ts +71 -7
- package/src/safety/printer-state.ts +117 -22
- package/src/slicer/profile-flatten.ts +32 -0
- package/src/slicer/remote-slicer.ts +216 -0
- package/src/stl/stl-manipulator.ts +70 -43
- package/scripts/build-bambu-native.zsh +0 -33
package/src/bambu-native.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { spawn, type ChildProcess } from "node:child_process";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
2
3
|
import fs from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
3
5
|
import path from "node:path";
|
|
4
6
|
import { fileURLToPath } from "node:url";
|
|
5
7
|
|
|
@@ -71,30 +73,240 @@ export type BambuNativeTemperatureCommand = {
|
|
|
71
73
|
function firstExistingExecutable(candidates: string[]): string | undefined {
|
|
72
74
|
return candidates.find((candidate) => {
|
|
73
75
|
try {
|
|
74
|
-
|
|
76
|
+
const stat = fs.statSync(candidate);
|
|
77
|
+
return stat.isFile() && (stat.mode & 0o111) !== 0;
|
|
75
78
|
} catch {
|
|
76
79
|
return false;
|
|
77
80
|
}
|
|
78
81
|
});
|
|
79
82
|
}
|
|
80
83
|
|
|
84
|
+
/**
|
|
85
|
+
* Hosts for which the native helper and Bambu's networking plug-in exist.
|
|
86
|
+
* Bambu publishes its Linux plug-in for x86-64 only.
|
|
87
|
+
*/
|
|
88
|
+
export function isBambuNativePlatform(
|
|
89
|
+
platform: NodeJS.Platform = process.platform,
|
|
90
|
+
arch: string = process.arch
|
|
91
|
+
): boolean {
|
|
92
|
+
return platform === "darwin" || (platform === "linux" && arch === "x64");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Private directory used by `bambu-printer-mcp-native-setup` on Linux. Mirrors
|
|
97
|
+
* nativeHomeDir() in native/bambu-native-print.cpp: empty values count as unset.
|
|
98
|
+
*/
|
|
99
|
+
export function bambuNativeHomeDir(env: NodeJS.ProcessEnv = process.env): string {
|
|
100
|
+
if (env.BAMBU_NATIVE_HOME) return env.BAMBU_NATIVE_HOME;
|
|
101
|
+
const configHome = env.XDG_CONFIG_HOME || path.join(env.HOME || os.homedir(), ".config");
|
|
102
|
+
return path.join(configHome, "bambu-printer-mcp", "bambu-native");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export const BAMBU_NATIVE_CERTIFICATES = ["slicer_base64.cer", "printer.cer"] as const;
|
|
106
|
+
/**
|
|
107
|
+
* The networking plug-in followed by the library it loads from its own
|
|
108
|
+
* directory for the eMMC file tunnel; without it uploads fail with -4.
|
|
109
|
+
*/
|
|
110
|
+
export const BAMBU_NATIVE_PLUGIN_LIBRARIES = ["libbambu_networking.so", "libBambuSource.so"] as const;
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* The only Linux plug-in build whose PrintParams layout matches the helper
|
|
114
|
+
* (Bambu Studio v02.08.02.61, network agent 02.08.02.54). The setup installs
|
|
115
|
+
* exactly these files and the server refuses anything else.
|
|
116
|
+
*/
|
|
117
|
+
export const BAMBU_NATIVE_PINNED_PLUGIN = Object.freeze({
|
|
118
|
+
version: "02.08.02.54",
|
|
119
|
+
sha256: Object.freeze({
|
|
120
|
+
"libbambu_networking.so": "bba49709c8dea14ba70251b43965646c0a5e2cde8fd133c64f764d425691bdf2",
|
|
121
|
+
"libBambuSource.so": "4e05f16fa02740fd12f0040b9efc3e70d43446c7a1a877c6a3926ef0758ca7db",
|
|
122
|
+
} as Record<(typeof BAMBU_NATIVE_PLUGIN_LIBRARIES)[number], string>),
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
/** Layout shared by the Linux setup script, the server and the helper's environment. */
|
|
126
|
+
export function bambuNativeManagedPaths(env: NodeJS.ProcessEnv = process.env) {
|
|
127
|
+
const home = bambuNativeHomeDir(env);
|
|
128
|
+
return {
|
|
129
|
+
home,
|
|
130
|
+
helper: path.join(home, "bin", "bambu-native-print"),
|
|
131
|
+
plugin: path.join(home, "plugins", BAMBU_NATIVE_PLUGIN_LIBRARIES[0]),
|
|
132
|
+
certDir: path.join(home, "cert"),
|
|
133
|
+
configDir: path.join(home, "config"),
|
|
134
|
+
receipt: path.join(home, "install.json"),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
139
|
+
export const BAMBU_NATIVE_SOURCE = path.join(PACKAGE_ROOT, "native", "bambu-native-print.cpp");
|
|
140
|
+
|
|
141
|
+
export function sha256Hex(data: Buffer | string): string {
|
|
142
|
+
return createHash("sha256").update(data).digest("hex");
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function sha256File(file: string): string {
|
|
146
|
+
return sha256Hex(fs.readFileSync(file));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
let nativeSourceSha256: string | undefined;
|
|
150
|
+
/** The packaged helper source never changes while the server runs. */
|
|
151
|
+
function packagedHelperSha256(): string {
|
|
152
|
+
nativeSourceSha256 ??= sha256File(BAMBU_NATIVE_SOURCE);
|
|
153
|
+
return nativeSourceSha256;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function packageVersion(): string {
|
|
157
|
+
try {
|
|
158
|
+
return String(JSON.parse(fs.readFileSync(path.join(PACKAGE_ROOT, "package.json"), "utf8")).version || "");
|
|
159
|
+
} catch {
|
|
160
|
+
return "";
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Name the setup from this server's own version, whose helper source it
|
|
166
|
+
* accepts, and its own install directory when that is not the default.
|
|
167
|
+
*/
|
|
168
|
+
export function bambuNativeSetupCommand(version = packageVersion(), env: NodeJS.ProcessEnv = process.env): string {
|
|
169
|
+
const home = bambuNativeHomeDir(env);
|
|
170
|
+
const defaultHome = bambuNativeHomeDir({ HOME: env.HOME });
|
|
171
|
+
const homeOption = home === defaultHome ? "" : ` --home ${JSON.stringify(home)}`;
|
|
172
|
+
return `npx -y -p bambu-printer-mcp${version ? `@${version}` : ""} bambu-printer-mcp-native-setup${homeOption}`;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const linuxSetupHint = () =>
|
|
176
|
+
`Run \`${bambuNativeSetupCommand()}\` to build the helper and install the pinned networking plug-in.`;
|
|
177
|
+
|
|
178
|
+
type NativeInstall = { helper: string; env: Record<string, string> };
|
|
179
|
+
|
|
180
|
+
const verifiedLibraries = new Map<string, string>();
|
|
181
|
+
/** Hash a plug-in library once per file version; it is ~16 MB. */
|
|
182
|
+
function libraryMatches(file: string, expected: string): boolean {
|
|
183
|
+
const stat = fs.statSync(file);
|
|
184
|
+
const key = `${fs.realpathSync(file)}\n${stat.dev}:${stat.ino}:${stat.size}:${stat.mtimeMs}`;
|
|
185
|
+
let digest = verifiedLibraries.get(key);
|
|
186
|
+
if (!digest) {
|
|
187
|
+
digest = sha256File(file);
|
|
188
|
+
verifiedLibraries.set(key, digest);
|
|
189
|
+
}
|
|
190
|
+
return digest === expected;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function samePath(a: string, b: string): boolean {
|
|
194
|
+
try {
|
|
195
|
+
return fs.realpathSync(a) === fs.realpathSync(b);
|
|
196
|
+
} catch {
|
|
197
|
+
return path.resolve(a) === path.resolve(b);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function isRegularFile(file: string): boolean {
|
|
202
|
+
try {
|
|
203
|
+
return fs.statSync(file).isFile();
|
|
204
|
+
} catch {
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Resolve a complete native installation. On Linux the helper, the pinned
|
|
211
|
+
* plug-in and both certificates must be present before any native route is
|
|
212
|
+
* chosen, and their resolved paths are handed to the helper so server and
|
|
213
|
+
* helper can never disagree about which plug-in is loaded.
|
|
214
|
+
*/
|
|
215
|
+
function resolveNativeInstall(): NativeInstall {
|
|
216
|
+
if (!isBambuNativePlatform()) {
|
|
217
|
+
throw new Error("Bambu native X2D transport requires macOS or x86-64 Linux and Bambu's networking plug-in.");
|
|
218
|
+
}
|
|
219
|
+
const helper = resolveNativeHelper();
|
|
220
|
+
if (process.platform !== "linux") return { helper, env: {} };
|
|
221
|
+
|
|
222
|
+
const managed = bambuNativeManagedPaths();
|
|
223
|
+
const plugin = process.env.BAMBU_NATIVE_PLUGIN || managed.plugin;
|
|
224
|
+
const certDir = process.env.BAMBU_NATIVE_CERT_DIR || managed.certDir;
|
|
225
|
+
const configDir = process.env.BAMBU_NATIVE_CONFIG_DIR || managed.configDir;
|
|
226
|
+
const certFile = process.env.BAMBU_NATIVE_CERT_FILE || BAMBU_NATIVE_CERTIFICATES[0];
|
|
227
|
+
if (!isRegularFile(plugin)) {
|
|
228
|
+
throw new Error(`Bambu networking plug-in is not installed at ${plugin}. ${linuxSetupHint()}`);
|
|
229
|
+
}
|
|
230
|
+
for (const library of BAMBU_NATIVE_PLUGIN_LIBRARIES.slice(1)) {
|
|
231
|
+
if (!isRegularFile(path.join(path.dirname(plugin), library))) {
|
|
232
|
+
throw new Error(`Bambu networking library ${library} is missing beside ${plugin}. ${linuxSetupHint()}`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
// A different plug-in build can change the PrintParams layout passed by
|
|
236
|
+
// value to start_local_print. Only an explicit trusted opt-in skips this.
|
|
237
|
+
if (process.env.BAMBU_NATIVE_ALLOW_UNPINNED_PLUGIN !== "1") {
|
|
238
|
+
const files: Array<[string, string]> = [
|
|
239
|
+
[plugin, BAMBU_NATIVE_PINNED_PLUGIN.sha256[BAMBU_NATIVE_PLUGIN_LIBRARIES[0]]],
|
|
240
|
+
...BAMBU_NATIVE_PLUGIN_LIBRARIES.slice(1).map(library =>
|
|
241
|
+
[path.join(path.dirname(plugin), library), BAMBU_NATIVE_PINNED_PLUGIN.sha256[library]] as [string, string]),
|
|
242
|
+
];
|
|
243
|
+
for (const [file, expected] of files) {
|
|
244
|
+
if (!libraryMatches(file, expected)) {
|
|
245
|
+
throw new Error(`${file} is not the pinned Bambu plug-in ${BAMBU_NATIVE_PINNED_PLUGIN.version}. ${linuxSetupHint()}`);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
for (const certificate of new Set([certFile, ...BAMBU_NATIVE_CERTIFICATES.slice(1)])) {
|
|
250
|
+
if (!isRegularFile(path.join(certDir, certificate))) {
|
|
251
|
+
throw new Error(`Bambu certificate ${certificate} is missing from ${certDir}. ${linuxSetupHint()}`);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
if (samePath(helper, managed.helper)) {
|
|
255
|
+
// A setup run from another package version builds a different helper.
|
|
256
|
+
let built: unknown;
|
|
257
|
+
try {
|
|
258
|
+
built = JSON.parse(fs.readFileSync(managed.receipt, "utf8")).helperSourceSha256;
|
|
259
|
+
} catch {
|
|
260
|
+
built = undefined;
|
|
261
|
+
}
|
|
262
|
+
if (built !== packagedHelperSha256()) {
|
|
263
|
+
throw new Error(`The installed native helper was built from a different bambu-printer-mcp version. ${linuxSetupHint()}`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return {
|
|
267
|
+
helper,
|
|
268
|
+
env: { BAMBU_NATIVE_PLUGIN: plugin, BAMBU_NATIVE_CERT_DIR: certDir, BAMBU_NATIVE_CONFIG_DIR: configDir },
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
81
272
|
export function assertBambuNativeAvailable(): void {
|
|
82
|
-
|
|
83
|
-
|
|
273
|
+
resolveNativeInstall();
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* True when X2D controls should use the native route. macOS keeps its
|
|
278
|
+
* unconditional routing. Linux opts in only with a complete installation, so
|
|
279
|
+
* stop, pause and heater-off keep their MQTT route otherwise.
|
|
280
|
+
*/
|
|
281
|
+
export function bambuNativeControlsAvailable(): boolean {
|
|
282
|
+
return bambuNativeUnavailableReason() === undefined;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** Why native X2D controls are not used on this host, or undefined when they are. */
|
|
286
|
+
export function bambuNativeUnavailableReason(): string | undefined {
|
|
287
|
+
if (process.platform === "darwin") return undefined;
|
|
288
|
+
if (process.platform !== "linux") return "Native X2D controls require macOS or x86-64 Linux.";
|
|
289
|
+
try {
|
|
290
|
+
resolveNativeInstall();
|
|
291
|
+
return undefined;
|
|
292
|
+
} catch (error) {
|
|
293
|
+
return error instanceof Error ? error.message : String(error);
|
|
294
|
+
}
|
|
84
295
|
}
|
|
85
296
|
|
|
86
297
|
function resolveNativeHelper(): string {
|
|
87
298
|
const configured = process.env.BAMBU_NATIVE_HELPER?.trim();
|
|
88
|
-
|
|
299
|
+
// Linux uses only the setup-managed helper, whose receipt proves it matches
|
|
300
|
+
// this package; a package-local build could be stale after an update.
|
|
89
301
|
const candidates = [
|
|
90
302
|
configured,
|
|
91
|
-
path.resolve(
|
|
303
|
+
process.platform === "linux" ? bambuNativeManagedPaths().helper : path.resolve(PACKAGE_ROOT, "native/bambu-native-print"),
|
|
92
304
|
].filter((value): value is string => Boolean(value));
|
|
93
305
|
const helper = firstExistingExecutable(candidates);
|
|
94
306
|
if (!helper) {
|
|
95
|
-
throw new Error(
|
|
96
|
-
|
|
97
|
-
|
|
307
|
+
throw new Error(process.platform === "linux"
|
|
308
|
+
? `Bambu native helper is not installed. ${linuxSetupHint()} Alternatively set BAMBU_NATIVE_HELPER.`
|
|
309
|
+
: "Bambu native helper is not installed. Build native/bambu-native-print with clang++ or set BAMBU_NATIVE_HELPER.");
|
|
98
310
|
}
|
|
99
311
|
return helper;
|
|
100
312
|
}
|
|
@@ -121,7 +333,11 @@ function isEmergencyCommand(messageJson: string): boolean {
|
|
|
121
333
|
(command?.command === "set_nozzle_temp" && command.target_temp === 0);
|
|
122
334
|
}
|
|
123
335
|
|
|
124
|
-
|
|
336
|
+
/**
|
|
337
|
+
* Interrupt this printer's pending print, upload and heating helpers and
|
|
338
|
+
* resolve once they have exited, so none can still dispatch afterwards.
|
|
339
|
+
*/
|
|
340
|
+
export async function interruptNativeOperations(serial: string): Promise<void> {
|
|
125
341
|
const pending = [...activeHelpers.values()].filter(helper => helper.serial === serial && !helper.emergency);
|
|
126
342
|
for (const helper of pending) helper.interrupt();
|
|
127
343
|
// Send stop/heater-off only after an older helper can no longer send a
|
|
@@ -142,11 +358,10 @@ function runNativeHelper(
|
|
|
142
358
|
if (needsAuthorization && typeof execution.beforeDispatch !== "function") {
|
|
143
359
|
throw new Error("Native dispatch requires a beforeDispatch fresh-state check.");
|
|
144
360
|
}
|
|
145
|
-
|
|
146
|
-
const helper = resolveNativeHelper();
|
|
361
|
+
const install = resolveNativeInstall();
|
|
147
362
|
return new Promise((resolve, reject) => {
|
|
148
|
-
const child = spawn(helper, [mode], {
|
|
149
|
-
env,
|
|
363
|
+
const child = spawn(install.helper, [mode], {
|
|
364
|
+
env: { ...env, ...install.env },
|
|
150
365
|
stdio: ["pipe", "pipe", "pipe"],
|
|
151
366
|
});
|
|
152
367
|
let markClosed!: () => void;
|
|
@@ -608,7 +823,7 @@ export async function probeBambuNative(host: string, token: string): Promise<Rec
|
|
|
608
823
|
20_000
|
|
609
824
|
);
|
|
610
825
|
if (result.resultCode !== 0) {
|
|
611
|
-
throw new Error(`Bambu native local tunnel probe failed (${result.resultCode})
|
|
826
|
+
throw new Error(`Bambu native local tunnel probe failed (${result.resultCode}): ${result.updates.join(" | ") || tail(result.stderr, 500)}`);
|
|
612
827
|
}
|
|
613
828
|
return { status: "ok", route: "bambu:///local", updates: result.updates };
|
|
614
829
|
}
|