bambu-printer-mcp 1.1.33 → 1.1.35

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.
@@ -1,7 +1,9 @@
1
1
  import { spawn, type ChildProcessWithoutNullStreams } 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
 
6
8
  const FRAME_MAGIC = 0x52424a50;
7
9
  const FRAME_HEADER_SIZE = 16;
@@ -10,6 +12,27 @@ const FRAME_TYPE_JSON_RESPONSE = 2;
10
12
  const FRAME_TYPE_BINARY_DATA = 3;
11
13
  const FRAME_TYPE_LOG = 4;
12
14
  const DEFAULT_REQUEST_TIMEOUT_MS = 120_000;
15
+ // bambu_networking.hpp ConnectStatus.
16
+ const LOCAL_CONNECT_STATUS = ["ok", "failed", "lost"];
17
+ // bambu_networking.hpp BAMBU_NETWORK_ERR_PRINT_LP_PUBLISH_MSG_FAILED.
18
+ const LOCAL_PRINT_PUBLISH_FAILED = -4030;
19
+ // bambu_networking.hpp print errors that name a recoverable cause.
20
+ const BAMBU_NETWORK_PRINT_ERRORS: Record<number, string> = {
21
+ [-4010]: "the file exceeds the 1 GB upload limit",
22
+ [-4020]: "FTPS upload to the printer failed",
23
+ [LOCAL_PRINT_PUBLISH_FAILED]: "the networking plug-in could not send the print command to the printer over MQTT",
24
+ };
25
+
26
+ /** A parenthesized explanation of a plug-in print result, or "" when it names no known cause. */
27
+ export function describeBambuNetworkPrintError(value: number): string {
28
+ return BAMBU_NETWORK_PRINT_ERRORS[value] ? ` (${BAMBU_NETWORK_PRINT_ERRORS[value]})` : "";
29
+ }
30
+ /** Waits from native/bambu-native-print.cpp, which prints through the same plug-in calls. */
31
+ export const LOCAL_PRINT_TIMING = Object.freeze({ connectTimeoutMs: 30_000, pollMs: 250, settleMs: 3_000, retryWaitMs: 8_000 });
32
+
33
+ export type LocalPrinter = { devId: string; devIp: string; username: string; password: string; useSsl: boolean };
34
+ export type BridgeJobUpdate = { status: unknown; code: unknown; msg: unknown };
35
+ export type LocalSession = { installCertificate: () => Promise<void>; settle: (ms: number) => Promise<void>; jobUpdates: BridgeJobUpdate[] };
13
36
 
14
37
  type JsonObject = Record<string, unknown>;
15
38
 
@@ -44,17 +67,44 @@ export type BambuNetworkBridgeStatus = {
44
67
  lastStderr: string;
45
68
  recentLogs: string[];
46
69
  runtime: {
47
- macosRuntimeDir: string;
48
- macosRuntimeDirExists: boolean;
70
+ /** Folder passed to the plug-in's set_cert_file. */
71
+ certDir: string;
72
+ /** Linux only: absolute plug-in library paths passed to FULU's host. */
73
+ pluginLibraries?: Record<string, string>;
74
+ /** macOS only: conventional FULU runtime discovery. */
75
+ macosRuntimeDir?: string;
76
+ macosRuntimeDirExists?: boolean;
49
77
  macosPluginDir?: string;
50
78
  macosPluginDirExists?: boolean;
51
- macosMissingRuntimeFiles: string[];
52
- macosMissingPluginFiles: string[];
79
+ macosMissingRuntimeFiles?: string[];
80
+ macosMissingPluginFiles?: string[];
53
81
  suggestedMacCommand?: string;
54
82
  };
55
83
  note?: string;
56
84
  };
57
85
 
86
+ /**
87
+ * The plug-in's certificate folder: BAMBU_NETWORK_CERT_DIR when set, else
88
+ * Bambu's pinned public certificates written to the agent's config folder,
89
+ * which the host must already be able to read.
90
+ */
91
+ export function bambuNetworkCertDir(configDir: string, env: NodeJS.ProcessEnv = process.env): string {
92
+ return trimEnv(env.BAMBU_NETWORK_CERT_DIR) || path.join(configDir, "cert");
93
+ }
94
+
95
+ export function prepareBambuNetworkCertDir(configDir: string, env: NodeJS.ProcessEnv = process.env): string {
96
+ const certDir = bambuNetworkCertDir(configDir, env);
97
+ if (trimEnv(env.BAMBU_NETWORK_CERT_DIR)) return certDir;
98
+ fs.mkdirSync(certDir, { recursive: true });
99
+ for (const [name, { sha256, base64 }] of Object.entries(BAMBU_NETWORK_CERTIFICATES)) {
100
+ const content = Buffer.from(base64, "base64");
101
+ if (createHash("sha256").update(content).digest("hex") !== sha256) throw new Error(`Bundled Bambu certificate ${name} does not match its pinned SHA-256.`);
102
+ const target = path.join(certDir, name);
103
+ if (!fileExists(target) || !fs.readFileSync(target).equals(content)) fs.writeFileSync(target, content);
104
+ }
105
+ return certDir;
106
+ }
107
+
58
108
  export function defaultBambuNetworkConfigDir(): string {
59
109
  return path.join(os.homedir(), ".config", "bambu-printer-mcp", "bambu-network");
60
110
  }
@@ -94,6 +144,75 @@ function collectMissingFiles(baseDir: string | undefined, files: string[]): stri
94
144
  return files.filter((file) => !fileExists(path.join(baseDir, file)));
95
145
  }
96
146
 
