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.
Files changed (38) hide show
  1. package/CONTRIBUTORS.md +7 -1
  2. package/README.md +29 -10
  3. package/dist/ams-mapping.d.ts +18 -2
  4. package/dist/ams-mapping.js +36 -5
  5. package/dist/bambu-native.d.ts +55 -0
  6. package/dist/bambu-native.js +212 -13
  7. package/dist/index.js +154 -71
  8. package/dist/printers/bambu.d.ts +29 -2
  9. package/dist/printers/bambu.js +59 -28
  10. package/dist/safety/hidden-hms.d.ts +32 -0
  11. package/dist/safety/hidden-hms.js +405 -0
  12. package/dist/safety/limits.d.ts +6 -0
  13. package/dist/safety/limits.js +9 -0
  14. package/dist/safety/print-file.d.ts +2 -0
  15. package/dist/safety/print-file.js +83 -7
  16. package/dist/safety/printer-state.d.ts +11 -0
  17. package/dist/safety/printer-state.js +115 -22
  18. package/dist/slicer/profile-flatten.js +25 -0
  19. package/dist/slicer/remote-slicer.d.ts +12 -0
  20. package/dist/slicer/remote-slicer.js +240 -0
  21. package/dist/stl/stl-manipulator.d.ts +2 -1
  22. package/dist/stl/stl-manipulator.js +71 -42
  23. package/native/bambu-native-print.cpp +100 -26
  24. package/package.json +7 -5
  25. package/scripts/build-bambu-native.sh +53 -0
  26. package/scripts/setup-bambu-native.mjs +372 -0
  27. package/src/ams-mapping.ts +38 -5
  28. package/src/bambu-native.ts +229 -14
  29. package/src/index.ts +156 -67
  30. package/src/printers/bambu.ts +65 -25
  31. package/src/safety/hidden-hms.ts +406 -0
  32. package/src/safety/limits.ts +9 -0
  33. package/src/safety/print-file.ts +71 -7
  34. package/src/safety/printer-state.ts +117 -22
  35. package/src/slicer/profile-flatten.ts +32 -0
  36. package/src/slicer/remote-slicer.ts +216 -0
  37. package/src/stl/stl-manipulator.ts +70 -43
  38. package/scripts/build-bambu-native.zsh +0 -33
@@ -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
- return fs.statSync(candidate).isFile() && (fs.statSync(candidate).mode & 0o111) !== 0;
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
- if (process.platform !== "darwin") throw new Error("Bambu native X2D transport requires macOS and the installed Bambu Studio networking plug-in.");
83
- resolveNativeHelper();
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
- const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
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(packageRoot, "native/bambu-native-print"),
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
- "Bambu native helper is not installed. Build native/bambu-native-print with clang++ or set BAMBU_NATIVE_HELPER."
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
- async function interruptNativeOperations(serial: string): Promise<void> {
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
- assertBambuNativeAvailable();
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
  }