bambu-printer-mcp 1.1.33 → 1.1.34

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 CHANGED
@@ -6,6 +6,8 @@ Thank you to everyone who builds, tests, reports problems, and shares real print
6
6
 
7
7
  | Contributor | Contribution |
8
8
  | --- | --- |
9
+ | [MorganFreeeman](https://github.com/MorganFreeeman) | Reports in [#67](https://github.com/DMontgomery40/bambu-printer-mcp/issues/67) that every H2D print stopped on stock HS01 nozzles. Supplies idle and mid-print firmware 01.04.00.00 nozzle reports showing a permanent `stat: 1` with `sn: "N/A"`, proposes keeping the check narrow to nozzles without an identity chip, and points out the misleading declined-confirmation advice and the missing settings-table entries. Maintainer validation uses their reports in regressions; no H2D hardware was available. |
10
+ | [Aaron Roberts (aaroberts)](https://github.com/aaroberts) | Diagnoses FULU's Linux host exit 101 and its working-directory library loading in [#68](https://github.com/DMontgomery40/bambu-printer-mcp/issues/68), reports both upstream as [FULU #50](https://github.com/FULU-Foundation/OrcaSlicer-bambulab/issues/50) and [#51](https://github.com/FULU-Foundation/OrcaSlicer-bambulab/issues/51), and shares a working configuration: the `cat` wrapper, absolute plug-in paths, the FULU GUI's session file for cloud login, and the required 02.05.02.5x plug-in. Reports cloud printing on two P1S printers with 1.1.33. The server now sets the library paths and explains exit 101. The host fixes themselves remain with FULU. |
9
11
  | [bwees](https://github.com/bwees) | Reports and fixes missing non-preset vendor data in [OrcaSlicer #16700](https://github.com/OrcaSlicer/OrcaSlicer/issues/16700) and [#16706](https://github.com/OrcaSlicer/OrcaSlicer/pull/16706), restoring the authoritative BBL CLI matrix required by cache-based MCP slicing. |
10
12
  | [ExPikaPaka](https://github.com/ExPikaPaka), [raistlin7447](https://github.com/raistlin7447), [HanifKoh](https://github.com/HanifKoh), and [SoftFever](https://github.com/SoftFever) | Provide and maintain OrcaSlicer's published preset-cache format, source-profile inheritance and include handling used as the contract for this independent cache reader. Credit covers upstream source and format evidence; it does not claim they tested this MCP integration. |
11
13
  | [mygrexit](https://github.com/mygrexit) | Contributes pinned x86-64 Linux X2D native setup, emergency routing and one-shot cleanup in [#56](https://github.com/DMontgomery40/bambu-printer-mcp/pull/56), checked Orca/time-lapse G-code forms in [#57](https://github.com/DMontgomery40/bambu-printer-mcp/pull/57), and model-specific hidden-HMS notices in [#58](https://github.com/DMontgomery40/bambu-printer-mcp/pull/58). Supplies regressions, review corrections, and contributor-reported Debian/X2D read-only, heater-off and print-start evidence in [#55](https://github.com/DMontgomery40/bambu-printer-mcp/issues/55); maintainer validation is offline and mocked. Also reports and fixes hanging Linux X2D stop/heater-off calls in [#61](https://github.com/DMontgomery40/bambu-printer-mcp/pull/61), with never-connecting transport and helper-exit regressions; the release adds protection against expired commands dispatching after reconnection. In [#64](https://github.com/DMontgomery40/bambu-printer-mcp/pull/64), contributes ready-card native job storage, fresh dispatch rechecks and tests, and reports Linux X2D firmware 01.01.01.00 printing to external storage with plug-in 02.08.02.54 and a Bambuddy thumbnail; maintainer verification uses source and mocked transports. |
@@ -37,7 +39,7 @@ Thank you to everyone who builds, tests, reports problems, and shares real print
37
39
 
38
40
  Thank you also to the existing contributors whose work the project builds on:
39
41
 
40
- - [David Montgomery (DMontgomery40)](https://github.com/DMontgomery40) — project maintainer; contributes the printer-profile, temperature, and hardware-safety reports informing the shared slicing and print checks.
42
+ - [David Montgomery (DMontgomery40)](https://github.com/DMontgomery40) — project maintainer; contributes the printer-profile, temperature, and hardware-safety reports informing the shared slicing and print checks. For 1.1.34, reports the stale A1/P1 camera snapshots and that FULU bridge LAN printing needs no Developer Mode, asks which OrcaSlicer CLI option combinations crash, and supplies the P1S used for the real FULU bridge prints.
41
43
  - [rowbotik](https://github.com/rowbotik) — printer, AMS, slicing, and operational work across the existing release history.
42
44
  - [len-foss](https://github.com/len-foss) — project code contribution.
43
45
  - [thebitrock](https://github.com/thebitrock) — project code contribution.
package/README.md CHANGED
@@ -179,7 +179,7 @@ See the [changelog](https://github.com/DMontgomery40/bambu-printer-mcp/blob/main
179
179
 
180
180
  [FULU OrcaSlicer-bambulab](https://github.com/FULU-Foundation/OrcaSlicer-bambulab) is a supported slicer target (`SLICER_TYPE=orcaslicer-bambulab`; aliases include `fulu-orca` and `orca-studio`). Use its GUI to export a sliced project for direct LAN printing, or configure its CLI with matching installed profiles. From 1.1.11, FULU/Orca share the [machine-preset gate and profile safety checks](https://github.com/DMontgomery40/bambu-printer-mcp/blob/main/docs/FULU.md#fulu-and-orca-cli-safety-limit).
181
181
 
182
- The optional FULU **BambuNetwork bridge** exposes `bambu_network_bridge_status`, `bambu_network_call`, and `print_3mf_bambu_network`, also reachable through `print_3mf` with `connection_mode: "bambu_network"`. Slicer selection does not enable the bridge. It needs a separately installed FULU runtime and an explicit launch command; cloud printing also needs an authenticated BambuNetwork session.
182
+ The optional FULU **BambuNetwork bridge** exposes `bambu_network_bridge_status`, `bambu_network_call`, and `print_3mf_bambu_network`, also reachable through `print_3mf` with `connection_mode: "bambu_network"`. Slicer selection does not enable the bridge. It needs a separately installed FULU runtime and an explicit launch command; cloud printing also needs an authenticated BambuNetwork session. On Linux, FULU's v1.0.0 host needs a small `cat` wrapper and the 02.05.02.5x plug-in; the guide covers both. LAN bridge prints open the plug-in's own LAN session with Bambu's printer certificate, which the server supplies.
183
183
 
184
184
  **[Follow the FULU setup guide](https://github.com/DMontgomery40/bambu-printer-mcp/blob/main/docs/FULU.md)** for the direct LAN recipe, Linux/Windows/macOS bridge setup, connection probes, authentication, and troubleshooting. Bridge protocol tests and a successful handshake do not establish a successful physical print.
185
185
 
@@ -623,7 +623,7 @@ Capture a single JPEG frame from the printer's chamber camera. Read-only.
623
623
 
624
624
  Two transports are wired in, picked by `bambu_model`:
625
625
 
626
- - **TCP-on-6000** for **A1, A1 mini, P1S, P1P**. Native protocol per [OpenBambuAPI/video.md](https://github.com/Doridian/OpenBambuAPI/blob/main/video.md): TLS on port 6000, 80-byte auth packet (`bblp` + access token), repeating 16-byte frame header + JPEG payload.
626
+ - **TCP-on-6000** for **A1, A1 mini, P1S, P1P**. Native protocol per [OpenBambuAPI/video.md](https://github.com/Doridian/OpenBambuAPI/blob/main/video.md): TLS on port 6000, 80-byte auth packet (`bblp` + access token), repeating 16-byte frame header + JPEG payload. The printer first replays the last frame it buffered, which after an idle period can show the chamber as it was long ago, so the tool skips that frame and returns the next live one (about 3 s). If no live frame follows within `timeout_ms`, it fails instead of returning a possibly stale image.
627
627
  - **RTSP** for **X1, X1 Carbon, X1E, P2S, X2D** and **H2, H2S, H2D, H2C, H2D Pro**. Shells out to ffmpeg with `rtsps://bblp:<token>@<host>:322/streaming/live/1 -frames:v 1`. The H2 series wasn't documented in OpenBambuAPI's `video.md` but its firmware uses the same RTSP endpoint as X1 (verified live against an H2S, 2026-04-27).
628
628
 
629
629
  **Requires ffmpeg in PATH** for the RTSP path. Install with `brew install ffmpeg` on macOS. Configure a trusted custom binary with the server-side `FFMPEG_PATH` environment variable, or set `MCP_ALLOW_EXECUTABLE_ARG=1` before using the `ffmpeg_path` tool argument. The TCP-on-6000 path uses native Node TLS and does not require ffmpeg.
@@ -1115,6 +1115,8 @@ Slice an STL or 3MF file using an external slicer and return the path to the out
1115
1115
 
1116
1116
  **FULU/Orca CLI safety:** from 1.1.11, these backends require the exact model/nozzle preset from the selected installation and stop on profile preparation failures. A process override cannot bypass that gate. See [configuration and validation limits](https://github.com/DMontgomery40/bambu-printer-mcp/blob/main/docs/FULU.md#fulu-and-orca-cli-safety-limit).
1117
1117
 
1118
+ **OrcaSlicer CLI transforms:** OrcaSlicer 2.4.2's CLI segfaults when `scale` or `rotate_x`/`rotate_y` moves an object below the bed while arranging, which is the default for STL input. For STL input with `orcaslicer` or `orcaslicer-bambulab`, the server applies `scale` and the rotations to a copy of the mesh in the CLI's order and leaves those flags out. For 3MF input it refuses `scale` above 1, `rotate_x` or `rotate_y` together with `arrange: true`. `repetitions` copies an STL with Orca's `--clone-objects`. `clone_objects` takes one total count, because the CLI counts copies per loaded file. With `arrange: false`, an STL that is not already on the plate fails to slice. A failure names the slicer's own reason.
1119
+
1118
1120
  `slicer_path` and `slicer_profile` fall back to the `SLICER_PATH` and `SLICER_PROFILE` environment variables when omitted. Per-call `slicer_path` overrides require `MCP_ALLOW_EXECUTABLE_ARG=1`.
1119
1121
 
1120
1122
  You can provide either `template_3mf_path` or `template_name` when you want to slice from a saved template. `template_name` resolves through the local template registry directory configured for the server.
@@ -1,3 +1,29 @@
1
+ /** A parenthesized explanation of a plug-in print result, or "" when it names no known cause. */
2
+ export declare function describeBambuNetworkPrintError(value: number): string;
3
+ /** Waits from native/bambu-native-print.cpp, which prints through the same plug-in calls. */
4
+ export declare const LOCAL_PRINT_TIMING: Readonly<{
5
+ connectTimeoutMs: 30000;
6
+ pollMs: 250;
7
+ settleMs: 3000;
8
+ retryWaitMs: 8000;
9
+ }>;
10
+ export type LocalPrinter = {
11
+ devId: string;
12
+ devIp: string;
13
+ username: string;
14
+ password: string;
15
+ useSsl: boolean;
16
+ };
17
+ export type BridgeJobUpdate = {
18
+ status: unknown;
19
+ code: unknown;
20
+ msg: unknown;
21
+ };
22
+ export type LocalSession = {
23
+ installCertificate: () => Promise<void>;
24
+ settle: (ms: number) => Promise<void>;
25
+ jobUpdates: BridgeJobUpdate[];
26
+ };
1
27
  type JsonObject = Record<string, unknown>;
2
28
  export type BambuNetworkBridgeOptions = {
3
29
  bridgeCommand?: string;
@@ -22,17 +48,37 @@ export type BambuNetworkBridgeStatus = {
22
48
  lastStderr: string;
23
49
  recentLogs: string[];
24
50
  runtime: {
25
- macosRuntimeDir: string;
26
- macosRuntimeDirExists: boolean;
51
+ /** Folder passed to the plug-in's set_cert_file. */
52
+ certDir: string;
53
+ /** Linux only: absolute plug-in library paths passed to FULU's host. */
54
+ pluginLibraries?: Record<string, string>;
55
+ /** macOS only: conventional FULU runtime discovery. */
56
+ macosRuntimeDir?: string;
57
+ macosRuntimeDirExists?: boolean;
27
58
  macosPluginDir?: string;
28
59
  macosPluginDirExists?: boolean;
29
- macosMissingRuntimeFiles: string[];
30
- macosMissingPluginFiles: string[];
60
+ macosMissingRuntimeFiles?: string[];
61
+ macosMissingPluginFiles?: string[];
31
62
  suggestedMacCommand?: string;
32
63
  };
33
64
  note?: string;
34
65
  };
66
+ /**
67
+ * The plug-in's certificate folder: BAMBU_NETWORK_CERT_DIR when set, else
68
+ * Bambu's pinned public certificates written to the agent's config folder,
69
+ * which the host must already be able to read.
70
+ */
71
+ export declare function bambuNetworkCertDir(configDir: string, env?: NodeJS.ProcessEnv): string;
72
+ export declare function prepareBambuNetworkCertDir(configDir: string, env?: NodeJS.ProcessEnv): string;
35
73
  export declare function defaultBambuNetworkConfigDir(): string;
74
+ /**
75
+ * FULU's Linux host opens its network and source libraries by bare name from
76
+ * its working directory instead of PJARCZAK_BAMBU_PLUGIN_DIR
77
+ * (FULU-Foundation/OrcaSlicer-bambulab#51), so name them in that directory
78
+ * unless they are already set. macOS wrappers run the host in a Linux guest
79
+ * that may not see host paths, so they are left alone.
80
+ */
81
+ export declare function linuxPluginLibraryEnv(env: NodeJS.ProcessEnv, platform: NodeJS.Platform): Record<string, string>;
36
82
  export declare class BambuNetworkBridge {
37
83
  private child?;
38
84
  private commandLine?;
@@ -45,6 +91,13 @@ export declare class BambuNetworkBridge {
45
91
  private lastStderr;
46
92
  private recentLogs;
47
93
  private inferredExpectedNetworkVersion?;
94
+ /** Whether the host accepted the certificate folder and callbacks a LAN session needs. */
95
+ private localSessionReady;
96
+ /**
97
+ * The plug-in holds one LAN printer connection and the host one event queue,
98
+ * so LAN sessions run one at a time, as Bambu Studio connects one printer.
99
+ */
100
+ private localSessionTail;
48
101
  resolveBridgeCommand(override?: string): string;
49
102
  resolveConfigDir(override?: string): string;
50
103
  resolveCountryCode(override?: string): string;
@@ -54,6 +107,27 @@ export declare class BambuNetworkBridge {
54
107
  agent: number;
55
108
  handshake: unknown;
56
109
  }>;
110
+ /**
111
+ * Open the plug-in's own LAN MQTT session with a printer, in the order Bambu
112
+ * Studio and native/bambu-native-print.cpp use: connect, install the
113
+ * printer's application certificate (again on each "wait_info"), wait for
114
+ * the local connection, request the printer's state and let the key
115
+ * exchange settle. Without the certificate the plug-in uploads a job but
116
+ * cannot encrypt or publish project_file (-4030). The session is closed
117
+ * after `run`, so printer reports stop accumulating in the host's queue.
118
+ */
119
+ withLocalSession<T>(printer: LocalPrinter, options: BambuNetworkBridgeOptions, assertActive: () => void, timing: typeof LOCAL_PRINT_TIMING, run: (session: LocalSession) => Promise<T>): Promise<T>;
120
+ private runLocalSession;
121
+ /**
122
+ * Run a LAN print method in a local session. A synchronous -4030 means the
123
+ * plug-in published nothing, so it is retried once after the certificate
124
+ * exchange, as the native helper does; each attempt passes the caller's
125
+ * fresh pre-dispatch checks.
126
+ */
127
+ startLocalPrint(printer: LocalPrinter, method: string, payload: JsonObject, options?: BambuNetworkBridgeOptions, assertActive?: () => void, timing?: typeof LOCAL_PRINT_TIMING): Promise<{
128
+ result: unknown;
129
+ jobUpdates: BridgeJobUpdate[];
130
+ }>;
57
131
  callWithAgent(method: string, payload?: JsonObject, options?: BambuNetworkBridgeOptions): Promise<unknown>;
58
132
  stop(): Promise<void>;
59
133
  private ensureStarted;
@@ -1,7 +1,9 @@
1
1
  import { spawn } from "node:child_process";
2
+ import { createHash } from "node:crypto";
2
3
  import fs from "node:fs";
3
4
  import os from "node:os";
4
5
  import path from "node:path";
6
+ import { BAMBU_NETWORK_CERTIFICATES } from "./bambu-network-certs.js";
5
7
  const FRAME_MAGIC = 0x52424a50;
6
8
  const FRAME_HEADER_SIZE = 16;
7
9
  const FRAME_TYPE_JSON_REQUEST = 1;
@@ -9,6 +11,45 @@ const FRAME_TYPE_JSON_RESPONSE = 2;
9
11
  const FRAME_TYPE_BINARY_DATA = 3;
10
12
  const FRAME_TYPE_LOG = 4;
11
13
  const DEFAULT_REQUEST_TIMEOUT_MS = 120000;
14
+ // bambu_networking.hpp ConnectStatus.
15
+ const LOCAL_CONNECT_STATUS = ["ok", "failed", "lost"];
16
+ // bambu_networking.hpp BAMBU_NETWORK_ERR_PRINT_LP_PUBLISH_MSG_FAILED.
17
+ const LOCAL_PRINT_PUBLISH_FAILED = -4030;
18
+ // bambu_networking.hpp print errors that name a recoverable cause.
19
+ const BAMBU_NETWORK_PRINT_ERRORS = {
20
+ [-4010]: "the file exceeds the 1 GB upload limit",
21
+ [-4020]: "FTPS upload to the printer failed",
22
+ [LOCAL_PRINT_PUBLISH_FAILED]: "the networking plug-in could not send the print command to the printer over MQTT",
23
+ };
24
+ /** A parenthesized explanation of a plug-in print result, or "" when it names no known cause. */
25
+ export function describeBambuNetworkPrintError(value) {
26
+ return BAMBU_NETWORK_PRINT_ERRORS[value] ? ` (${BAMBU_NETWORK_PRINT_ERRORS[value]})` : "";
27
+ }
28
+ /** Waits from native/bambu-native-print.cpp, which prints through the same plug-in calls. */
29
+ export const LOCAL_PRINT_TIMING = Object.freeze({ connectTimeoutMs: 30000, pollMs: 250, settleMs: 3000, retryWaitMs: 8000 });
30
+ /**
31
+ * The plug-in's certificate folder: BAMBU_NETWORK_CERT_DIR when set, else
32
+ * Bambu's pinned public certificates written to the agent's config folder,
33
+ * which the host must already be able to read.
34
+ */
35
+ export function bambuNetworkCertDir(configDir, env = process.env) {
36
+ return trimEnv(env.BAMBU_NETWORK_CERT_DIR) || path.join(configDir, "cert");
37
+ }
38
+ export function prepareBambuNetworkCertDir(configDir, env = process.env) {
39
+ const certDir = bambuNetworkCertDir(configDir, env);
40
+ if (trimEnv(env.BAMBU_NETWORK_CERT_DIR))
41
+ return certDir;
42
+ fs.mkdirSync(certDir, { recursive: true });
43
+ for (const [name, { sha256, base64 }] of Object.entries(BAMBU_NETWORK_CERTIFICATES)) {
44
+ const content = Buffer.from(base64, "base64");
45
+ if (createHash("sha256").update(content).digest("hex") !== sha256)
46
+ throw new Error(`Bundled Bambu certificate ${name} does not match its pinned SHA-256.`);
47
+ const target = path.join(certDir, name);
48
+ if (!fileExists(target) || !fs.readFileSync(target).equals(content))
49
+ fs.writeFileSync(target, content);
50
+ }
51
+ return certDir;
52
+ }
12
53
  export function defaultBambuNetworkConfigDir() {
13
54
  return path.join(os.homedir(), ".config", "bambu-printer-mcp", "bambu-network");
14
55
  }
@@ -40,6 +81,72 @@ function collectMissingFiles(baseDir, files) {
40
81
  return files;
41
82
  return files.filter((file) => !fileExists(path.join(baseDir, file)));
42
83
  }
84
+ /**
85
+ * FULU's Linux host opens its network and source libraries by bare name from
86
+ * its working directory instead of PJARCZAK_BAMBU_PLUGIN_DIR
87
+ * (FULU-Foundation/OrcaSlicer-bambulab#51), so name them in that directory
88
+ * unless they are already set. macOS wrappers run the host in a Linux guest
89
+ * that may not see host paths, so they are left alone.
90
+ */
91
+ export function linuxPluginLibraryEnv(env, platform) {
92
+ const pluginDir = trimEnv(env.PJARCZAK_BAMBU_PLUGIN_DIR);
93
+ if (platform !== "linux" || !pluginDir)
94
+ return {};
95
+ const libraries = {};
96
+ if (!trimEnv(env.PJARCZAK_BAMBU_NETWORK_SO))
97
+ libraries.PJARCZAK_BAMBU_NETWORK_SO = path.resolve(pluginDir, "libbambu_networking.so");
98
+ if (!trimEnv(env.PJARCZAK_BAMBU_SOURCE_SO))
99
+ libraries.PJARCZAK_BAMBU_SOURCE_SO = path.resolve(pluginDir, "libBambuSource.so");
100
+ return libraries;
101
+ }
102
+ /** FULU's Linux host exits 101 when it cannot reopen a socket stdout (FULU-Foundation/OrcaSlicer-bambulab#50). */
103
+ const STDOUT_SOCKET_HINT = "Exit code 101 means FULU's Linux host could not reopen its stdout, which fails when stdout is a socket, as Node.js provides " +
104
+ "(https://github.com/FULU-Foundation/OrcaSlicer-bambulab/issues/50). Launch the host through a wrapper that pipes its output through cat: " +
105
+ "https://github.com/DMontgomery40/bambu-printer-mcp/blob/main/docs/FULU.md#linux";
106
+ /** Conventional FULU macOS runtime locations; the wrapper command is suggested only when it exists. */
107
+ function macosRuntimeHints() {
108
+ const macosRuntimeDir = trimEnv(process.env.PJARCZAK_MAC_RUNTIME_DIR) ||
109
+ path.join(os.homedir(), "Library", "Application Support", "OrcaSlicer", "macos-bridge", "runtime");
110
+ const macosPluginDir = trimEnv(process.env.BAMBU_NETWORK_PLUGIN_DIR) ||
111
+ trimEnv(process.env.PJARCZAK_BAMBU_PLUGIN_DIR) ||
112
+ resolveFirstDirectoryWithFile([
113
+ path.join(os.homedir(), "Library", "Application Support", "OrcaSlicer", "plugins"),
114
+ "/Applications/Orca Studio.app/Contents/MacOS",
115
+ "/Applications/OrcaSlicer.app/Contents/MacOS",
116
+ "/Applications/OrcaSlicer-BMCU.app/Contents/MacOS",
117
+ "/Applications/Orca Studio.app/Contents/Resources",
118
+ "/Applications/OrcaSlicer.app/Contents/Resources",
119
+ "/Applications/OrcaSlicer-BMCU.app/Contents/Resources",
120
+ macosRuntimeDir,
121
+ ], "pjarczak-bambu-linux-host-wrapper") ||
122
+ resolveFirstExistingDirectory([macosRuntimeDir]);
123
+ const runtimeRequiredFiles = [
124
+ "libbambu_networking.so",
125
+ "libBambuSource.so",
126
+ "pjarczak_bambu_linux_host",
127
+ "pjarczak_bambu_linux_host_abi1",
128
+ "pjarczak_bambu_linux_host_abi0",
129
+ "ca-certificates.crt",
130
+ "slicer_base64.cer",
131
+ ];
132
+ const pluginRequiredFiles = [
133
+ "pjarczak-bambu-linux-host-wrapper",
134
+ "install_runtime_macos.sh",
135
+ "verify_runtime_macos.sh",
136
+ ];
137
+ const wrapper = macosPluginDir ? path.join(macosPluginDir, "pjarczak-bambu-linux-host-wrapper") : undefined;
138
+ return {
139
+ macosRuntimeDir,
140
+ macosRuntimeDirExists: fileExists(macosRuntimeDir),
141
+ macosPluginDir,
142
+ macosPluginDirExists: macosPluginDir ? fileExists(macosPluginDir) : undefined,
143
+ macosMissingRuntimeFiles: collectMissingFiles(macosRuntimeDir, runtimeRequiredFiles),
144
+ macosMissingPluginFiles: collectMissingFiles(macosPluginDir, pluginRequiredFiles),
145
+ suggestedMacCommand: wrapper && fileExists(wrapper)
146
+ ? `PJARCZAK_BAMBU_PLUGIN_DIR=${shellQuote(macosRuntimeDir)} ${shellQuote(wrapper)} ${shellQuote(path.join(macosRuntimeDir, "pjarczak_bambu_linux_host"))}`
147
+ : undefined,
148
+ };
149
+ }
43
150
  function readFrameJson(payload) {
44
151
  const text = payload.toString("utf8");
45
152
  return text.length > 0 ? JSON.parse(text) : {};
@@ -57,6 +164,11 @@ function bridgeValueAsNumber(value, method) {
57
164
  }
58
165
  throw new Error(`FULU BambuNetwork bridge method ${method} did not return a numeric value.`);
59
166
  }
167
+ function bridgeNumericResult(value) {
168
+ return typeof value === "object" && value !== null && typeof value.value === "number"
169
+ ? value.value
170
+ : undefined;
171
+ }
60
172
  function assertBridgeOk(value, method) {
61
173
  if (isBridgeFailure(value)) {
62
174
  const errorText = value.error === undefined ? "unknown bridge error" : String(value.error);
@@ -70,6 +182,13 @@ export class BambuNetworkBridge {
70
182
  this.nextRequestId = 1;
71
183
  this.lastStderr = "";
72
184
  this.recentLogs = [];
185
+ /** Whether the host accepted the certificate folder and callbacks a LAN session needs. */
186
+ this.localSessionReady = false;
187
+ /**
188
+ * The plug-in holds one LAN printer connection and the host one event queue,
189
+ * so LAN sessions run one at a time, as Bambu Studio connects one printer.
190
+ */
191
+ this.localSessionTail = Promise.resolve();
73
192
  }
74
193
  resolveBridgeCommand(override) {
75
194
  return (trimEnv(override) ||
@@ -94,43 +213,20 @@ export class BambuNetworkBridge {
94
213
  const command = this.resolveBridgeCommand(options.bridgeCommand);
95
214
  const configDir = this.resolveConfigDir(options.configDir);
96
215
  const countryCode = this.resolveCountryCode(options.countryCode);
97
- const macosRuntimeDir = trimEnv(process.env.PJARCZAK_MAC_RUNTIME_DIR) ||
98
- path.join(os.homedir(), "Library", "Application Support", "OrcaSlicer", "macos-bridge", "runtime");
99
- const macosPluginDir = trimEnv(process.env.BAMBU_NETWORK_PLUGIN_DIR) ||
100
- trimEnv(process.env.PJARCZAK_BAMBU_PLUGIN_DIR) ||
101
- resolveFirstDirectoryWithFile([
102
- path.join(os.homedir(), "Library", "Application Support", "OrcaSlicer", "plugins"),
103
- "/Applications/Orca Studio.app/Contents/MacOS",
104
- "/Applications/OrcaSlicer.app/Contents/MacOS",
105
- "/Applications/OrcaSlicer-BMCU.app/Contents/MacOS",
106
- "/Applications/Orca Studio.app/Contents/Resources",
107
- "/Applications/OrcaSlicer.app/Contents/Resources",
108
- "/Applications/OrcaSlicer-BMCU.app/Contents/Resources",
109
- macosRuntimeDir,
110
- ], "pjarczak-bambu-linux-host-wrapper") ||
111
- resolveFirstExistingDirectory([macosRuntimeDir]);
112
- const runtimeRequiredFiles = [
113
- "libbambu_networking.so",
114
- "libBambuSource.so",
115
- "pjarczak_bambu_linux_host",
116
- "pjarczak_bambu_linux_host_abi1",
117
- "pjarczak_bambu_linux_host_abi0",
118
- "ca-certificates.crt",
119
- "slicer_base64.cer",
120
- ];
121
- const pluginRequiredFiles = [
122
- "pjarczak-bambu-linux-host-wrapper",
123
- "install_runtime_macos.sh",
124
- "verify_runtime_macos.sh",
125
- ];
126
- let suggestedMacCommand;
127
- if (macosPluginDir) {
128
- const wrapper = path.join(macosPluginDir, "pjarczak-bambu-linux-host-wrapper");
129
- const host = path.join(macosRuntimeDir, "pjarczak_bambu_linux_host");
130
- suggestedMacCommand =
131
- `PJARCZAK_BAMBU_PLUGIN_DIR=${shellQuote(macosRuntimeDir)} ` +
132
- `${shellQuote(wrapper)} ${shellQuote(host)}`;
216
+ const runtime = { certDir: bambuNetworkCertDir(configDir) };
217
+ if (process.platform === "linux") {
218
+ const pluginLibraries = {
219
+ ...Object.fromEntries(["PJARCZAK_BAMBU_NETWORK_SO", "PJARCZAK_BAMBU_SOURCE_SO"].flatMap(name => {
220
+ const value = trimEnv(process.env[name]);
221
+ return value ? [[name, value]] : [];
222
+ })),
223
+ ...linuxPluginLibraryEnv(process.env, process.platform),
224
+ };
225
+ if (Object.keys(pluginLibraries).length)
226
+ runtime.pluginLibraries = pluginLibraries;
133
227
  }
228
+ if (process.platform === "darwin")
229
+ Object.assign(runtime, macosRuntimeHints());
134
230
  return {
135
231
  configured: command.length > 0,
136
232
  command,
@@ -142,15 +238,7 @@ export class BambuNetworkBridge {
142
238
  agentReady: this.agentId !== undefined,
143
239
  lastStderr: this.lastStderr,
144
240
  recentLogs: this.recentLogs.slice(-10),
145
- runtime: {
146
- macosRuntimeDir,
147
- macosRuntimeDirExists: fileExists(macosRuntimeDir),
148
- macosPluginDir,
149
- macosPluginDirExists: macosPluginDir ? fileExists(macosPluginDir) : undefined,
150
- macosMissingRuntimeFiles: collectMissingFiles(macosRuntimeDir, runtimeRequiredFiles),
151
- macosMissingPluginFiles: collectMissingFiles(macosPluginDir, pluginRequiredFiles),
152
- suggestedMacCommand,
153
- },
241
+ runtime,
154
242
  note: command
155
243
  ? undefined
156
244
  : "Set BAMBU_NETWORK_BRIDGE_COMMAND to the FULU OrcaSlicer-bambulab Linux host or macOS/WSL wrapper command.",
@@ -202,11 +290,13 @@ export class BambuNetworkBridge {
202
290
  const configDir = this.resolveConfigDir(options.configDir);
203
291
  const countryCode = this.resolveCountryCode(options.countryCode);
204
292
  const userInfo = trimEnv(options.userInfo) || trimEnv(process.env.BAMBU_NETWORK_USER_INFO) || "";
205
- const key = `${command}\n${configDir}\n${countryCode}\n${userInfo}`;
293
+ const certDir = bambuNetworkCertDir(configDir);
294
+ const key = `${command}\n${configDir}\n${countryCode}\n${userInfo}\n${certDir}`;
206
295
  if (this.agentId !== undefined && this.agentKey === key) {
207
296
  return { agent: this.agentId, handshake: this.handshake };
208
297
  }
209
298
  fs.mkdirSync(configDir, { recursive: true });
299
+ prepareBambuNetworkCertDir(configDir);
210
300
  this.handshake = await this.request("bridge.handshake", {}, options);
211
301
  assertBridgeOk(this.handshake, "bridge.handshake");
212
302
  if (this.needsExpectedVersionRetry(this.handshake) && !trimEnv(process.env.PJARCZAK_EXPECTED_BAMBU_NETWORK_VERSION)) {
@@ -215,6 +305,13 @@ export class BambuNetworkBridge {
215
305
  this.handshake = await this.request("bridge.handshake", {}, options);
216
306
  assertBridgeOk(this.handshake, "bridge.handshake");
217
307
  }
308
+ const loaded = this.handshake;
309
+ if (loaded?.network_loaded === false && typeof loaded.network_status === "string" && /file open failed/i.test(loaded.network_status)) {
310
+ throw new Error(`FULU BambuNetwork bridge could not open its networking library (${loaded.network_status}). ` +
311
+ "Set PJARCZAK_BAMBU_PLUGIN_DIR to the directory containing libbambu_networking.so and libBambuSource.so, " +
312
+ "or set PJARCZAK_BAMBU_NETWORK_SO and PJARCZAK_BAMBU_SOURCE_SO to their absolute paths: " +
313
+ "https://github.com/DMontgomery40/bambu-printer-mcp/blob/main/docs/FULU.md#linux");
314
+ }
218
315
  const created = await this.request("net.create_agent", { log_dir: configDir, country_code: countryCode }, options);
219
316
  assertBridgeOk(created, "net.create_agent");
220
317
  const agent = bridgeValueAsNumber(created, "net.create_agent");
@@ -226,16 +323,162 @@ export class BambuNetworkBridge {
226
323
  };
227
324
  await withAgent("net.set_config_dir", { config_dir: configDir });
228
325
  await withAgent("net.init_log");
326
+ // Bambu Studio and FULU's GUI set this before starting the agent. The
327
+ // plug-in verifies a LAN printer with printer.cer from this folder, and
328
+ // FULU's host crashes on connect_printer when no folder was ever set.
329
+ const certificates = await withAgent("net.set_cert_file", { folder: certDir, filename: "slicer_base64.cer" }, false);
229
330
  await withAgent("net.set_country_code", { country_code: countryCode });
230
331
  await withAgent("net.start");
332
+ // The plug-in runs callbacks through queue_on_main; FULU's host executes
333
+ // them on its own dispatcher and queues on_local_connect for poll_events.
334
+ // Cloud jobs and probes do not depend on these, so a host without them
335
+ // still starts; a LAN session refuses before connecting instead.
336
+ const callbacks = [await withAgent("net.set_queue_on_main_fn", {}, false), await withAgent("net.set_on_local_connect_fn", {}, false)];
337
+ const localSessionReady = ![certificates, ...callbacks].some(isBridgeFailure);
231
338
  await withAgent("net.connect_server", {}, false);
232
339
  if (userInfo) {
233
340
  await withAgent("net.change_user", { user_info: userInfo });
234
341
  }
235
342
  this.agentId = agent;
236
343
  this.agentKey = key;
344
+ this.localSessionReady = localSessionReady;
237
345
  return { agent, handshake: this.handshake };
238
346
  }
347
+ /**
348
+ * Open the plug-in's own LAN MQTT session with a printer, in the order Bambu
349
+ * Studio and native/bambu-native-print.cpp use: connect, install the
350
+ * printer's application certificate (again on each "wait_info"), wait for
351
+ * the local connection, request the printer's state and let the key
352
+ * exchange settle. Without the certificate the plug-in uploads a job but
353
+ * cannot encrypt or publish project_file (-4030). The session is closed
354
+ * after `run`, so printer reports stop accumulating in the host's queue.
355
+ */
356
+ async withLocalSession(printer, options, assertActive, timing, run) {
357
+ const previous = this.localSessionTail;
358
+ let release;
359
+ this.localSessionTail = new Promise(resolve => { release = resolve; });
360
+ try {
361
+ await previous;
362
+ return await this.runLocalSession(printer, options, assertActive, timing, run);
363
+ }
364
+ finally {
365
+ release();
366
+ }
367
+ }
368
+ async runLocalSession(printer, options, assertActive, timing, run) {
369
+ const jobUpdates = [];
370
+ const installCertificate = async () => {
371
+ await this.callWithAgent("net.install_device_cert", { dev_id: printer.devId, lan_only: true }, options);
372
+ await this.callWithAgent("net.update_cert", {}, options);
373
+ };
374
+ // Drain queued events: collect job reports, return this printer's
375
+ // local-connect statuses, and reinstall the certificate on "wait_info".
376
+ const pump = async () => {
377
+ const polled = await this.request("bridge.poll_events", { limit: 1024 }, options);
378
+ assertBridgeOk(polled, "bridge.poll_events");
379
+ const statuses = [];
380
+ let waitInfo = false;
381
+ for (const event of Array.isArray(polled?.events) ? polled.events : []) {
382
+ if (event?.name === "job.update_status")
383
+ jobUpdates.push({ status: event.payload?.status, code: event.payload?.code, msg: event.payload?.msg });
384
+ const eventDev = event?.payload?.dev_id;
385
+ if (eventDev !== undefined && eventDev !== "" && eventDev !== printer.devId)
386
+ continue;
387
+ if (event?.name === "on_local_connect")
388
+ statuses.push(event.payload.status);
389
+ if ((event?.name === "on_local_message" || event?.name === "on_message") && event.payload?.msg === "wait_info")
390
+ waitInfo = true;
391
+ }
392
+ if (waitInfo)
393
+ await installCertificate();
394
+ return statuses;
395
+ };
396
+ const settle = async (ms) => {
397
+ for (const deadline = Date.now() + ms; Date.now() < deadline;) {
398
+ assertActive();
399
+ await pump();
400
+ await new Promise(resolve => setTimeout(resolve, timing.pollMs));
401
+ }
402
+ };
403
+ await this.ensureAgent(options);
404
+ assertActive();
405
+ if (!this.localSessionReady) {
406
+ throw new Error("FULU BambuNetwork bridge host did not accept the certificate folder or local-connection callbacks a LAN print needs. Update the FULU runtime; no print command was sent.");
407
+ }
408
+ for (const callback of ["net.set_on_printer_connected_fn", "net.set_on_local_message_fn"]) {
409
+ assertBridgeOk(await this.callWithAgent(callback, {}, options), callback);
410
+ }
411
+ await this.callWithAgent("net.update_cert", {}, options);
412
+ await pump();
413
+ assertActive();
414
+ const requested = await this.callWithAgent("net.connect_printer", {
415
+ dev_id: printer.devId, dev_ip: printer.devIp, username: printer.username, password: printer.password, use_ssl: printer.useSsl,
416
+ }, options);
417
+ assertBridgeOk(requested, "net.connect_printer");
418
+ const connectResult = bridgeValueAsNumber(requested, "net.connect_printer");
419
+ if (connectResult !== 0) {
420
+ throw new Error(`FULU BambuNetwork bridge could not start a LAN connection to printer ${printer.devId} (net.connect_printer returned ${connectResult}). No print command was sent.`);
421
+ }
422
+ let connected = false;
423
+ try {
424
+ await installCertificate();
425
+ const deadline = Date.now() + timing.connectTimeoutMs;
426
+ while (!connected) {
427
+ assertActive();
428
+ for (const status of await pump()) {
429
+ if (status === 0) {
430
+ connected = true;
431
+ continue;
432
+ }
433
+ const reason = typeof status === "number" && LOCAL_CONNECT_STATUS[status] ? LOCAL_CONNECT_STATUS[status] : `reported status ${status}`;
434
+ throw new Error(`FULU BambuNetwork bridge LAN connection to printer ${printer.devId} at ${printer.devIp} ${reason}. Check the printer IP, LAN access code and LAN mode. No print command was sent.`);
435
+ }
436
+ if (connected)
437
+ break;
438
+ if (Date.now() >= deadline) {
439
+ throw new Error(`FULU BambuNetwork bridge did not connect to printer ${printer.devId} at ${printer.devIp} over LAN within ${Math.round(timing.connectTimeoutMs / 1000)} s. No print command was sent.`);
440
+ }
441
+ await new Promise(resolve => setTimeout(resolve, timing.pollMs));
442
+ }
443
+ // Bambu Studio asks a newly connected printer for its full state and
444
+ // security metadata before building the encrypted project_file message.
445
+ for (const [message, qos] of [
446
+ [{ pushing: { sequence_id: "1", command: "pushall", version: 1, push_target: 1 } }, 0],
447
+ [{ info: { sequence_id: "2", command: "get_version" } }, 1],
448
+ [{ system: { sequence_id: "3", command: "get_access_code" } }, 0],
449
+ ]) {
450
+ await this.callWithAgent("net.send_message_to_printer", { dev_id: printer.devId, msg: JSON.stringify(message), qos, flag: 0 }, options);
451
+ }
452
+ await settle(timing.settleMs);
453
+ const result = await run({ installCertificate, settle, jobUpdates });
454
+ await pump();
455
+ return result;
456
+ }
457
+ finally {
458
+ const cleanupFailed = (step) => (error) => console.error(`FULU BambuNetwork bridge ${step} after the LAN session failed: ${error instanceof Error ? error.message : error}`);
459
+ if (connected)
460
+ await this.callWithAgent("net.disconnect_printer", {}, options).catch(cleanupFailed("disconnect"));
461
+ await this.request("bridge.poll_events", { limit: 4096 }, options).catch(cleanupFailed("event cleanup"));
462
+ }
463
+ }
464
+ /**
465
+ * Run a LAN print method in a local session. A synchronous -4030 means the
466
+ * plug-in published nothing, so it is retried once after the certificate
467
+ * exchange, as the native helper does; each attempt passes the caller's
468
+ * fresh pre-dispatch checks.
469
+ */
470
+ async startLocalPrint(printer, method, payload, options = {}, assertActive = () => { }, timing = LOCAL_PRINT_TIMING) {
471
+ return await this.withLocalSession(printer, options, assertActive, timing, async (session) => {
472
+ let result = await this.callWithAgent(method, payload, options);
473
+ if (bridgeNumericResult(result) === LOCAL_PRINT_PUBLISH_FAILED) {
474
+ await session.settle(timing.retryWaitMs);
475
+ await session.installCertificate();
476
+ await session.settle(timing.settleMs);
477
+ result = await this.callWithAgent(method, payload, options);
478
+ }
479
+ return { result, jobUpdates: session.jobUpdates };
480
+ });
481
+ }
239
482
  async callWithAgent(method, payload = {}, options = {}) {
240
483
  const { agent } = await this.ensureAgent(options);
241
484
  return await this.request(method, { ...payload, agent }, options);
@@ -287,6 +530,7 @@ export class BambuNetworkBridge {
287
530
  this.child = spawn(shell, shellArgs, {
288
531
  env: {
289
532
  ...process.env,
533
+ ...linuxPluginLibraryEnv(process.env, process.platform),
290
534
  PJARCZAK_BAMBU_COUNTRY_CODE: this.resolveCountryCode(),
291
535
  PJARCZAK_EXPECTED_BAMBU_NETWORK_VERSION: this.inferredExpectedNetworkVersion ||
292
536
  process.env.PJARCZAK_EXPECTED_BAMBU_NETWORK_VERSION ||
@@ -307,7 +551,8 @@ export class BambuNetworkBridge {
307
551
  if (this.child !== spawnedChild) {
308
552
  return;
309
553
  }
310
- this.rejectAllPending(new Error(`FULU BambuNetwork bridge exited (code=${code ?? "null"}, signal=${signal ?? "null"}). ${this.lastStderr}`.trim()));
554
+ this.rejectAllPending(new Error(`FULU BambuNetwork bridge exited (code=${code ?? "null"}, signal=${signal ?? "null"}). ${this.lastStderr}`.trim() +
555
+ (code === 101 ? ` ${STDOUT_SOCKET_HINT}` : "")));
311
556
  this.child = undefined;
312
557
  this.commandLine = undefined;
313
558
  this.agentId = undefined;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Bambu's public certificates from Bambu Studio v02.08.02.61 resources/cert,
3
+ * the folder Bambu Studio and FULU's GUI pass to the networking plug-in's
4
+ * set_cert_file. printer.cer holds the BBL CA roots that sign each printer's
5
+ * LAN MQTT certificate; slicer_base64.cer is the cloud certificate. FULU's
6
+ * standalone Linux host runtime ships only slicer_base64.cer, so its plug-in
7
+ * cannot verify a LAN printer without these. Exact bytes, pinned by SHA-256
8
+ * (the same pins as scripts/setup-bambu-native.mjs).
9
+ */
10
+ export declare const BAMBU_NETWORK_CERTIFICATES: Readonly<Record<"printer.cer" | "slicer_base64.cer", {
11
+ sha256: string;
12
+ base64: string;
13
+ }>>;