147
+ /**
148
+ * FULU's Linux host opens its network and source libraries by bare name from
149
+ * its working directory instead of PJARCZAK_BAMBU_PLUGIN_DIR
150
+ * (FULU-Foundation/OrcaSlicer-bambulab#51), so name them in that directory
151
+ * unless they are already set. macOS wrappers run the host in a Linux guest
152
+ * that may not see host paths, so they are left alone.
153
+ */
154
+ export function linuxPluginLibraryEnv(env: NodeJS.ProcessEnv, platform: NodeJS.Platform): Record<string, string> {
155
+ const pluginDir = trimEnv(env.PJARCZAK_BAMBU_PLUGIN_DIR);
156
+ if (platform !== "linux" || !pluginDir) return {};
157
+ const libraries: Record<string, string> = {};
158
+ if (!trimEnv(env.PJARCZAK_BAMBU_NETWORK_SO)) libraries.PJARCZAK_BAMBU_NETWORK_SO = path.resolve(pluginDir, "libbambu_networking.so");
159
+ if (!trimEnv(env.PJARCZAK_BAMBU_SOURCE_SO)) libraries.PJARCZAK_BAMBU_SOURCE_SO = path.resolve(pluginDir, "libBambuSource.so");
160
+ return libraries;
161
+ }
162
+
163
+ /** FULU's Linux host exits 101 when it cannot reopen a socket stdout (FULU-Foundation/OrcaSlicer-bambulab#50). */
164
+ const STDOUT_SOCKET_HINT =
165
+ "Exit code 101 means FULU's Linux host could not reopen its stdout, which fails when stdout is a socket, as Node.js provides " +
166
+ "(https://github.com/FULU-Foundation/OrcaSlicer-bambulab/issues/50). Launch the host through a wrapper that pipes its output through cat: " +
167
+ "https://github.com/DMontgomery40/bambu-printer-mcp/blob/main/docs/FULU.md#linux";
168
+
169
+ /** Conventional FULU macOS runtime locations; the wrapper command is suggested only when it exists. */
170
+ function macosRuntimeHints(): Partial<BambuNetworkBridgeStatus["runtime"]> {
171
+ const macosRuntimeDir =
172
+ trimEnv(process.env.PJARCZAK_MAC_RUNTIME_DIR) ||
173
+ path.join(os.homedir(), "Library", "Application Support", "OrcaSlicer", "macos-bridge", "runtime");
174
+ const macosPluginDir =
175
+ trimEnv(process.env.BAMBU_NETWORK_PLUGIN_DIR) ||
176
+ trimEnv(process.env.PJARCZAK_BAMBU_PLUGIN_DIR) ||
177
+ resolveFirstDirectoryWithFile([
178
+ path.join(os.homedir(), "Library", "Application Support", "OrcaSlicer", "plugins"),
179
+ "/Applications/Orca Studio.app/Contents/MacOS",
180
+ "/Applications/OrcaSlicer.app/Contents/MacOS",
181
+ "/Applications/OrcaSlicer-BMCU.app/Contents/MacOS",
182
+ "/Applications/Orca Studio.app/Contents/Resources",
183
+ "/Applications/OrcaSlicer.app/Contents/Resources",
184
+ "/Applications/OrcaSlicer-BMCU.app/Contents/Resources",
185
+ macosRuntimeDir,
186
+ ], "pjarczak-bambu-linux-host-wrapper") ||
187
+ resolveFirstExistingDirectory([macosRuntimeDir]);
188
+ const runtimeRequiredFiles = [
189
+ "libbambu_networking.so",
190
+ "libBambuSource.so",
191
+ "pjarczak_bambu_linux_host",
192
+ "pjarczak_bambu_linux_host_abi1",
193
+ "pjarczak_bambu_linux_host_abi0",
194
+ "ca-certificates.crt",
195
+ "slicer_base64.cer",
196
+ ];
197
+ const pluginRequiredFiles = [
198
+ "pjarczak-bambu-linux-host-wrapper",
199
+ "install_runtime_macos.sh",
200
+ "verify_runtime_macos.sh",
201
+ ];
202
+ const wrapper = macosPluginDir ? path.join(macosPluginDir, "pjarczak-bambu-linux-host-wrapper") : undefined;
203
+ return {
204
+ macosRuntimeDir,
205
+ macosRuntimeDirExists: fileExists(macosRuntimeDir),
206
+ macosPluginDir,
207
+ macosPluginDirExists: macosPluginDir ? fileExists(macosPluginDir) : undefined,
208
+ macosMissingRuntimeFiles: collectMissingFiles(macosRuntimeDir, runtimeRequiredFiles),
209
+ macosMissingPluginFiles: collectMissingFiles(macosPluginDir, pluginRequiredFiles),
210
+ suggestedMacCommand: wrapper && fileExists(wrapper)
211
+ ? `PJARCZAK_BAMBU_PLUGIN_DIR=${shellQuote(macosRuntimeDir)} ${shellQuote(wrapper)} ${shellQuote(path.join(macosRuntimeDir, "pjarczak_bambu_linux_host"))}`
212
+ : undefined,
213
+ };
214
+ }
215
+
97
216
  function readFrameJson(payload: Buffer): unknown {
98
217
  const text = payload.toString("utf8");
99
218
  return text.length > 0 ? JSON.parse(text) : {};
@@ -119,6 +238,12 @@ function bridgeValueAsNumber(value: unknown, method: string): number {
119
238
  throw new Error(`FULU BambuNetwork bridge method ${method} did not return a numeric value.`);
120
239
  }
121
240
 
241
+ function bridgeNumericResult(value: unknown): number | undefined {
242
+ return typeof value === "object" && value !== null && typeof (value as { value?: unknown }).value === "number"
243
+ ? (value as { value: number }).value
244
+ : undefined;
245
+ }
246
+
122
247
  function assertBridgeOk(value: unknown, method: string): void {
123
248
  if (isBridgeFailure(value)) {
124
249
  const errorText = value.error === undefined ? "unknown bridge error" : String(value.error);
@@ -138,6 +263,13 @@ export class BambuNetworkBridge {
138
263
  private lastStderr = "";
139
264
  private recentLogs: string[] = [];
140
265
  private inferredExpectedNetworkVersion?: string;
266
+ /** Whether the host accepted the certificate folder and callbacks a LAN session needs. */
267
+ private localSessionReady = false;
268
+ /**
269
+ * The plug-in holds one LAN printer connection and the host one event queue,
270
+ * so LAN sessions run one at a time, as Bambu Studio connects one printer.
271
+ */
272
+ private localSessionTail: Promise<void> = Promise.resolve();
141
273
 
142
274
  resolveBridgeCommand(override?: string): string {
143
275
  return (
@@ -172,47 +304,18 @@ export class BambuNetworkBridge {
172
304
  const configDir = this.resolveConfigDir(options.configDir);
173
305
  const countryCode = this.resolveCountryCode(options.countryCode);
174
306
 
175
- const macosRuntimeDir =
176
- trimEnv(process.env.PJARCZAK_MAC_RUNTIME_DIR) ||
177
- path.join(os.homedir(), "Library", "Application Support", "OrcaSlicer", "macos-bridge", "runtime");
178
- const macosPluginDir =
179
- trimEnv(process.env.BAMBU_NETWORK_PLUGIN_DIR) ||
180
- trimEnv(process.env.PJARCZAK_BAMBU_PLUGIN_DIR) ||
181
- resolveFirstDirectoryWithFile([
182
- path.join(os.homedir(), "Library", "Application Support", "OrcaSlicer", "plugins"),
183
- "/Applications/Orca Studio.app/Contents/MacOS",
184
- "/Applications/OrcaSlicer.app/Contents/MacOS",
185
- "/Applications/OrcaSlicer-BMCU.app/Contents/MacOS",
186
- "/Applications/Orca Studio.app/Contents/Resources",
187
- "/Applications/OrcaSlicer.app/Contents/Resources",
188
- "/Applications/OrcaSlicer-BMCU.app/Contents/Resources",
189
- macosRuntimeDir,
190
- ], "pjarczak-bambu-linux-host-wrapper") ||
191
- resolveFirstExistingDirectory([macosRuntimeDir]);
192
-
193
- const runtimeRequiredFiles = [
194
- "libbambu_networking.so",
195
- "libBambuSource.so",
196
- "pjarczak_bambu_linux_host",
197
- "pjarczak_bambu_linux_host_abi1",
198
- "pjarczak_bambu_linux_host_abi0",
199
- "ca-certificates.crt",
200
- "slicer_base64.cer",
201
- ];
202
- const pluginRequiredFiles = [
203
- "pjarczak-bambu-linux-host-wrapper",
204
- "install_runtime_macos.sh",
205
- "verify_runtime_macos.sh",
206
- ];
207
-
208
- let suggestedMacCommand: string | undefined;
209
- if (macosPluginDir) {
210
- const wrapper = path.join(macosPluginDir, "pjarczak-bambu-linux-host-wrapper");
211
- const host = path.join(macosRuntimeDir, "pjarczak_bambu_linux_host");
212
- suggestedMacCommand =
213
- `PJARCZAK_BAMBU_PLUGIN_DIR=${shellQuote(macosRuntimeDir)} ` +
214
- `${shellQuote(wrapper)} ${shellQuote(host)}`;
307
+ const runtime: BambuNetworkBridgeStatus["runtime"] = { certDir: bambuNetworkCertDir(configDir) };
308
+ if (process.platform === "linux") {
309
+ const pluginLibraries = {
310
+ ...Object.fromEntries(["PJARCZAK_BAMBU_NETWORK_SO", "PJARCZAK_BAMBU_SOURCE_SO"].flatMap(name => {
311
+ const value = trimEnv(process.env[name]);
312
+ return value ? [[name, value]] : [];
313
+ })),
314
+ ...linuxPluginLibraryEnv(process.env, process.platform),
315
+ };
316
+ if (Object.keys(pluginLibraries).length) runtime.pluginLibraries = pluginLibraries;
215
317
  }
318
+ if (process.platform === "darwin") Object.assign(runtime, macosRuntimeHints());
216
319
 
217
320
  return {
218
321
  configured: command.length > 0,
@@ -225,15 +328,7 @@ export class BambuNetworkBridge {
225
328
  agentReady: this.agentId !== undefined,
226
329
  lastStderr: this.lastStderr,
227
330
  recentLogs: this.recentLogs.slice(-10),
228
- runtime: {
229
- macosRuntimeDir,
230
- macosRuntimeDirExists: fileExists(macosRuntimeDir),
231
- macosPluginDir,
232
- macosPluginDirExists: macosPluginDir ? fileExists(macosPluginDir) : undefined,
233
- macosMissingRuntimeFiles: collectMissingFiles(macosRuntimeDir, runtimeRequiredFiles),
234
- macosMissingPluginFiles: collectMissingFiles(macosPluginDir, pluginRequiredFiles),
235
- suggestedMacCommand,
236
- },
331
+ runtime,
237
332
  note: command
238
333
  ? undefined
239
334
  : "Set BAMBU_NETWORK_BRIDGE_COMMAND to the FULU OrcaSlicer-bambulab Linux host or macOS/WSL wrapper command.",
@@ -294,13 +389,15 @@ export class BambuNetworkBridge {
294
389
  const configDir = this.resolveConfigDir(options.configDir);
295
390
  const countryCode = this.resolveCountryCode(options.countryCode);
296
391
  const userInfo = trimEnv(options.userInfo) || trimEnv(process.env.BAMBU_NETWORK_USER_INFO) || "";
297
- const key = `${command}\n${configDir}\n${countryCode}\n${userInfo}`;
392
+ const certDir = bambuNetworkCertDir(configDir);
393
+ const key = `${command}\n${configDir}\n${countryCode}\n${userInfo}\n${certDir}`;
298
394
 
299
395
  if (this.agentId !== undefined && this.agentKey === key) {
300
396
  return { agent: this.agentId, handshake: this.handshake };
301
397
  }
302
398
 
303
399
  fs.mkdirSync(configDir, { recursive: true });
400
+ prepareBambuNetworkCertDir(configDir);
304
401
 
305
402
  this.handshake = await this.request("bridge.handshake", {}, options);
306
403
  assertBridgeOk(this.handshake, "bridge.handshake");
@@ -310,6 +407,15 @@ export class BambuNetworkBridge {
310
407
  this.handshake = await this.request("bridge.handshake", {}, options);
311
408
  assertBridgeOk(this.handshake, "bridge.handshake");
312
409
  }
410
+ const loaded = this.handshake as { network_loaded?: unknown; network_status?: unknown } | undefined;
411
+ if (loaded?.network_loaded === false && typeof loaded.network_status === "string" && /file open failed/i.test(loaded.network_status)) {
412
+ throw new Error(
413
+ `FULU BambuNetwork bridge could not open its networking library (${loaded.network_status}). ` +
414
+ "Set PJARCZAK_BAMBU_PLUGIN_DIR to the directory containing libbambu_networking.so and libBambuSource.so, " +
415
+ "or set PJARCZAK_BAMBU_NETWORK_SO and PJARCZAK_BAMBU_SOURCE_SO to their absolute paths: " +
416
+ "https://github.com/DMontgomery40/bambu-printer-mcp/blob/main/docs/FULU.md#linux"
417
+ );
418
+ }
313
419
 
314
420
  const created = await this.request("net.create_agent", { log_dir: configDir, country_code: countryCode }, options);
315
421
  assertBridgeOk(created, "net.create_agent");
@@ -323,8 +429,18 @@ export class BambuNetworkBridge {
323
429
 
324
430
  await withAgent("net.set_config_dir", { config_dir: configDir });
325
431
  await withAgent("net.init_log");
432
+ // Bambu Studio and FULU's GUI set this before starting the agent. The
433
+ // plug-in verifies a LAN printer with printer.cer from this folder, and
434
+ // FULU's host crashes on connect_printer when no folder was ever set.
435
+ const certificates = await withAgent("net.set_cert_file", { folder: certDir, filename: "slicer_base64.cer" }, false);
326
436
  await withAgent("net.set_country_code", { country_code: countryCode });
327
437
  await withAgent("net.start");
438
+ // The plug-in runs callbacks through queue_on_main; FULU's host executes
439
+ // them on its own dispatcher and queues on_local_connect for poll_events.
440
+ // Cloud jobs and probes do not depend on these, so a host without them
441
+ // still starts; a LAN session refuses before connecting instead.
442
+ const callbacks = [await withAgent("net.set_queue_on_main_fn", {}, false), await withAgent("net.set_on_local_connect_fn", {}, false)];
443
+ const localSessionReady = ![certificates, ...callbacks].some(isBridgeFailure);
328
444
  await withAgent("net.connect_server", {}, false);
329
445
 
330
446
  if (userInfo) {
@@ -333,9 +449,160 @@ export class BambuNetworkBridge {
333
449
 
334
450
  this.agentId = agent;
335
451
  this.agentKey = key;
452
+ this.localSessionReady = localSessionReady;
336
453
  return { agent, handshake: this.handshake };
337
454
  }
338
455
 
456
+ /**
457
+ * Open the plug-in's own LAN MQTT session with a printer, in the order Bambu
458
+ * Studio and native/bambu-native-print.cpp use: connect, install the
459
+ * printer's application certificate (again on each "wait_info"), wait for
460
+ * the local connection, request the printer's state and let the key
461
+ * exchange settle. Without the certificate the plug-in uploads a job but
462
+ * cannot encrypt or publish project_file (-4030). The session is closed
463
+ * after `run`, so printer reports stop accumulating in the host's queue.
464
+ */
465
+ async withLocalSession<T>(
466
+ printer: LocalPrinter,
467
+ options: BambuNetworkBridgeOptions,
468
+ assertActive: () => void,
469
+ timing: typeof LOCAL_PRINT_TIMING,
470
+ run: (session: LocalSession) => Promise<T>
471
+ ): Promise<T> {
472
+ const previous = this.localSessionTail;
473
+ let release!: () => void;
474
+ this.localSessionTail = new Promise<void>(resolve => { release = resolve; });
475
+ try {
476
+ await previous;
477
+ return await this.runLocalSession(printer, options, assertActive, timing, run);
478
+ } finally {
479
+ release();
480
+ }
481
+ }
482
+
483
+ private async runLocalSession<T>(
484
+ printer: LocalPrinter,
485
+ options: BambuNetworkBridgeOptions,
486
+ assertActive: () => void,
487
+ timing: typeof LOCAL_PRINT_TIMING,
488
+ run: (session: LocalSession) => Promise<T>
489
+ ): Promise<T> {
490
+ const jobUpdates: BridgeJobUpdate[] = [];
491
+ const installCertificate = async () => {
492
+ await this.callWithAgent("net.install_device_cert", { dev_id: printer.devId, lan_only: true }, options);
493
+ await this.callWithAgent("net.update_cert", {}, options);
494
+ };
495
+ // Drain queued events: collect job reports, return this printer's
496
+ // local-connect statuses, and reinstall the certificate on "wait_info".
497
+ const pump = async (): Promise<unknown[]> => {
498
+ const polled = await this.request("bridge.poll_events", { limit: 1024 }, options) as { events?: unknown };
499
+ assertBridgeOk(polled, "bridge.poll_events");
500
+ const statuses: unknown[] = [];
501
+ let waitInfo = false;
502
+ for (const event of Array.isArray(polled?.events) ? polled.events : []) {
503
+ if (event?.name === "job.update_status") jobUpdates.push({ status: event.payload?.status, code: event.payload?.code, msg: event.payload?.msg });
504
+ const eventDev = event?.payload?.dev_id;
505
+ if (eventDev !== undefined && eventDev !== "" && eventDev !== printer.devId) continue;
506
+ if (event?.name === "on_local_connect") statuses.push(event.payload.status);
507
+ if ((event?.name === "on_local_message" || event?.name === "on_message") && event.payload?.msg === "wait_info") waitInfo = true;
508
+ }
509
+ if (waitInfo) await installCertificate();
510
+ return statuses;
511
+ };
512
+ const settle = async (ms: number) => {
513
+ for (const deadline = Date.now() + ms; Date.now() < deadline;) {
514
+ assertActive();
515
+ await pump();
516
+ await new Promise(resolve => setTimeout(resolve, timing.pollMs));
517
+ }
518
+ };
519
+
520
+ await this.ensureAgent(options);
521
+ assertActive();
522
+ if (!this.localSessionReady) {
523
+ 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.");
524
+ }
525
+ for (const callback of ["net.set_on_printer_connected_fn", "net.set_on_local_message_fn"]) {
526
+ assertBridgeOk(await this.callWithAgent(callback, {}, options), callback);
527
+ }
528
+ await this.callWithAgent("net.update_cert", {}, options);
529
+ await pump();
530
+ assertActive();
531
+ const requested = await this.callWithAgent("net.connect_printer", {
532
+ dev_id: printer.devId, dev_ip: printer.devIp, username: printer.username, password: printer.password, use_ssl: printer.useSsl,
533
+ }, options);
534
+ assertBridgeOk(requested, "net.connect_printer");
535
+ const connectResult = bridgeValueAsNumber(requested, "net.connect_printer");
536
+ if (connectResult !== 0) {
537
+ 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.`);
538
+ }
539
+
540
+ let connected = false;
541
+ try {
542
+ await installCertificate();
543
+ const deadline = Date.now() + timing.connectTimeoutMs;
544
+ while (!connected) {
545
+ assertActive();
546
+ for (const status of await pump()) {
547
+ if (status === 0) {
548
+ connected = true;
549
+ continue;
550
+ }
551
+ const reason = typeof status === "number" && LOCAL_CONNECT_STATUS[status] ? LOCAL_CONNECT_STATUS[status] : `reported status ${status}`;
552
+ 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.`);
553
+ }
554
+ if (connected) break;
555
+ if (Date.now() >= deadline) {
556
+ 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.`);
557
+ }
558
+ await new Promise(resolve => setTimeout(resolve, timing.pollMs));
559
+ }
560
+ // Bambu Studio asks a newly connected printer for its full state and
561
+ // security metadata before building the encrypted project_file message.
562
+ for (const [message, qos] of [
563
+ [{ pushing: { sequence_id: "1", command: "pushall", version: 1, push_target: 1 } }, 0],
564
+ [{ info: { sequence_id: "2", command: "get_version" } }, 1],
565
+ [{ system: { sequence_id: "3", command: "get_access_code" } }, 0],
566
+ ] as const) {
567
+ await this.callWithAgent("net.send_message_to_printer", { dev_id: printer.devId, msg: JSON.stringify(message), qos, flag: 0 }, options);
568
+ }
569
+ await settle(timing.settleMs);
570
+ const result = await run({ installCertificate, settle, jobUpdates });
571
+ await pump();
572
+ return result;
573
+ } finally {
574
+ const cleanupFailed = (step: string) => (error: unknown) => console.error(`FULU BambuNetwork bridge ${step} after the LAN session failed: ${error instanceof Error ? error.message : error}`);
575
+ if (connected) await this.callWithAgent("net.disconnect_printer", {}, options).catch(cleanupFailed("disconnect"));
576
+ await this.request("bridge.poll_events", { limit: 4096 }, options).catch(cleanupFailed("event cleanup"));
577
+ }
578
+ }
579
+
580
+ /**
581
+ * Run a LAN print method in a local session. A synchronous -4030 means the
582
+ * plug-in published nothing, so it is retried once after the certificate
583
+ * exchange, as the native helper does; each attempt passes the caller's
584
+ * fresh pre-dispatch checks.
585
+ */
586
+ async startLocalPrint(
587
+ printer: LocalPrinter,
588
+ method: string,
589
+ payload: JsonObject,
590
+ options: BambuNetworkBridgeOptions = {},
591
+ assertActive: () => void = () => {},
592
+ timing: typeof LOCAL_PRINT_TIMING = LOCAL_PRINT_TIMING
593
+ ): Promise<{ result: unknown; jobUpdates: BridgeJobUpdate[] }> {
594
+ return await this.withLocalSession(printer, options, assertActive, timing, async session => {
595
+ let result = await this.callWithAgent(method, payload, options);
596
+ if (bridgeNumericResult(result) === LOCAL_PRINT_PUBLISH_FAILED) {
597
+ await session.settle(timing.retryWaitMs);
598
+ await session.installCertificate();
599
+ await session.settle(timing.settleMs);
600
+ result = await this.callWithAgent(method, payload, options);
601
+ }
602
+ return { result, jobUpdates: session.jobUpdates };
603
+ });
604
+ }
605
+
339
606
  async callWithAgent(
340
607
  method: string,
341
608
  payload: JsonObject = {},
@@ -401,6 +668,7 @@ export class BambuNetworkBridge {
401
668
  this.child = spawn(shell, shellArgs, {
402
669
  env: {
403
670
  ...process.env,
671
+ ...linuxPluginLibraryEnv(process.env, process.platform),
404
672
  PJARCZAK_BAMBU_COUNTRY_CODE: this.resolveCountryCode(),
405
673
  PJARCZAK_EXPECTED_BAMBU_NETWORK_VERSION:
406
674
  this.inferredExpectedNetworkVersion ||
@@ -424,7 +692,8 @@ export class BambuNetworkBridge {
424
692
  return;
425
693
  }
426
694
  this.rejectAllPending(
427
- new Error(`FULU BambuNetwork bridge exited (code=${code ?? "null"}, signal=${signal ?? "null"}). ${this.lastStderr}`.trim())
695
+ new Error(`FULU BambuNetwork bridge exited (code=${code ?? "null"}, signal=${signal ?? "null"}). ${this.lastStderr}`.trim() +
696
+ (code === 101 ? ` ${STDOUT_SOCKET_HINT}` : ""))
428
697
  );
429
698
  this.child = undefined;
430
699
  this.commandLine = undefined;
@@ -0,0 +1,154 @@
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 const BAMBU_NETWORK_CERTIFICATES: Readonly<Record<"printer.cer" | "slicer_base64.cer", { sha256: string; base64: string }>> = Object.freeze({
11
+ "printer.cer": {
12
+ sha256: "36f2bcee347ec7adce719b5fd350099591a4d3d0ec4e039c7019890d78e152a0",
13
+ base64:
14
+ "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUZmekNDQTJlZ0F3SUJBZ0lVWHR6UjZ0UmlML1JIQlJYT295RlUwK1hybGlv" +
15
+ "d0RRWUpLb1pJaHZjTkFRRUwKQlFBd1JqRUxNQWtHQTFVRUJoTUNRMDR4SVRBZkJnTlZCQW9NR0VKQ1RDQlVaV05vYm05c2IyZHBa" +
16
+ "WE1nUTI4dQpJRXgwWkRFVU1CSUdBMVVFQXd3TFFrSk1JRU5CTWlCU1UwRXdJQmNOTWpVd05qRTNNREV6T0RBNFdoZ1BNakExCk1E" +
17
+ "QTJNVGN3TVRNNE1EaGFNRVl4Q3pBSkJnTlZCQVlUQWtOT01TRXdId1lEVlFRS0RCaENRa3dnVkdWamFHNXYKYkc5bmFXVnpJRU52" +
18
+ "TGlCTWRHUXhGREFTQmdOVkJBTU1DMEpDVENCRFFUSWdVbE5CTUlJQ0lqQU5CZ2txaGtpRwo5dzBCQVFFRkFBT0NBZzhBTUlJQ0Nn" +
19
+ "S0NBZ0VBbzQ1NTBHNGM0MmdUS3pRcWl4d0tUMDg5Uml6SWRacHlPY0dBCjY3OXJQYU9kV3NNcVZ3bllQUDJGcE1xWEtrakZiZWRF" +
20
+ "K1NwR2xvaTJOS0N1aVBOVlJicTlQSE9PWndUczdZTG8KYk93ZjUzRkp1TzZ2UkZwekZmWDF0bGM5emxGcUp2Wm5ZTzlOZ0hwTXlz" +
21
+ "aWRvY1djZ3JETi9TSUR5d2dQQjVDVgpiWWczVnZ6dWE5ZndaeDllNUtUOXhkNUlwVHFkVHJXUzQ3alFPVktMaGRRQ2JKRklsTXJi" +
22
+ "bE93TEJBeCtmSG9rCndxaDZ0a0k2S3R1eXlqdzhEeXNlYmkxbmRXakt0WjJtVzQ3cjh4Wi9KK3ozRVpxY3lKTVk2TVJ0eC96YjFq" +
23
+ "QkYKdUh0a2pyYjVLdjFETXpTS2xrYU5KSWJ2QytNaytoSTk3VytTakxTUnVJZEM3K29KVXpXYVN6Z3U5Y2pYQ1ZmbQpxOHQ0SUwv" +
24
+ "MzVoUDY5UEs5NUxnTGVjdElyUDk2Q1lBVC9hVk1HMTlGckZXMFFXRXlmVCtrekc0amt1bWZQYkhxClkybk5rRU4wK3RqajNoNFdk" +
25
+ "enJXZ1FFb2pLL2xoZmNSRlZrdHM3NCthWm9NcFFQK3ZtTDE3Q0ttU3pYazVvL2UKSzIxeGd4SmR6TWJkenRmVHBpYmlYazBhYmZP" +
26
+ "cE4rMVZSKzNOWWErYlJPQUtOeUdhUmVFR3N5VzJiamNqTng1MQo1VnF6ajNTVnhoTVNwNXZmRjlFNEExakU5OU0vbDlqUURNNlJ6" +
27
+ "a1QwbE1jY0dBZDV0VVNkTnZEbHJxdFFhUWlLCnYvWnNYUGdYTFRXZk9wdmFMTkVnd2RNZ1pNdWhqcGt3dkFaeW9ZZmVGOWt5eWRq" +
28
+ "RGg3YnZyWC8vY3ovVm9wQVUKbHhVdFF0TUNBd0VBQWFOak1HRXdIUVlEVlIwT0JCWUVGTlZKZ1FhZDFzTlROMGp4Vmt3YkovWE0x" +
29
+ "YW4xTUI4RwpBMVVkSXdRWU1CYUFGTlZKZ1FhZDFzTlROMGp4Vmt3YkovWE0xYW4xTUE4R0ExVWRFd0VCL3dRRk1BTUJBZjh3CkRn" +
30
+ "WURWUjBQQVFIL0JBUURBZ0VHTUEwR0NTcUdTSWIzRFFFQkN3VUFBNElDQVFCRlpES01KZnAvTjRnQmVGSGgKTWlGZWhhVXlNUzZl" +
31
+ "OW16clRmTUxKTEpvajZKb3BhOVY5aklmY0NFQkdadVJUaHFGY0FUVitVZEZIU0lOcFVjSAp1cGNDWW5helRSQzRkbjFobnhuUTFv" +
32
+ "alFjSHhkR3A5eEd3L1ljbEFLRDk3ZDhiUFNoZkJNVDF0bzl6Yk1LN1Q1Ckw4emdxZzAxWUlPS2pRazBIY2QwKzBpVXI2bTh6UTVQ" +
33
+ "OFJsM1FYcUF5ZVdncW1ZUXJyalRXd1BzZ2RmSE5YS1gKdkRyeDcvY3FyeTVsS1U4MDJoVXBsS01CeGVsdjRXODQwN1l0ajFsZkpP" +
34
+ "d3Z4cXh4c0ZVNWpTd2NVRzN6bzJ2awpRdGpSczhtNUJLdXA1SzFPUFlra1B1N0xkODlYMFhwVTA3My9kTkRHMTF1eGIxZURLcnRO" +
35
+ "UDZ2WnVaak5FMlBxCjhIQ29JMUV0UCtJdHlxdFVNdkhpNloyenNtbEEyNWJyb1Zpb2VVS3hqbEllY3BROUpSL0ZoRHU5Q1dORi9u" +
36
+ "RFcKTFNPUk5hTU16Z3NNU3pJK0hDaVVocU4rcU1JdlZQNnJ6R1RKendxei9sYzVMZitaUENuR0E5V0pUVDR1UEloZgp1ZmJabW5V" +
37
+ "SjM1V3VXS0h4b3ZEc3FCaDg4elE5c1orZWk0SGk0dlZ6T2hVZ2ZHM2FMb1NRRVlxUm9xYWJvQU5oCndDd3p5dVcyUnY1NHU1UVNC" +
38
+ "YmQ2R3gxT3B2c1dtTFBXZDIvaUwya0lTbDV3Zm1MR1Z5ZHZTSmErcmJPZnVBeTcKeWNWUWFjVkRRQ0FuYmhvVnJReTcrNDU0UXNL" +
39
+ "U1czWlY2QmN5UnJvcmV3Q3lDWWdkN255eGZseEhaVEJFeWtYWApoYUdOZS9LRk52SkJNT0l1SVV6a25SUm1pUT09Ci0tLS0tRU5E" +
40
+ "IENFUlRJRklDQVRFLS0tLS0KLS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUI4ekNDQVptZ0F3SUJBZ0lVZTYxakdRNFJ6" +
41
+ "SUM4aytzTnVxYkkvQ2FOcVBJd0NnWUlLb1pJemowRUF3SXcKUmpFTE1Ba0dBMVVFQmhNQ1EwNHhJVEFmQmdOVkJBb01HRUpDVENC" +
42
+ "VVpXTm9ibTlzYjJkcFpYTWdRMjh1SUV4MApaREVVTUJJR0ExVUVBd3dMUWtKTUlFTkJNaUJGUTBNd0lCY05NalV3TmpFM01ERXpP" +
43
+ "RE0xV2hnUE1qQTFNREEyCk1UY3dNVE00TXpWYU1FWXhDekFKQmdOVkJBWVRBa05PTVNFd0h3WURWUVFLREJoQ1Frd2dWR1ZqYUc1" +
44
+ "dmJHOW4KYVdWeklFTnZMaUJNZEdReEZEQVNCZ05WQkFNTUMwSkNUQ0JEUVRJZ1JVTkRNRmt3RXdZSEtvWkl6ajBDQVFZSQpLb1pJ" +
45
+ "emowREFRY0RRZ0FFcEtURjd3UlN0eTREWHBHSnpnQ1B3Umg4Z2hMbHhVQzNxSmJ5RWdMcVR2SmdiaXdZCkFQUEhLN2tWYlZtZXJr" +
46
+ "cWhIT1Q0UWVXUmxURzNkT1FHTEEyVnBhTmpNR0V3SFFZRFZSME9CQllFRkt1UnBzalkKUkVPeUlLSDdId09FNmpoR0JkNk5NQjhH" +
47
+ "QTFVZEl3UVlNQmFBRkt1UnBzallSRU95SUtIN0h3T0U2amhHQmQ2TgpNQThHQTFVZEV3RUIvd1FGTUFNQkFmOHdEZ1lEVlIwUEFR" +
48
+ "SC9CQVFEQWdFR01Bb0dDQ3FHU000OUJBTUNBMGdBCk1FVUNJRXJCaVVtM1ZkdFAzcno0a2I4YUxwSTVwK0J6TDdNOXZFbEJHV1dK" +
49
+ "eHBITUFpRUEzcjV0SldWR3d1eGkKWUNyQjFjNDBLWUZSRnlhaEdyaE9KWkFqL1loUmRuVT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUt" +
50
+ "LS0tLQotLS0tLUJFR0lOIENFUlRJRklDQVRFLS0tLS0KTUlJRWVUQ0NBMkdnQXdJQkFnSVVPcStsTklhQzJ4c3N3a0ZxajVKUHlW" +
51
+ "Qmw0NWN3RFFZSktvWklodmNOQVFFTApCUUF3UWpFTE1Ba0dBMVVFQmhNQ1EwNHhJakFnQmdOVkJBb01HVUpDVENCVVpXTm9ibTlz" +
52
+ "YjJkcFpYTWdRMjh1CkxDQk1kR1F4RHpBTkJnTlZCQU1NQmtKQ1RDQkRRVEFlRncweU5UQTJNVGN3TWpBeE1qZGFGdzB6TlRBMk1U" +
53
+ "VXcKTWpBeE1qZGFNRVl4Q3pBSkJnTlZCQVlUQWtOT01TRXdId1lEVlFRS0RCaENRa3dnVkdWamFHNXZiRzluYVdWegpJRU52TGlC" +
54
+ "TWRHUXhGREFTQmdOVkJBTU1DMEpDVENCRFFUSWdVbE5CTUlJQ0lqQU5CZ2txaGtpRzl3MEJBUUVGCkFBT0NBZzhBTUlJQ0NnS0NB" +
55
+ "Z0VBbzQ1NTBHNGM0MmdUS3pRcWl4d0tUMDg5Uml6SWRacHlPY0dBNjc5clBhT2QKV3NNcVZ3bllQUDJGcE1xWEtrakZiZWRFK1Nw" +
56
+ "R2xvaTJOS0N1aVBOVlJicTlQSE9PWndUczdZTG9iT3dmNTNGSgp1TzZ2UkZwekZmWDF0bGM5emxGcUp2Wm5ZTzlOZ0hwTXlzaWRv" +
57
+ "Y1djZ3JETi9TSUR5d2dQQjVDVmJZZzNWdnp1CmE5ZndaeDllNUtUOXhkNUlwVHFkVHJXUzQ3alFPVktMaGRRQ2JKRklsTXJibE93" +
58
+ "TEJBeCtmSG9rd3FoNnRrSTYKS3R1eXlqdzhEeXNlYmkxbmRXakt0WjJtVzQ3cjh4Wi9KK3ozRVpxY3lKTVk2TVJ0eC96YjFqQkZ1" +
59
+ "SHRranJiNQpLdjFETXpTS2xrYU5KSWJ2QytNaytoSTk3VytTakxTUnVJZEM3K29KVXpXYVN6Z3U5Y2pYQ1ZmbXE4dDRJTC8zCjVo" +
60
+ "UDY5UEs5NUxnTGVjdElyUDk2Q1lBVC9hVk1HMTlGckZXMFFXRXlmVCtrekc0amt1bWZQYkhxWTJuTmtFTjAKK3RqajNoNFdkenJX" +
61
+ "Z1FFb2pLL2xoZmNSRlZrdHM3NCthWm9NcFFQK3ZtTDE3Q0ttU3pYazVvL2VLMjF4Z3hKZAp6TWJkenRmVHBpYmlYazBhYmZPcE4r" +
62
+ "MVZSKzNOWWErYlJPQUtOeUdhUmVFR3N5VzJiamNqTng1MTVWcXpqM1NWCnhoTVNwNXZmRjlFNEExakU5OU0vbDlqUURNNlJ6a1Qw" +
63
+ "bE1jY0dBZDV0VVNkTnZEbHJxdFFhUWlLdi9ac1hQZ1gKTFRXZk9wdmFMTkVnd2RNZ1pNdWhqcGt3dkFaeW9ZZmVGOWt5eWRqRGg3" +
64
+ "YnZyWC8vY3ovVm9wQVVseFV0UXRNQwpBd0VBQWFOak1HRXdEd1lEVlIwVEFRSC9CQVV3QXdFQi96QU9CZ05WSFE4QkFmOEVCQU1D" +
65
+ "QVFZd0hRWURWUjBPCkJCWUVGTlZKZ1FhZDFzTlROMGp4Vmt3YkovWE0xYW4xTUI4R0ExVWRJd1FZTUJhQUZJODBRbWpjWjA2UHhD" +
66
+ "S2UKeFh4SjVhdmRSTDRlTUEwR0NTcUdTSWIzRFFFQkN3VUFBNElCQVFBdlM4dHlmYWdhR3NGZjlZbmNBMmtvL05hNQo5QlZGKzhU" +
67
+ "bFVvKzMyb3pud0lWcFMxQWhTZ0xQNnJOVmVrWE5GS2J1UDVodHVkTFExN1pSQkpJL1VNVnlZRURxCklON3h2N1pqK3pKd0Y2VzZo" +
68
+ "YVlyamIyVms4aWd3MVh2TlVMWmZ2Vk5OS0lrdkpVaVZxRXNsV3JDK2s3NGNyay8KV3Y4Q2hWZit6cXZmSU42TFYzZXNhR1JMMDJK" +
69
+ "M0FwclFHYjdERGhSMUVlZlFNU2NEa05wR0pNVW12Q21ma25ybAppSzhxZ3ZRTjFTV083SlJmNmZOS0hzTjFaUXZ5UDBwZ0xXeHBU" +
70
+ "M1YwLzAvV3R0cVgzY01HdUpGK2pWVXptL05oCnhZaEZld0c4dmMzS3pUam53UUFwTUE2Q1c1NTRGT0pXRnlPRDJqbjV5SkxUM1Z1" +
71
+ "ZSthWURRUnA0YktNeAotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCi0tLS0tQkVHSU4gQ0VSVElGSUNBVEUtLS0tLQpNSUlDcmpD" +
72
+ "Q0FaYWdBd0lCQWdJVU9xK2xOSWFDMnhzc3drRnFqNUpQeVZCbDQ1Z3dEUVlKS29aSWh2Y05BUUVMCkJRQXdRakVMTUFrR0ExVUVC" +
73
+ "aE1DUTA0eElqQWdCZ05WQkFvTUdVSkNUQ0JVWldOb2JtOXNiMmRwWlhNZ1EyOHUKTENCTWRHUXhEekFOQmdOVkJBTU1Ca0pDVENC" +
74
+ "RFFUQWVGdzB5TlRBMk1UY3dNakF4TkRkYUZ3MHpOVEEyTVRVdwpNakF4TkRkYU1FWXhDekFKQmdOVkJBWVRBa05PTVNFd0h3WURW" +
75
+ "UVFLREJoQ1Frd2dWR1ZqYUc1dmJHOW5hV1Z6CklFTnZMaUJNZEdReEZEQVNCZ05WQkFNTUMwSkNUQ0JEUVRJZ1JVTkRNRmt3RXdZ" +
76
+ "SEtvWkl6ajBDQVFZSUtvWkkKemowREFRY0RRZ0FFcEtURjd3UlN0eTREWHBHSnpnQ1B3Umg4Z2hMbHhVQzNxSmJ5RWdMcVR2Smdi" +
77
+ "aXdZQVBQSApLN2tWYlZtZXJrcWhIT1Q0UWVXUmxURzNkT1FHTEEyVnBhTmpNR0V3RHdZRFZSMFRBUUgvQkFVd0F3RUIvekFPCkJn" +
78
+ "TlZIUThCQWY4RUJBTUNBUVl3SFFZRFZSME9CQllFRkt1UnBzallSRU95SUtIN0h3T0U2amhHQmQ2Tk1COEcKQTFVZEl3UVlNQmFB" +
79
+ "Rkk4MFFtamNaMDZQeENLZXhYeEo1YXZkUkw0ZU1BMEdDU3FHU0liM0RRRUJDd1VBQTRJQgpBUUNnNlBqVVNTWlYrNGJ2ZWpjVk12" +
80
+ "Z1htS3pmRDk1b3NXbjBjdG5vTUJ4UERhK20rR2crQmNMVDJJbEZBZTNFCktZTXZ1NFQyOTVXUWM5MnJqS1lxVzZjaXJGcHBuZzl1" +
81
+ "RUZXMm1aTGlteGFTbXV0c1RmdEUzc2JNVk1KL1NMWU4KUFY3VEZ2Nm1jQlNJRldYd21CT0lwYmg0QlVjVmZPTlR2ZFNmSXFmeUFW" +
82
+ "eHNxNHh6YzJuYzZoUEJwQW0yMUF5agpUb0MxZXYvVGJESjhWbGxGWmlFVm1XV2xJUDNhTnpBbThTMm1PcHhQQjJXbmFuYVpIU3J2" +
83
+ "WExGaHN0eXp3cmpECnlPMS9pc09aN3d0cjdyY3VUSmRFdnZ2Q2ltT1psa2ZSaGFEb1Rldzl0UTBFMkZWcHp6U2ludzAycW1RMXhJ" +
84
+ "RTkKNS9INVp6SlNQa3BlQUhXRVBuS2t4ZzB2Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0KLS0tLS1CRUdJTiBDRVJUSUZJQ0FU" +
85
+ "RS0tLS0tCk1JSURaVENDQWsyZ0F3SUJBZ0lVVjFGY2t3WEVseWVrMW9uRm5ROWtMN0JrNE44d0RRWUpLb1pJaHZjTkFRRUwKQlFB" +
86
+ "d1FqRUxNQWtHQTFVRUJoTUNRMDR4SWpBZ0JnTlZCQW9NR1VKQ1RDQlVaV05vYm05c2IyZHBaWE1nUTI4dQpMQ0JNZEdReER6QU5C" +
87
+ "Z05WQkFNTUJrSkNUQ0JEUVRBZUZ3MHlNakEwTURRd016UXlNVEZhRncwek1qQTBNREV3Ck16UXlNVEZhTUVJeEN6QUpCZ05WQkFZ" +
88
+ "VEFrTk9NU0l3SUFZRFZRUUtEQmxDUWt3Z1ZHVmphRzV2Ykc5bmFXVnoKSUVOdkxpd2dUSFJrTVE4d0RRWURWUVFEREFaQ1Frd2dR" +
89
+ "MEV3Z2dFaU1BMEdDU3FHU0liM0RRRUJBUVVBQTRJQgpEd0F3Z2dFS0FvSUJBUURMM3BuRGR4R09rNVo2dnVnaVQ0ZHBNMGp1KzNY" +
90
+ "YXR4ejA5VVk3bWJqNHRrSWRieTRICm9lRWRpWVNaamM1TEpuZ0p1Q0h3dEViQkp0MUJyaVJkU1ZyRjZNOUQyVWFCRHlhbUVvMGR4" +
91
+ "d1NhVnhaaURWV0MKZWVDUGRFTHBGWmRFaFNOVGFUNE83emd2Y25Gc2ZITWEvMHZNQWt2RTdpMHFwM21qRXpZTGZ6NjBheGNEb0pM" +
92
+ "awpwN242eEtYSStjSmJBNElsVG9GanBTbGRQbUMreW5PbzdZQU9zWHQ3QVlLWTZHbHowQndVVnpTSnhVKy8rVkZ5Ci9Rcm1ZR053" +
93
+ "bHJRdGRSRUhlUmkwU05LMzJ4MStiT25kZkpQMHNvanVJckRqS3NkQ0x5ZTVDU1pJdnFuYm93d1cKMWpSd1pnVEJSMjlacDJuekNv" +
94
+ "eEpZY1U5VFNRcC80S1p1V05WQWdNQkFBR2pVekJSTUIwR0ExVWREZ1FXQkJTUApORUpvM0dkT2o4UWluc1Y4U2VXcjNVUytIakFm" +
95
+ "QmdOVkhTTUVHREFXZ0JTUE5FSm8zR2RPajhRaW5zVjhTZVdyCjNVUytIakFQQmdOVkhSTUJBZjhFQlRBREFRSC9NQTBHQ1NxR1NJ" +
96
+ "YjNEUUVCQ3dVQUE0SUJBUUFCbEJJVDVaZUcKZmdjSzFMT2gxQ045c1R6eE1DTGJ0VFBGRjFOR0dBMTNtQXB1NmoxaDVZRUxiU0tj" +
97
+ "VXFmWHpNblZlQWIwNkh0dQozQ29Db2Urd2o3TE9OVEZPKyt2Qm0yL2lmNkp0L0RVdzFDQUVjTnlxZWg2RVMwTlg4TEpSVlNlMHFk" +
98
+ "VHhQSnVBCkJkT29vOTZpWDg5clJQb3hlZWQxY3BxNWhad2Jla2EzK0NKR1Y3Nml0V3AzNVVwNXJtbVVxcmx5UU9yL1dheDYKaXRv" +
99
+ "c0l6RzBNZmhnVXpVNTFBMlAvaFNuRDNORE1Ydit3VVkvQXZxZ0lMN3U3ZmJES25rdTFHekVLSWtmSDhobQpSczZkOFNDVTg5eHly" +
100
+ "d3pRMFBSODUzaXJIYXMzV3JIVnFhYjNQK3FOd1IwWWlyTDBRazdYdC9xM08xZ3JpTmcyCkJsYmpnM29icEhvOQotLS0tLUVORCBD" +
101
+ "RVJUSUZJQ0FURS0tLS0t",
102
+ },
103
+ "slicer_base64.cer": {
104
+ sha256: "f504dc1c37bc9741b7a95198914e67f3f6f1d917b1e45eaa14b9e84eb7b51ec8",
105
+ base64:
106
+ "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUY3ekNDQk5lZ0F3SUJBZ0lRQm5XSDBPTmY1dE1XNlZyNXhtWXhEREFOQmdr" +
107
+ "cWhraUc5dzBCQVFzRkFEQloKTVFzd0NRWURWUVFHRXdKVlV6RVZNQk1HQTFVRUNoTU1SR2xuYVVObGNuUWdTVzVqTVRNd01RWURW" +
108
+ "UVFERXlwUwpZWEJwWkZOVFRDQlVURk1nUkZZZ1VsTkJJRTFwZUdWa0lGTklRVEkxTmlBeU1ESXdJRU5CTFRFd0hoY05NakV3Ck56" +
109
+ "RXpNREF3TURBd1doY05Nakl3TnpFek1qTTFPVFU1V2pBWk1SY3dGUVlEVlFRRERBNHFMbUpoYldKMWJHRmkKTG1OdmJUQ0NBU0l3" +
110
+ "RFFZSktvWklodmNOQVFFQkJRQURnZ0VQQURDQ0FRb0NnZ0VCQUt4c3pMZGpBS21TMkpscQpaNTI4WkVsM0VxWkd2MWp1UnZ2TTR4" +
111
+ "cCtvcGVNWENzMWpLc2IxV2ZpZUZBL0tqWEJ6NG51ZVpzRkxhYnB5aXBBCjlvUFBkOG9HajhzWjRoOENJYk1iejJHeG1VVktUb1Ra" +
112
+ "OWYzd1lwMHNXRFVQVHhoWEd3L0p6cXk2bWN3cWhvQ08KUWJWck5hUUNxUXVOcElLTWx2UFg1YUsxQTBRL0FzZE4ybXBWdFNHMENw" +
113
+ "cGNSZjVaRTQyelZGUGcxaWVReGdpSAp3NkxXTGlaM0NHcHRueVAxc0JsSUx6VTQ4cXJoMUQwN1ptdUVITDA1cEo3ZFFBc2hzK0Jr" +
114
+ "bFJ6eXpUZ2lVKzFlCitRZVBGYXExRG9zekZlMndMS3Z1UjhPL1JaOGZGS0pzTnlodE80b21jMklTR3ZSb3lrN3FLU3JGUU9jNG1l" +
115
+ "a1kKMDJzSUZHVUNBd0VBQWFPQ0F2RXdnZ0x0TUI4R0ExVWRJd1FZTUJhQUZLU041YjU4ZWVSd0kyMHVLVFN0STFqYwo5VEYvTUIw" +
116
+ "R0ExVWREZ1FXQkJUUDIrTDR0YjJhOGZsMXBPWSt6RmxBNEdOamtqQW5CZ05WSFJFRUlEQWVnZzRxCkxtSmhiV0oxYkdGaUxtTnZi" +
117
+ "WUlNWW1GdFluVnNZV0l1WTI5dE1BNEdBMVVkRHdFQi93UUVBd0lGb0RBZEJnTlYKSFNVRUZqQVVCZ2dyQmdFRkJRY0RBUVlJS3dZ" +
118
+ "QkJRVUhBd0l3UGdZRFZSMGdCRGN3TlRBekJnWm5nUXdCQWdFdwpLVEFuQmdnckJnRUZCUWNDQVJZYmFIUjBjRG92TDNkM2R5NWth" +
119
+ "V2RwWTJWeWRDNWpiMjB2UTFCVE1JR0ZCZ2dyCkJnRUZCUWNCQVFSNU1IY3dKQVlJS3dZQkJRVUhNQUdHR0doMGRIQTZMeTl2WTNO" +
120
+ "d0xtUnBaMmxqWlhKMExtTnYKYlRCUEJnZ3JCZ0VGQlFjd0FvWkRhSFIwY0RvdkwyTmhZMlZ5ZEhNdVpHbG5hV05sY25RdVkyOXRM" +
121
+ "MUpoY0dsawpVMU5NVkV4VFJGWlNVMEZOYVhobFpGTklRVEkxTmpJd01qQkRRUzB4TG1OeWREQUpCZ05WSFJNRUFqQUFNSUlCCmZn" +
122
+ "WUtLd1lCQkFIV2VRSUVBZ1NDQVc0RWdnRnFBV2dBZGdBcGViN3duams1SWZCV2M1OWpwWGZsdmxkOW5HQUsKK1BsTlhTWmNKVjNI" +
123
+ "aEFBQUFYcWVwZEJsQUFBRUF3QkhNRVVDSUcrQVVPdnNqV1VIMDdBK0FaTkNyVVJackxYLwo2MldSVXcrM2FTWlhxSG8vQWlFQW9r" +
124
+ "c1dpMm13bTZrNEZDZW45VThNeTNJbmVEU1lxbnY3K2JoODhLOExrdHdBCmRnQlJvN0QxL1FGNW5GWnR1RGQ0and5a2Vzd2JKOHYz" +
125
+ "bm9oQ21nMysxSXNGNVFBQUFYcWVwZERWQUFBRUF3QkgKTUVVQ0lRRDZLYk5YRWZaT1pnN1MvdURIRlRWY00xczByemw0ZWtQNFhW" +
126
+ "cHlRSHQ5MkFJZ0pVbndQMzlDY1NjTwppZ1dZSm1yeGpCQTMxR25XajE4THVLeURWMWpYSmNRQWRnQkJ5TXF4M3lKR1NoREdvVG9K" +
127
+ "UW9kZVRqR0xHd1ByCjYwdkhhUENRWXBZRzlnQUFBWHFlcGRCRUFBQUVBd0JITUVVQ0lRQ3Y3dWZlY21CVmhwbVF1aUpFV1R6dFkw" +
128
+ "ck4KbnBnWnZnQlVNL3ppVlo0d1VBSWdRQXVHbUM5KzZzbkQwUklud3R0Ly9WdmRZcERYT2RpVVN3ZG4zdXc0NXNjdwpEUVlKS29a" +
129
+ "SWh2Y05BUUVMQlFBRGdnRUJBTEJSMFhuSFNBRm0zODN3TDM0VGcvZnFjdWtwWUFFTGZ4MG9zczVpCnZGUWhERUZndVovUXhjaDcw" +
130
+ "UmZBTnYyK0hETFVKNUJIWmZFRnpPQVZFc2xlalBEL2xPZFZKZVozWGE3azhXZXAKZDJudi9hVmR1UmlUOUc4Q0JFa3MrVWxXKzlW" +
131
+ "NmwvWVB5YThJMWh4bWRLWm5YWlhrSm1kOUJia1A4SndieHNzcApxUEYzRzlkc1Y1RTBab0RTWkZuOXgzSkpIM1QzNUZkSXJ4b0sv" +
132
+ "K3MvK3ZvbzdPbkRPWkdqekZrRjUrWW00ajZ1Cks2dW9vMVRXSmFDOW1NQ2FiNElneXJ5alNiVlNvNDlROUdsdXRvaGR2aEZrNEhD" +
133
+ "SWRmejJ3M0JYM29XdFJ3LzIKdUtLTHlEamRYeWYzVFlqWWFTYmJhblNmRDEycGQwWUsyejAvK0daUHZoMUtiN1E9Ci0tLS0tRU5E" +
134
+ "IENFUlRJRklDQVRFLS0tLS0KLS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURyekNDQXBlZ0F3SUJBZ0lRQ0R2Z1ZwQkNS" +
135
+ "ckdoZFdySldaSEhTakFOQmdrcWhraUc5dzBCQVFVRkFEQmgKTVFzd0NRWURWUVFHRXdKVlV6RVZNQk1HQTFVRUNoTU1SR2xuYVVO" +
136
+ "bGNuUWdTVzVqTVJrd0Z3WURWUVFMRXhCMwpkM2N1WkdsbmFXTmxjblF1WTI5dE1TQXdIZ1lEVlFRREV4ZEVhV2RwUTJWeWRDQkhi" +
137
+ "RzlpWVd3Z1VtOXZkQ0JEClFUQWVGdzB3TmpFeE1UQXdNREF3TURCYUZ3MHpNVEV4TVRBd01EQXdNREJhTUdFeEN6QUpCZ05WQkFZ" +
138
+ "VEFsVlQKTVJVd0V3WURWUVFLRXd4RWFXZHBRMlZ5ZENCSmJtTXhHVEFYQmdOVkJBc1RFSGQzZHk1a2FXZHBZMlZ5ZEM1agpiMjB4" +
139
+ "SURBZUJnTlZCQU1URjBScFoybERaWEowSUVkc2IySmhiQ0JTYjI5MElFTkJNSUlCSWpBTkJna3Foa2lHCjl3MEJBUUVGQUFPQ0FR" +
140
+ "OEFNSUlCQ2dLQ0FRRUE0anZoRVhMZXFLVFRvMWVxVUtLUEMzZVF5YUtsN2hMT2xsc0IKQ1NETUFaT25UakMzVS9kRHhHa0FWNTNp" +
141
+ "alNMZGh3WkFBSUVKenM0Ymc3L2Z6VHR4UnVMV1pzY0ZzM1luRm85NwpuaDZWZmU2M1NLTUkydGF2ZWd3NUJtVi9TbDBmdkJmNHE3" +
142
+ "N3VLTmQwZjNwNG1WbUZhRzVjSXpKTHYwN0E2RnB0CjQzQy9keEMvL0FIMmhkbW9SQkJZTXFsMUdOWFJvcjVINGlkcTlKb3orRWtJ" +
143
+ "WUl2VVg3UTZoTCtocWtwTWZUN1AKVDE5c2RsNmdTemVSbnR3aTVtM09GQnFPYXN2K3piTVVaQmZIV3ltZU1yL3k3dnJUQzBMVXE3" +
144
+ "ZEJNdG9NMU8vNApnZFc3alZnL3RSdm9TU2lpY05veEJOMzNzaGJ5VEFwT0I2anRTajFldFgramtNT3ZKd0lEQVFBQm8yTXdZVEFP" +
145
+ "CkJnTlZIUThCQWY4RUJBTUNBWVl3RHdZRFZSMFRBUUgvQkFVd0F3RUIvekFkQmdOVkhRNEVGZ1FVQTk1UU5WYlIKVEx0bThLUGlH" +
146
+ "eHZEbDdJOTBWVXdId1lEVlIwakJCZ3dGb0FVQTk1UU5WYlJUTHRtOEtQaUd4dkRsN0k5MFZVdwpEUVlKS29aSWh2Y05BUUVGQlFB" +
147
+ "RGdnRUJBTXVjTjZwSUV4SUsrdDFFbkU5U3NQVGZyZ1QxZVhrSW95UVkvRXNyCmhNQXR1ZFhIL3ZUQkgxakx1RzJjZW5Ubm1DbXJF" +
148
+ "YlhqY0tDaHpVeUltWk9Na1hEaXF3OGN2cE9wLzJQVjVBZGcKMDZPL25Wc0o4ZFdPNDFQMGptUDZQNmZidEdiZlltYlcwVzVCamZJ" +
149
+ "dHRlcDNTcCtkV09JcldjQkFJKzB0S0lKRgpQbmxVa2lhWTRJQklxRGZ2OE5aNVlCYmVyT2dPelc2c1JCYzRMMG5hNFVVK0tyazJV" +
150
+ "ODg2VUFiM0x1akVWMGxzCllTRVkxUVN0ZUR3c09vQnJwK3V2RlJUcDJJbkJ1VGhzNHBGc2l2OWt1WGNsVnpEQUd5U2o0ZHpwMzBk" +
151
+ "OHRiUWsKQ0FVdzdDMjlDNzlGdjFDNXFmUHJtQUVTcmNpSXhwZzBYNDBLUE1icDFaV1ZiZDQ9Ci0tLS0tRU5EIENFUlRJRklDQVRF" +
152
+ "LS0tLS0K",
153
+ },
154
+ });