bambu-printer-mcp 1.1.32 → 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 +5 -1
- package/README.md +4 -2
- package/dist/bambu-network-bridge.d.ts +78 -4
- package/dist/bambu-network-bridge.js +292 -47
- package/dist/bambu-network-certs.d.ts +13 -0
- package/dist/bambu-network-certs.js +152 -0
- package/dist/index.js +28 -11
- package/dist/printers/bambu.d.ts +12 -2
- package/dist/printers/bambu.js +74 -41
- package/dist/safety/printer-state.d.ts +2 -0
- package/dist/safety/printer-state.js +42 -1
- package/dist/slicer/orca-profile-cache.d.ts +18 -0
- package/dist/slicer/orca-profile-cache.js +460 -0
- package/dist/slicer/profile-flatten.js +1 -1
- package/dist/stl/stl-manipulator.d.ts +8 -0
- package/dist/stl/stl-manipulator.js +148 -21
- package/package.json +1 -1
- package/src/bambu-network-bridge.ts +324 -55
- package/src/bambu-network-certs.ts +154 -0
- package/src/index.ts +43 -25
- package/src/printers/bambu.ts +74 -58
- package/src/safety/printer-state.ts +42 -1
- package/src/slicer/orca-profile-cache.ts +379 -0
- package/src/slicer/profile-flatten.ts +1 -1
- package/src/stl/stl-manipulator.ts +156 -16
|
@@ -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
|
-
|
|
48
|
-
|
|
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
|
|
52
|
-
macosMissingPluginFiles
|
|
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
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
|
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
|
+
});
|