bambu-printer-mcp 1.1.12 → 1.1.14

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.
@@ -0,0 +1,758 @@
1
+ #include <dlfcn.h>
2
+
3
+ #include <algorithm>
4
+ #include <chrono>
5
+ #include <condition_variable>
6
+ #include <cstdlib>
7
+ #include <cstring>
8
+ #include <fstream>
9
+ #include <functional>
10
+ #include <iostream>
11
+ #include <memory>
12
+ #include <mutex>
13
+ #include <stdexcept>
14
+ #include <sstream>
15
+ #include <string>
16
+ #include <sys/stat.h>
17
+ #include <thread>
18
+
19
+ // This small adapter intentionally mirrors the public C++ ABI declarations
20
+ // used by Bambu Studio's optional networking plug-in. The plug-in is loaded
21
+ // at runtime, so the MCP remains usable on machines without Bambu Studio.
22
+ namespace BBL {
23
+
24
+ using OnLocalConnectedFn = std::function<void(int, std::string, std::string)>;
25
+ using OnUpdateStatusFn = std::function<void(int, int, std::string)>;
26
+ using WasCancelledFn = std::function<bool()>;
27
+
28
+ struct PrintParams {
29
+ std::string dev_id;
30
+ std::string task_name;
31
+ std::string project_name;
32
+ std::string preset_name;
33
+ std::string filename;
34
+ std::string config_filename;
35
+ int plate_index = 0;
36
+ std::string ftp_folder;
37
+ std::string ftp_file;
38
+ std::string ftp_file_md5;
39
+ std::string nozzle_mapping;
40
+ std::string ams_mapping;
41
+ std::string ams_mapping2;
42
+ std::string ams_mapping_info;
43
+ std::string nozzles_info;
44
+ std::string connection_type;
45
+ std::string comments;
46
+ int origin_profile_id = 0;
47
+ int stl_design_id = 0;
48
+ std::string origin_model_id;
49
+ std::string print_type;
50
+ std::string dst_file;
51
+ std::string dev_name;
52
+ std::string dev_ip;
53
+ bool use_ssl_for_ftp = true;
54
+ bool use_ssl_for_mqtt = true;
55
+ std::string username;
56
+ std::string password;
57
+ bool task_bed_leveling = true;
58
+ bool task_flow_cali = true;
59
+ bool task_vibration_cali = true;
60
+ bool task_layer_inspect = false;
61
+ bool task_record_timelapse = false;
62
+ bool task_timelapse_use_internal = false;
63
+ bool task_use_ams = true;
64
+ std::string task_bed_type;
65
+ std::string extra_options;
66
+ int auto_bed_leveling = 0;
67
+ int auto_flow_cali = 0;
68
+ int auto_offset_cali = 0;
69
+ int extruder_cali_manual_mode = -1;
70
+ bool task_ext_change_assist = false;
71
+ bool try_emmc_print = true;
72
+ std::string svc_context;
73
+ std::string slicer_uid;
74
+ std::string queue_plate_id;
75
+ };
76
+
77
+ } // namespace BBL
78
+
79
+ namespace {
80
+
81
+ using FtErr = int;
82
+ struct FT_TunnelHandle;
83
+ using FtAbiVersionFn = int (*)();
84
+ using FtTunnelCreateFn = FtErr (*)(const char *, FT_TunnelHandle **);
85
+ using FtTunnelSyncConnectFn = FtErr (*)(FT_TunnelHandle *);
86
+ using FtTunnelReleaseFn = void (*)(FT_TunnelHandle *);
87
+
88
+ using CreateAgentFn = void *(*)(std::string);
89
+ using DestroyAgentFn = int (*)(void *);
90
+ using InitLogFn = int (*)(void *);
91
+ using SetConfigDirFn = int (*)(void *, std::string);
92
+ using SetCertFileFn = int (*)(void *, std::string, std::string);
93
+ using SetCountryCodeFn = int (*)(void *, std::string);
94
+ using StartFn = int (*)(void *);
95
+ using SetLocalConnectFn = int (*)(void *, BBL::OnLocalConnectedFn);
96
+ using SetPrinterConnectedFn = int (*)(void *, std::function<void(std::string)>);
97
+ using SetMessageFn = int (*)(void *, std::function<void(std::string, std::string)>);
98
+ using SetLocalMessageFn = int (*)(void *, std::function<void(std::string, std::string)>);
99
+ using SetQueueOnMainFn = int (*)(void *, std::function<void(std::function<void()>)>);
100
+ using ConnectPrinterFn = int (*)(void *, std::string, std::string, std::string, std::string, bool);
101
+ using SendMessageToPrinterFn = int (*)(void *, std::string, std::string, int, int);
102
+ using InstallDeviceCertFn = void (*)(void *, std::string, bool);
103
+ using UpdateCertFn = int (*)(void *);
104
+ using StartLocalPrintFn = int (*)(void *, BBL::PrintParams, BBL::OnUpdateStatusFn, BBL::WasCancelledFn);
105
+ using StartSendGcodeToSdcardFn = int (*)(void *, BBL::PrintParams, BBL::OnUpdateStatusFn, BBL::WasCancelledFn, std::function<bool(int, std::string)>);
106
+
107
+ std::mutex output_mutex;
108
+
109
+ void outputLine(const std::string &line) {
110
+ std::lock_guard<std::mutex> lock(output_mutex);
111
+ std::cout << line << std::endl;
112
+ }
113
+
114
+ std::string envOr(const char *name, const std::string &fallback = {}) {
115
+ const char *value = std::getenv(name);
116
+ return value && *value ? std::string(value) : fallback;
117
+ }
118
+
119
+ std::string homeDir() {
120
+ return envOr("HOME", "/Users/Shared");
121
+ }
122
+
123
+ std::string defaultPluginPath() {
124
+ // Bambu Connect ships the current X2D networking plug-in. Prefer it when
125
+ // present; older Bambu Studio installations remain a compatible fallback.
126
+ const std::string connectPlugin =
127
+ "/Applications/Bambu Connect.app/Contents/Resources/app.asar.unpacked/plugins/plugins-mac/libbambu_networking.dylib";
128
+ struct stat connectStat {};
129
+ if (stat(connectPlugin.c_str(), &connectStat) == 0 && S_ISREG(connectStat.st_mode)) {
130
+ return connectPlugin;
131
+ }
132
+ return homeDir() + "/Library/Application Support/BambuStudio/plugins/libbambu_networking.dylib";
133
+ }
134
+
135
+ void *symbol(void *handle, const char *name) {
136
+ return dlsym(handle, name);
137
+ }
138
+
139
+ template <typename T>
140
+ T requiredSymbol(void *handle, const char *name) {
141
+ dlerror();
142
+ void *raw = symbol(handle, name);
143
+ const char *error = dlerror();
144
+ if (!raw || error) {
145
+ throw std::runtime_error(std::string("missing native symbol ") + name +
146
+ (error ? std::string(": ") + error : std::string()));
147
+ }
148
+ return reinterpret_cast<T>(raw);
149
+ }
150
+
151
+ struct NativeApi {
152
+ void *handle = nullptr;
153
+ CreateAgentFn createAgent = nullptr;
154
+ DestroyAgentFn destroyAgent = nullptr;
155
+ InitLogFn initLog = nullptr;
156
+ SetConfigDirFn setConfigDir = nullptr;
157
+ SetCertFileFn setCertFile = nullptr;
158
+ SetCountryCodeFn setCountryCode = nullptr;
159
+ StartFn start = nullptr;
160
+ SetLocalConnectFn setLocalConnect = nullptr;
161
+ SetPrinterConnectedFn setPrinterConnected = nullptr;
162
+ SetMessageFn setMessage = nullptr;
163
+ SetLocalMessageFn setLocalMessage = nullptr;
164
+ SetQueueOnMainFn setQueueOnMain = nullptr;
165
+ ConnectPrinterFn connectPrinter = nullptr;
166
+ SendMessageToPrinterFn sendMessageToPrinter = nullptr;
167
+ InstallDeviceCertFn installDeviceCert = nullptr;
168
+ UpdateCertFn updateCert = nullptr;
169
+ StartLocalPrintFn startLocalPrint = nullptr;
170
+ FtAbiVersionFn ftAbiVersion = nullptr;
171
+ FtTunnelCreateFn ftTunnelCreate = nullptr;
172
+ FtTunnelSyncConnectFn ftTunnelSyncConnect = nullptr;
173
+ FtTunnelReleaseFn ftTunnelRelease = nullptr;
174
+ StartSendGcodeToSdcardFn startSendGcodeToSdcard = nullptr;
175
+
176
+ ~NativeApi() {
177
+ if (handle) dlclose(handle);
178
+ }
179
+ };
180
+
181
+ NativeApi loadApi() {
182
+ const std::string plugin = envOr("BAMBU_NATIVE_PLUGIN", defaultPluginPath());
183
+ void *handle = dlopen(plugin.c_str(), RTLD_NOW | RTLD_LOCAL);
184
+ if (!handle) {
185
+ const char *error = dlerror();
186
+ throw std::runtime_error("could not load Bambu networking plug-in: " + plugin +
187
+ (error ? std::string(" (") + error + ")" : std::string()));
188
+ }
189
+
190
+ NativeApi api;
191
+ api.handle = handle;
192
+ api.createAgent = requiredSymbol<CreateAgentFn>(handle, "bambu_network_create_agent");
193
+ api.destroyAgent = requiredSymbol<DestroyAgentFn>(handle, "bambu_network_destroy_agent");
194
+ api.initLog = requiredSymbol<InitLogFn>(handle, "bambu_network_init_log");
195
+ api.setConfigDir = requiredSymbol<SetConfigDirFn>(handle, "bambu_network_set_config_dir");
196
+ api.setCertFile = requiredSymbol<SetCertFileFn>(handle, "bambu_network_set_cert_file");
197
+ api.setCountryCode = requiredSymbol<SetCountryCodeFn>(handle, "bambu_network_set_country_code");
198
+ api.start = requiredSymbol<StartFn>(handle, "bambu_network_start");
199
+ api.setLocalConnect = requiredSymbol<SetLocalConnectFn>(handle, "bambu_network_set_on_local_connect_fn");
200
+ api.setPrinterConnected = requiredSymbol<SetPrinterConnectedFn>(handle, "bambu_network_set_on_printer_connected_fn");
201
+ api.setMessage = requiredSymbol<SetMessageFn>(handle, "bambu_network_set_on_message_fn");
202
+ api.setLocalMessage = requiredSymbol<SetLocalMessageFn>(handle, "bambu_network_set_on_local_message_fn");
203
+ api.setQueueOnMain = requiredSymbol<SetQueueOnMainFn>(handle, "bambu_network_set_queue_on_main_fn");
204
+ api.connectPrinter = requiredSymbol<ConnectPrinterFn>(handle, "bambu_network_connect_printer");
205
+ api.sendMessageToPrinter = requiredSymbol<SendMessageToPrinterFn>(handle, "bambu_network_send_message_to_printer");
206
+ api.installDeviceCert = requiredSymbol<InstallDeviceCertFn>(handle, "bambu_network_install_device_cert");
207
+ api.updateCert = requiredSymbol<UpdateCertFn>(handle, "bambu_network_update_cert");
208
+ api.startLocalPrint = requiredSymbol<StartLocalPrintFn>(handle, "bambu_network_start_local_print");
209
+ api.startSendGcodeToSdcard = requiredSymbol<StartSendGcodeToSdcardFn>(handle, "bambu_network_start_send_gcode_to_sdcard");
210
+ api.ftAbiVersion = requiredSymbol<FtAbiVersionFn>(handle, "ft_abi_version");
211
+ api.ftTunnelCreate = requiredSymbol<FtTunnelCreateFn>(handle, "ft_tunnel_create");
212
+ api.ftTunnelSyncConnect = requiredSymbol<FtTunnelSyncConnectFn>(handle, "ft_tunnel_sync_connect");
213
+ api.ftTunnelRelease = requiredSymbol<FtTunnelReleaseFn>(handle, "ft_tunnel_release");
214
+ return api;
215
+ }
216
+
217
+ std::string localUrl(const std::string &host, const std::string &token) {
218
+ return "bambu:///local/" + host + "?port=6000&user=bblp&passwd=" + token;
219
+ }
220
+
221
+ struct LocalConnectionState {
222
+ std::mutex mutex;
223
+ std::condition_variable cv;
224
+ bool resolved = false;
225
+ bool ok = false;
226
+ int status = -1;
227
+ std::string message;
228
+ };
229
+
230
+ bool boolEnv(const char *name, bool fallback);
231
+
232
+ void runProbe(NativeApi &api) {
233
+ const std::string host = envOr("BAMBU_NATIVE_HOST");
234
+ const std::string token = envOr("BAMBU_NATIVE_ACCESS_CODE");
235
+ if (host.empty() || token.empty()) {
236
+ throw std::runtime_error("BAMBU_NATIVE_HOST and BAMBU_NATIVE_ACCESS_CODE are required for --probe");
237
+ }
238
+
239
+ // Avoid calling the C++ string-returning symbol in the probe. The tunnel
240
+ // API is C ABI and is the only part needed to verify the X2D route.
241
+ const std::string configDir = envOr("BAMBU_NATIVE_CONFIG_DIR",
242
+ homeDir() + "/Library/Application Support/BambuStudio");
243
+ const std::string certDir = envOr("BAMBU_NATIVE_CERT_DIR",
244
+ "/Applications/BambuStudio.app/Contents/Resources/cert");
245
+ const std::string certFile = envOr("BAMBU_NATIVE_CERT_FILE", "slicer_base64.cer");
246
+ void *agent = api.createAgent(configDir);
247
+ if (!agent) throw std::runtime_error("bambu_network_create_agent returned null in probe");
248
+ int agentResult = api.setConfigDir(agent, configDir);
249
+ if (agentResult == 0) agentResult = api.initLog(agent);
250
+ if (agentResult == 0) agentResult = api.setCertFile(agent, certDir, certFile);
251
+ if (agentResult == 0) agentResult = api.setCountryCode(agent, envOr("BAMBU_NATIVE_COUNTRY", "US"));
252
+ if (agentResult == 0) agentResult = api.start(agent);
253
+ const std::string version = "plugin-loaded";
254
+ const int abi = api.ftAbiVersion();
255
+ FT_TunnelHandle *tunnel = nullptr;
256
+ const FtErr createResult = api.ftTunnelCreate(localUrl(host, token).c_str(), &tunnel);
257
+ if (createResult != 0 || !tunnel) {
258
+ outputLine("probe=failed stage=tunnel_create result=" + std::to_string(createResult));
259
+ return;
260
+ }
261
+
262
+ const FtErr connectResult = api.ftTunnelSyncConnect(tunnel);
263
+ api.ftTunnelRelease(tunnel);
264
+ api.destroyAgent(agent);
265
+ outputLine("probe=local_tunnel version=" + version + " ft_abi=" + std::to_string(abi) +
266
+ " connect_result=" + std::to_string(connectResult));
267
+ }
268
+
269
+ void runMqttProbe(NativeApi &api) {
270
+ const std::string host = envOr("BAMBU_NATIVE_HOST");
271
+ const std::string token = envOr("BAMBU_NATIVE_ACCESS_CODE");
272
+ const std::string serial = envOr("BAMBU_NATIVE_SERIAL");
273
+ if (host.empty() || token.empty() || serial.empty()) {
274
+ throw std::runtime_error("BAMBU_NATIVE_HOST, BAMBU_NATIVE_ACCESS_CODE, and BAMBU_NATIVE_SERIAL are required for --probe-mqtt");
275
+ }
276
+
277
+ const std::string configDir = envOr("BAMBU_NATIVE_CONFIG_DIR",
278
+ homeDir() + "/Library/Application Support/BambuStudio");
279
+ const std::string certDir = envOr("BAMBU_NATIVE_CERT_DIR",
280
+ "/Applications/BambuStudio.app/Contents/Resources/cert");
281
+ const std::string certFile = envOr("BAMBU_NATIVE_CERT_FILE", "slicer_base64.cer");
282
+ void *agent = api.createAgent(configDir);
283
+ if (!agent) throw std::runtime_error("bambu_network_create_agent returned null in MQTT probe");
284
+ const auto destroyAgent = [&]() { api.destroyAgent(agent); };
285
+
286
+ int result = api.setConfigDir(agent, configDir);
287
+ if (result == 0) result = api.initLog(agent);
288
+ if (result == 0) result = api.setCertFile(agent, certDir, certFile);
289
+ if (result == 0) result = api.setCountryCode(agent, envOr("BAMBU_NATIVE_COUNTRY", "US"));
290
+ if (result == 0) result = api.start(agent);
291
+ if (result != 0) {
292
+ destroyAgent();
293
+ throw std::runtime_error("MQTT probe setup failed: " + std::to_string(result));
294
+ }
295
+ result = api.setQueueOnMain(agent, [](std::function<void()> callback) {
296
+ if (callback) callback();
297
+ });
298
+ if (result != 0) {
299
+ destroyAgent();
300
+ throw std::runtime_error("set_queue_on_main_fn failed in MQTT probe: " + std::to_string(result));
301
+ }
302
+
303
+ auto localConnection = std::make_shared<LocalConnectionState>();
304
+ // Bambu Studio installs the printer's application certificate after the
305
+ // networking plug-in reports the printer connected. Without this call the
306
+ // plug-in can upload to eMMC but cannot encrypt the project_file command
307
+ // and returns -4030 (missing device_pub_key_map).
308
+ const int printerCallbackResult = api.setPrinterConnected(
309
+ agent,
310
+ [agent, serial, &api](std::string devId) {
311
+ if (devId.empty() || devId == serial) {
312
+ api.installDeviceCert(agent, serial, boolEnv("BAMBU_NATIVE_LAN_ONLY", true));
313
+ outputLine("native_install_device_cert dev=" + serial);
314
+ }
315
+ });
316
+ if (printerCallbackResult != 0) {
317
+ destroyAgent();
318
+ throw std::runtime_error("set_on_printer_connected_fn failed: " + std::to_string(printerCallbackResult));
319
+ }
320
+ const int messageCallbackResult = api.setMessage(
321
+ agent,
322
+ [agent, serial, &api](std::string devId, std::string message) {
323
+ outputLine("native_message dev=" + devId + " msg=" + message);
324
+ if (message == "wait_info" && (devId.empty() || devId == serial)) {
325
+ api.installDeviceCert(agent, serial, boolEnv("BAMBU_NATIVE_LAN_ONLY", true));
326
+ outputLine("native_install_device_cert dev=" + serial + " stage=wait_info");
327
+ }
328
+ });
329
+ if (messageCallbackResult != 0) {
330
+ destroyAgent();
331
+ throw std::runtime_error("set_on_message_fn failed in MQTT probe: " + std::to_string(messageCallbackResult));
332
+ }
333
+ const int localMessageCallbackResult = api.setLocalMessage(
334
+ agent,
335
+ [agent, serial, &api](std::string devId, std::string message) {
336
+ if (message == "wait_info" && (devId.empty() || devId == serial)) {
337
+ outputLine("native_local_message dev=" + devId + " msg=wait_info");
338
+ api.installDeviceCert(agent, serial, boolEnv("BAMBU_NATIVE_LAN_ONLY", true));
339
+ outputLine("native_install_device_cert dev=" + serial + " stage=wait_info");
340
+ }
341
+ });
342
+ if (localMessageCallbackResult != 0) {
343
+ destroyAgent();
344
+ throw std::runtime_error("set_on_local_message_fn failed in MQTT probe: " + std::to_string(localMessageCallbackResult));
345
+ }
346
+ result = api.updateCert(agent);
347
+ outputLine("native_update_cert result=" + std::to_string(result));
348
+ const int callbackResult = api.setLocalConnect(agent, [localConnection, serial](int status, std::string devId, std::string message) {
349
+ outputLine("mqtt_probe_connect status=" + std::to_string(status) + " dev=" + devId + " msg=" + message);
350
+ if (!devId.empty() && devId != serial) return;
351
+ {
352
+ std::lock_guard<std::mutex> lock(localConnection->mutex);
353
+ localConnection->resolved = true;
354
+ localConnection->ok = status == 0;
355
+ localConnection->status = status;
356
+ localConnection->message = std::move(message);
357
+ }
358
+ localConnection->cv.notify_all();
359
+ });
360
+ if (callbackResult != 0) {
361
+ destroyAgent();
362
+ throw std::runtime_error("set_on_local_connect_fn failed in MQTT probe: " + std::to_string(callbackResult));
363
+ }
364
+ result = api.connectPrinter(agent, serial, host, "bblp", token, true);
365
+ if (result != 0) {
366
+ destroyAgent();
367
+ throw std::runtime_error("connect_printer failed in MQTT probe: " + std::to_string(result));
368
+ }
369
+
370
+ std::unique_lock<std::mutex> lock(localConnection->mutex);
371
+ const bool resolved = localConnection->cv.wait_for(
372
+ lock,
373
+ std::chrono::seconds(10),
374
+ [&localConnection]() { return localConnection->resolved; });
375
+ if (!resolved) {
376
+ lock.unlock();
377
+ destroyAgent();
378
+ throw std::runtime_error("MQTT probe timed out waiting for local-connect callback");
379
+ }
380
+ const int status = localConnection->status;
381
+ const std::string message = localConnection->message;
382
+ const bool ok = localConnection->ok;
383
+ lock.unlock();
384
+ destroyAgent();
385
+ outputLine("probe=mqtt status=" + std::to_string(status) +
386
+ (message.empty() ? std::string{} : " message=" + message));
387
+ if (!ok) std::exit(21);
388
+ }
389
+
390
+ std::string bedTypeForNative(const std::string &bedType) {
391
+ if (bedType == "textured_plate" || bedType == "pte") return "pte";
392
+ if (bedType == "cool_plate" || bedType == "pc") return "pc";
393
+ if (bedType == "engineering_plate" || bedType == "pe") return "pe";
394
+ if (bedType == "hot_plate" || bedType == "pei") return "pei";
395
+ if (bedType == "supertack_plate" || bedType == "suprtack") return "suprtack";
396
+ return bedType;
397
+ }
398
+
399
+ int intEnv(const char *name, int fallback) {
400
+ const std::string value = envOr(name);
401
+ if (value.empty()) return fallback;
402
+ try {
403
+ return std::stoi(value);
404
+ } catch (...) {
405
+ return fallback;
406
+ }
407
+ }
408
+
409
+ bool boolEnv(const char *name, bool fallback) {
410
+ const std::string value = envOr(name);
411
+ if (value.empty()) return fallback;
412
+ return value == "1" || value == "true" || value == "TRUE" || value == "yes";
413
+ }
414
+
415
+ void requestPrinterState(NativeApi &api, void *agent, const std::string &serial) {
416
+ const int pushAll = api.sendMessageToPrinter(
417
+ agent,
418
+ serial,
419
+ R"({"pushing":{"sequence_id":"1","command":"pushall","version":1,"push_target":1}})",
420
+ 0,
421
+ 0);
422
+ outputLine("native_request command=pushall result=" + std::to_string(pushAll));
423
+
424
+ const int version = api.sendMessageToPrinter(
425
+ agent,
426
+ serial,
427
+ R"({"info":{"sequence_id":"2","command":"get_version"}})",
428
+ 1,
429
+ 0);
430
+ outputLine("native_request command=get_version result=" + std::to_string(version));
431
+
432
+ const int accessCode = api.sendMessageToPrinter(
433
+ agent,
434
+ serial,
435
+ R"({"system":{"sequence_id":"3","command":"get_access_code"}})",
436
+ 0,
437
+ 0);
438
+ outputLine("native_request command=get_access_code result=" + std::to_string(accessCode));
439
+ }
440
+
441
+ enum class NativeOperation {
442
+ Print,
443
+ Upload,
444
+ Command,
445
+ AuthorizedCommand,
446
+ };
447
+
448
+ void runOperation(NativeApi &api, NativeOperation operation) {
449
+ const bool uploadOnly = operation == NativeOperation::Upload;
450
+ const bool commandOnly = operation == NativeOperation::Command || operation == NativeOperation::AuthorizedCommand;
451
+ const bool needsAuthorization = operation == NativeOperation::Print || operation == NativeOperation::AuthorizedCommand;
452
+ const char *confirmation = commandOnly ? "BAMBU_NATIVE_COMMAND_CONFIRM" :
453
+ uploadOnly ? "BAMBU_NATIVE_UPLOAD_CONFIRM" : "BAMBU_NATIVE_CONFIRM";
454
+ if (envOr(confirmation) != "1") {
455
+ throw std::runtime_error(std::string("refusing native ") +
456
+ (commandOnly ? "command" : uploadOnly ? "upload" : "print") +
457
+ " without " + confirmation + "=1");
458
+ }
459
+
460
+ const std::string host = envOr("BAMBU_NATIVE_HOST");
461
+ const std::string token = envOr("BAMBU_NATIVE_ACCESS_CODE");
462
+ const std::string serial = envOr("BAMBU_NATIVE_SERIAL");
463
+ const std::string file = envOr("BAMBU_NATIVE_FILE");
464
+ const std::string commandJson = envOr("BAMBU_NATIVE_COMMAND_JSON");
465
+ if (host.empty() || token.empty() || serial.empty() ||
466
+ (commandOnly ? commandJson.empty() : file.empty())) {
467
+ throw std::runtime_error(std::string("native ") +
468
+ (commandOnly ? "command" : uploadOnly ? "upload" : "print") +
469
+ (commandOnly ? " requires host, access code, serial, and command JSON" :
470
+ " requires host, access code, serial, and file"));
471
+ }
472
+ struct stat fileStat {};
473
+ if (!commandOnly && (stat(file.c_str(), &fileStat) != 0 || !S_ISREG(fileStat.st_mode))) {
474
+ throw std::runtime_error(std::string("native ") + (uploadOnly ? "upload" : "print") +
475
+ " file is not readable: " + file);
476
+ }
477
+
478
+ const std::string configDir = envOr("BAMBU_NATIVE_CONFIG_DIR",
479
+ homeDir() + "/Library/Application Support/BambuStudio");
480
+ const std::string certDir = envOr("BAMBU_NATIVE_CERT_DIR",
481
+ "/Applications/BambuStudio.app/Contents/Resources/cert");
482
+ const std::string certFile = envOr("BAMBU_NATIVE_CERT_FILE", "slicer_base64.cer");
483
+ const std::string country = envOr("BAMBU_NATIVE_COUNTRY", "US");
484
+
485
+ void *agent = api.createAgent(configDir);
486
+ if (!agent) throw std::runtime_error("bambu_network_create_agent returned null");
487
+ const auto destroyAgent = [&]() { api.destroyAgent(agent); };
488
+
489
+ const auto authorizeDispatch = [&](int attempt) {
490
+ if (!needsAuthorization) return;
491
+ // Connection/certificate setup and retries can outlive the original
492
+ // preflight. The parent owns the shared safety parser and must approve
493
+ // fresh state at this exact dispatch boundary, on every attempt.
494
+ outputLine("native_dispatch_request=" + std::to_string(attempt));
495
+ std::string authorization;
496
+ if (!std::getline(std::cin, authorization) ||
497
+ authorization != "native_dispatch_authorized=" + std::to_string(attempt)) {
498
+ destroyAgent();
499
+ throw std::runtime_error("native dispatch authorization was missing or rejected");
500
+ }
501
+ };
502
+
503
+ int result = api.setConfigDir(agent, configDir);
504
+ if (result != 0) { destroyAgent(); throw std::runtime_error("set_config_dir failed: " + std::to_string(result)); }
505
+ result = api.initLog(agent);
506
+ if (result != 0) { destroyAgent(); throw std::runtime_error("init_log failed: " + std::to_string(result)); }
507
+ result = api.setCertFile(agent, certDir, certFile);
508
+ if (result != 0) { destroyAgent(); throw std::runtime_error("set_cert_file failed: " + std::to_string(result)); }
509
+ result = api.setCountryCode(agent, country);
510
+ if (result != 0) { destroyAgent(); throw std::runtime_error("set_country_code failed: " + std::to_string(result)); }
511
+ result = api.start(agent);
512
+ if (result != 0) { destroyAgent(); throw std::runtime_error("network start failed: " + std::to_string(result)); }
513
+
514
+ result = api.setQueueOnMain(agent, [](std::function<void()> callback) {
515
+ if (callback) callback();
516
+ });
517
+ if (result != 0) { destroyAgent(); throw std::runtime_error("set_queue_on_main_fn failed: " + std::to_string(result)); }
518
+
519
+ auto localConnection = std::make_shared<LocalConnectionState>();
520
+ const int printerCallbackResult = api.setPrinterConnected(
521
+ agent,
522
+ [agent, serial, &api](std::string devId) {
523
+ if (devId.empty() || devId == serial) {
524
+ api.installDeviceCert(agent, serial, boolEnv("BAMBU_NATIVE_LAN_ONLY", true));
525
+ outputLine("native_install_device_cert dev=" + serial + " stage=printer_connected");
526
+ }
527
+ });
528
+ if (printerCallbackResult != 0) {
529
+ destroyAgent();
530
+ throw std::runtime_error("set_on_printer_connected_fn failed: " + std::to_string(printerCallbackResult));
531
+ }
532
+ const int callbackResult = api.setLocalConnect(agent, [localConnection, serial, agent, &api](int status, std::string devId, std::string message) {
533
+ outputLine("native_connect status=" + std::to_string(status) + " dev=" + devId + " msg=" + message);
534
+ if (!devId.empty() && devId != serial) return;
535
+ if (status == 0) {
536
+ // The local-connect callback is the point at which Bambu Studio
537
+ // installs the device certificate for LAN-only printers.
538
+ api.installDeviceCert(agent, serial, boolEnv("BAMBU_NATIVE_LAN_ONLY", true));
539
+ outputLine("native_install_device_cert dev=" + serial + " stage=local_connect");
540
+ }
541
+ {
542
+ std::lock_guard<std::mutex> lock(localConnection->mutex);
543
+ localConnection->resolved = true;
544
+ localConnection->ok = status == 0;
545
+ localConnection->status = status;
546
+ localConnection->message = std::move(message);
547
+ }
548
+ localConnection->cv.notify_all();
549
+ });
550
+ if (callbackResult != 0) {
551
+ destroyAgent();
552
+ throw std::runtime_error("set_on_local_connect_fn failed: " + std::to_string(callbackResult));
553
+ }
554
+ const int messageCallbackResult = api.setMessage(
555
+ agent,
556
+ [agent, serial, &api](std::string devId, std::string message) {
557
+ if (message == "wait_info" && (devId.empty() || devId == serial)) {
558
+ outputLine("native_message dev=" + devId + " msg=wait_info");
559
+ api.installDeviceCert(agent, serial, boolEnv("BAMBU_NATIVE_LAN_ONLY", true));
560
+ outputLine("native_install_device_cert dev=" + serial + " stage=wait_info");
561
+ }
562
+ });
563
+ if (messageCallbackResult != 0) {
564
+ destroyAgent();
565
+ throw std::runtime_error("set_on_message_fn failed: " + std::to_string(messageCallbackResult));
566
+ }
567
+ const int localMessageCallbackResult = api.setLocalMessage(
568
+ agent,
569
+ [agent, serial, &api](std::string devId, std::string message) {
570
+ // GUI_App::process_network_msg handles this exact signal by
571
+ // installing the device certificate again. The plug-in emits it
572
+ // after the async app-key request, which is the point where its
573
+ // device_pub_key_map becomes usable for project_file.
574
+ if (message == "wait_info" && (devId.empty() || devId == serial)) {
575
+ outputLine("native_local_message dev=" + devId + " msg=wait_info");
576
+ api.installDeviceCert(agent, serial, boolEnv("BAMBU_NATIVE_LAN_ONLY", true));
577
+ outputLine("native_install_device_cert dev=" + serial + " stage=wait_info");
578
+ }
579
+ });
580
+ if (localMessageCallbackResult != 0) {
581
+ destroyAgent();
582
+ throw std::runtime_error("set_on_local_message_fn failed: " + std::to_string(localMessageCallbackResult));
583
+ }
584
+ result = api.updateCert(agent);
585
+ outputLine("native_update_cert result=" + std::to_string(result));
586
+ result = api.connectPrinter(agent, serial, host, "bblp", token, true);
587
+ if (result != 0) { destroyAgent(); throw std::runtime_error("connect_printer failed: " + std::to_string(result)); }
588
+
589
+ // Some plug-in builds emit the local-connect callback without emitting the
590
+ // printer-connected callback. Request installation explicitly as well;
591
+ // the operation is idempotent and is required before encrypted printing.
592
+ api.installDeviceCert(agent, serial, boolEnv("BAMBU_NATIVE_LAN_ONLY", true));
593
+ outputLine("native_install_device_cert dev=" + serial);
594
+ result = api.updateCert(agent);
595
+ outputLine("native_update_cert result=" + std::to_string(result) + " stage=connected");
596
+
597
+ // connect_printer starts the MQTT loop asynchronously. Calling
598
+ // start_local_print immediately races the CONNACK and makes the plug-in
599
+ // report -4030 (publish MQTT message failed), even though the credentials
600
+ // and the printer are valid. Wait for the local-connect callback before
601
+ // handing the job to the plug-in.
602
+ {
603
+ std::unique_lock<std::mutex> lock(localConnection->mutex);
604
+ const bool resolved = localConnection->cv.wait_for(
605
+ lock,
606
+ std::chrono::seconds(10),
607
+ [&localConnection]() { return localConnection->resolved; });
608
+ if (!resolved) {
609
+ lock.unlock();
610
+ destroyAgent();
611
+ throw std::runtime_error(std::string("local MQTT connection timed out before native ") +
612
+ (commandOnly ? "command" : uploadOnly ? "upload" : "print"));
613
+ }
614
+ if (!localConnection->ok) {
615
+ const int status = localConnection->status;
616
+ const std::string message = localConnection->message;
617
+ lock.unlock();
618
+ destroyAgent();
619
+ throw std::runtime_error(
620
+ std::string("local MQTT connection failed before native ") +
621
+ (commandOnly ? "command" : uploadOnly ? "upload" : "print") +
622
+ " status=" + std::to_string(status) +
623
+ (message.empty() ? std::string{} : " message=" + message));
624
+ }
625
+ }
626
+
627
+ // GUI_App sends these immediately after a printer connects. They prompt
628
+ // the device to publish its complete state and security metadata before
629
+ // the encrypted project_file message is built.
630
+ requestPrinterState(api, agent, serial);
631
+
632
+ // Allow the asynchronous certificate exchange and device-public-key map
633
+ // update to complete before start_local_print publishes project_file.
634
+ if (commandOnly) {
635
+ std::this_thread::sleep_for(std::chrono::milliseconds(250));
636
+ const int qos = std::clamp(intEnv("BAMBU_NATIVE_COMMAND_QOS", 0), 0, 1);
637
+ const int flag = std::clamp(intEnv("BAMBU_NATIVE_COMMAND_FLAG", 0), 0, 1);
638
+ authorizeDispatch(1);
639
+ result = api.sendMessageToPrinter(agent, serial, commandJson, qos, flag);
640
+ if (result == -4030 || result == -4) {
641
+ // A protected control may be the first command that asks the
642
+ // printer for its public key. A non-zero synchronous return means
643
+ // it was not queued, so one guarded retry cannot duplicate it.
644
+ const int retryWaitMs = std::max(250, intEnv("BAMBU_NATIVE_CONTROL_RETRY_MS", 1000));
645
+ outputLine("native_retry reason=control_cert_bootstrap wait_ms=" + std::to_string(retryWaitMs));
646
+ std::this_thread::sleep_for(std::chrono::milliseconds(retryWaitMs));
647
+ api.installDeviceCert(agent, serial, boolEnv("BAMBU_NATIVE_LAN_ONLY", true));
648
+ const int retryUpdate = api.updateCert(agent);
649
+ outputLine("native_update_cert result=" + std::to_string(retryUpdate) + " stage=control_retry");
650
+ std::this_thread::sleep_for(std::chrono::milliseconds(250));
651
+ authorizeDispatch(2);
652
+ result = api.sendMessageToPrinter(agent, serial, commandJson, qos, flag);
653
+ }
654
+ outputLine("native_command result=" + std::to_string(result));
655
+ destroyAgent();
656
+ if (result != 0) std::exit(20);
657
+ return;
658
+ }
659
+
660
+ std::this_thread::sleep_for(std::chrono::seconds(3));
661
+
662
+ BBL::PrintParams params;
663
+ const std::string projectName = envOr("BAMBU_NATIVE_PROJECT_NAME", "native-mcp-job");
664
+ params.dev_id = serial;
665
+ params.task_name = envOr("BAMBU_NATIVE_TASK_NAME", projectName);
666
+ params.project_name = projectName;
667
+ params.preset_name = envOr("BAMBU_NATIVE_PRESET_NAME", projectName + "_plate_1");
668
+ params.filename = file;
669
+ params.dst_file = envOr("BAMBU_NATIVE_DST_FILE");
670
+ params.config_filename = envOr("BAMBU_NATIVE_CONFIG_FILE", file);
671
+ params.plate_index = intEnv("BAMBU_NATIVE_PLATE_INDEX", 1);
672
+ params.nozzle_mapping = envOr("BAMBU_NATIVE_NOZZLE_MAPPING");
673
+ params.ams_mapping = envOr("BAMBU_NATIVE_AMS_MAPPING");
674
+ params.ams_mapping2 = envOr("BAMBU_NATIVE_AMS_MAPPING2");
675
+ params.ams_mapping_info = envOr("BAMBU_NATIVE_AMS_MAPPING_INFO");
676
+ params.nozzles_info = envOr("BAMBU_NATIVE_NOZZLES_INFO");
677
+ params.connection_type = "lan";
678
+ params.dev_ip = host;
679
+ params.use_ssl_for_ftp = true;
680
+ params.use_ssl_for_mqtt = true;
681
+ params.username = "bblp";
682
+ params.password = token;
683
+ params.task_bed_leveling = boolEnv("BAMBU_NATIVE_BED_LEVELING", true);
684
+ params.task_flow_cali = boolEnv("BAMBU_NATIVE_FLOW_CALIBRATION", true);
685
+ params.task_vibration_cali = boolEnv("BAMBU_NATIVE_VIBRATION_CALIBRATION", true);
686
+ params.task_layer_inspect = boolEnv("BAMBU_NATIVE_LAYER_INSPECT", false);
687
+ params.task_record_timelapse = boolEnv("BAMBU_NATIVE_TIMELAPSE", false);
688
+ params.task_timelapse_use_internal = boolEnv("BAMBU_NATIVE_TIMELAPSE_INTERNAL", false);
689
+ params.task_use_ams = boolEnv("BAMBU_NATIVE_USE_AMS", true);
690
+ params.task_bed_type = bedTypeForNative(envOr("BAMBU_NATIVE_BED_TYPE", "textured_plate"));
691
+ params.auto_bed_leveling = intEnv("BAMBU_NATIVE_AUTO_BED_LEVELING", 0);
692
+ params.auto_flow_cali = intEnv("BAMBU_NATIVE_AUTO_FLOW_CALI", 0);
693
+ params.auto_offset_cali = intEnv("BAMBU_NATIVE_AUTO_OFFSET_CALI", 0);
694
+ params.extruder_cali_manual_mode = intEnv("BAMBU_NATIVE_EXTRUDER_CALI_MANUAL_MODE", -1);
695
+ params.task_ext_change_assist = boolEnv("BAMBU_NATIVE_EXTERNAL_CHANGE_ASSIST", false);
696
+ params.try_emmc_print = boolEnv("BAMBU_NATIVE_TRY_EMMC_PRINT", true);
697
+ params.print_type = envOr("BAMBU_NATIVE_PRINT_TYPE", "from_normal");
698
+
699
+ const BBL::OnUpdateStatusFn update = [](int status, int code, std::string message) {
700
+ outputLine("native_update status=" + std::to_string(status) + " code=" +
701
+ std::to_string(code) + " msg=" + message);
702
+ };
703
+ const BBL::WasCancelledFn cancel = []() { return false; };
704
+ const auto startAuthorizedPrint = [&](int attempt) {
705
+ authorizeDispatch(attempt);
706
+ return api.startLocalPrint(agent, params, update, cancel);
707
+ };
708
+ if (uploadOnly) {
709
+ const std::function<bool(int, std::string)> wait = [](int, std::string) { return false; };
710
+ result = api.startSendGcodeToSdcard(agent, params, update, cancel, wait);
711
+ } else {
712
+ result = startAuthorizedPrint(1);
713
+ if (result == -4030) {
714
+ // The first encrypted publish can be the plug-in's certificate
715
+ // bootstrap. Keep the agent alive long enough for the printer's
716
+ // response to populate device_pub_key_map, then retry exactly once.
717
+ const int retryWaitSeconds = intEnv("BAMBU_NATIVE_CERT_RETRY_SECONDS", 8);
718
+ outputLine("native_retry reason=cert_bootstrap wait_seconds=" + std::to_string(retryWaitSeconds));
719
+ std::this_thread::sleep_for(std::chrono::seconds(std::max(1, retryWaitSeconds)));
720
+ api.installDeviceCert(agent, serial, boolEnv("BAMBU_NATIVE_LAN_ONLY", true));
721
+ const int retryUpdate = api.updateCert(agent);
722
+ outputLine("native_update_cert result=" + std::to_string(retryUpdate) + " stage=retry");
723
+ std::this_thread::sleep_for(std::chrono::seconds(3));
724
+ result = startAuthorizedPrint(2);
725
+ }
726
+ }
727
+ outputLine(std::string(uploadOnly ? "native_upload" : "native_print") + " result=" + std::to_string(result));
728
+ destroyAgent();
729
+ if (result != 0) std::exit(20);
730
+ }
731
+
732
+ } // namespace
733
+
734
+ int main(int argc, char **argv) {
735
+ try {
736
+ const bool probe = argc == 2 && std::strcmp(argv[1], "--probe") == 0;
737
+ const bool mqttProbe = argc == 2 && std::strcmp(argv[1], "--probe-mqtt") == 0;
738
+ const bool print = argc == 2 && std::strcmp(argv[1], "--print-authorized") == 0;
739
+ const bool upload = argc == 2 && std::strcmp(argv[1], "--upload") == 0;
740
+ const bool command = argc == 2 && std::strcmp(argv[1], "--command") == 0;
741
+ const bool authorizedCommand = argc == 2 && std::strcmp(argv[1], "--command-authorized") == 0;
742
+ if (!probe && !mqttProbe && !print && !upload && !command && !authorizedCommand) {
743
+ std::cerr << "usage: bambu-native-print --probe|--probe-mqtt|--print-authorized|--upload|--command|--command-authorized" << std::endl;
744
+ return 2;
745
+ }
746
+ NativeApi api = loadApi();
747
+ if (probe) runProbe(api);
748
+ if (mqttProbe) runMqttProbe(api);
749
+ if (print) runOperation(api, NativeOperation::Print);
750
+ if (upload) runOperation(api, NativeOperation::Upload);
751
+ if (command) runOperation(api, NativeOperation::Command);
752
+ if (authorizedCommand) runOperation(api, NativeOperation::AuthorizedCommand);
753
+ return 0;
754
+ } catch (const std::exception &error) {
755
+ std::cerr << "native_error=" << error.what() << std::endl;
756
+ return 1;
757
+ }
758
+ }