# Bambu Studio Network Plugin — full reference This document describes how Bambu Studio integrates with its proprietary **Network Plugin** (`bambu_networking`) — where it is downloaded from, where it is installed, how it is validated, and the exact C ABI contract it must implement. The goal is to document how the plugin is integrated, loaded, validated and invoked, based purely on the Bambu Studio source code. The reference is derived from three independent sources, none of them involving binary disassembly: 1. A read-through of the upstream [bambulab/BambuStudio](https://github.com/bambulab/BambuStudio) (and the closely related [SoftFever/OrcaSlicer](https://github.com/SoftFever/OrcaSlicer)) trees — every Studio-side claim in this document is backed by a concrete file and line range in those sources. 2. **MITM captures** of the stock `libbambu_networking.so` against `api.bambulab.com`, MakerWorld and the printer's LAN MQTT / FTPS / RTSPS endpoints, used to reverse the wire format the closed-source `bambu_networking` binary actually produces (HTTPS bodies, MQTT JSON envelopes, FTPS dialect quirks, etc.). 3. **Cross-ABI matrix runs** against the stock plugin in versions `02.05.00` … `02.06.01` to track how a given `BBL::PrintParams` value is rendered onto the LAN MQTT `print:project_file` payload, and how that mapping shifts as fields are added to `PrintParams` over time. Where a claim originates from MITM or matrix runs rather than Studio source it is marked accordingly (see "Evidence" tags in §6.10.2 and the per-field tables in §6.8.2). Behaviour that has not been confirmed against either source is flagged as such. All source references point at the current BambuStudio tree. --- ## 1. Architecture overview Bambu Studio is a wxWidgets/C++ application. All networking code (Bambu Lab cloud, MQTT/SSDP to printers, print/upload jobs, authentication, OSS, tracking, and so on) lives in a separate **dynamically-loaded library** (`.dll` / `.so` / `.dylib`). Studio talks to it through a single C ABI whose symbols all start with `bambu_network_…`. Key players: | Role | Source | |------|--------| | C ABI declarations (`dlsym` typedefs) | `src/slic3r/Utils/NetworkAgent.hpp` | | Symbol resolver and method wrappers | `src/slic3r/Utils/NetworkAgent.cpp` | | Shared protocol structures / constants | `src/slic3r/Utils/bambu_networking.hpp` | | `ft_*` File Transfer ABI | `src/slic3r/Utils/FileTransferUtils.{hpp,cpp}` | | Module signature verification | `src/slic3r/Utils/CertificateVerify.{hpp,cpp}` | | Lifecycle (URL, download, install, version) | `src/slic3r/GUI/GUI_App.cpp` | | OTA synchronization | `src/slic3r/Utils/PresetUpdater.cpp` | | UI job "download & install" | `src/slic3r/GUI/Jobs/UpgradeNetworkJob.{hpp,cpp}` | | **`libBambuSource` C ABI** (`Bambu_*`) | `src/slic3r/GUI/Printer/BambuTunnel.h` | | **`libBambuSource` loader / shim** | `src/slic3r/GUI/Printer/PrinterFileSystem.cpp` (`StaticBambuLib`) | | **GStreamer source element (Linux only)** | `src/slic3r/GUI/Printer/gstbambusrc.{c,h}` | | **macOS native player wrapper** | `src/slic3r/GUI/wxMediaCtrl2.mm`, `src/slic3r/GUI/BambuPlayer/BambuPlayer.h` | | **Linux wxMediaCtrl shim (gstbambusrc registration)** | `src/slic3r/GUI/wxMediaCtrl2.{cpp,h}` (`__LINUX__` branch) | | **Windows / Linux camera widget — Studio (current)** | `src/slic3r/GUI/wxMediaCtrl3.{cpp,h}` (BambuStudio commit `94d91be60`, June 2024). Drives `Bambu_*` C ABI directly + decodes via `AVVideoDecoder` (FFmpeg). | | **Windows / Linux camera widget — Orca (and pre-`94d91be6` Studio)** | `src/slic3r/GUI/wxMediaCtrl2.{cpp,h}` Windows branch. Drives wxWidgets DirectShow backend → `bambu:` URL scheme → CLSID `{233E64FB-…}` source filter | | **Camera UI panel** | `src/slic3r/GUI/MediaPlayCtrl.{cpp,h}` | | **File browser UI / CTRL protocol consumer** | `src/slic3r/GUI/Printer/PrinterFileSystem.{cpp,h}`, `src/slic3r/GUI/MediaFilePanel.{cpp,h}` | > Note: the code occasionally refers to two further libraries, **`BambuSource`** and **`live555`**. These are the camera/player and the RTSP stack; they are fetched and installed through the exact same mechanism and live next to the main library. The "Network Plugin" contract proper is `bambu_networking`, but a usable Studio installation ALSO needs a working `libBambuSource` for the camera live view *and* the printer file browser. The `libBambuSource` ABI is its own beast (different symbol prefix `Bambu_*`, different loader, per-platform back-ends) — it is documented separately in **§7**. The current Studio version pinned in sources (tag `v02.06.00.51`) is `SLIC3R_VERSION = "02.06.00.51"` (`version.inc`); the expected agent version is `BAMBU_NETWORK_AGENT_VERSION = "02.06.00.50"` (`src/slic3r/Utils/bambu_networking.hpp:100`). --- ## 2. Where the plugin is downloaded from ### 2.1. Base API The URL is built by `GUI_App::get_http_url` based on the `country_code` stored in `app_config`: ```1469:1505:src/slic3r/GUI/GUI_App.cpp std::string GUI_App::get_http_url(std::string country_code, std::string path) { std::string url; if (country_code == "US") { url = "https://api.bambulab.com/"; } else if (country_code == "CN") { url = "https://api.bambulab.cn/"; } // ENV_CN_DEV -> https://api-dev.bambu-lab.com/ // ENV_CN_QA -> https://api-qa.bambu-lab.com/ // ENV_CN_PRE -> https://api-pre.bambu-lab.com/ // NEW_ENV_DEV_HOST -> https://api-dev.bambulab.net/ // NEW_ENV_QAT_HOST -> https://api-qa.bambulab.net/ // NEW_ENV_PRE_HOST -> https://api-pre.bambulab.net/ else { url = "https://api.bambulab.com/"; } url += path.empty() ? "v1/iot-service/api/slicer/resource" : path; return url; } ``` The resulting base is `https://api.bambulab.com/v1/iot-service/api/slicer/resource` (or its regional equivalent). ### 2.2. Manifest request `GUI_App::get_plugin_url` assembles the query parameter `slicer/plugins/cloud=`: ```1545:1556:src/slic3r/GUI/GUI_App.cpp std::string GUI_App::get_plugin_url(std::string name, std::string country_code) { std::string url = get_http_url(country_code); std::string curr_version = SLIC3R_VERSION; std::string using_version = curr_version.substr(0, 9) + "00"; if (name == "cameratools") using_version = curr_version.substr(0, 6) + "00.00"; url += (boost::format("?slicer/%1%/cloud=%2%") % name % using_version).str(); return url; } ``` For the networking plugin the helper is called with `name == "plugins"`. For `SLIC3R_VERSION = "02.06.00.51"` the request becomes: ``` GET https://api.bambulab.com/v1/iot-service/api/slicer/resource?slicer/plugins/cloud=02.06.00.00 ``` ### 2.3. Response format (JSON manifest) The response is parsed in `GUI_App::download_plugin` (see `src/slic3r/GUI/GUI_App.cpp` around lines 1617–1649). The expected shape: ```json { "message": "success", "resources": [ { "type": "slicer/plugins/cloud", "version": "02.05.03.xx", "description": "…changelog…", "url": "https:////plugin.zip", "force_update": false } ] } ``` Studio consumes only `version`, `description`, `url` and `force_update`. `url` points at a ZIP archive that is fetched next. ### 2.4. Special HTTP headers - **`X-BBL-OS-Type`** is temporarily set to `"windows_arm"` when downloading the plugin on Windows ARM64 and restored to `"windows"` after the request: `src/slic3r/GUI/GUI_App.cpp` 1597–1605, 1665–1672 and `src/slic3r/Utils/PresetUpdater.cpp` 1209–1237. - All other "sticky" headers (User-Agent etc.) are registered through `Slic3r::Http::set_extra_headers` and forwarded into the plugin via `bambu_network_set_extra_http_header`. ### 2.5. Background synchronization (OTA) `PresetUpdater::priv::sync_plugins` hits the same HTTP API, but its purpose is to populate the OTA cache rather than install the plugin immediately: ```1165:1253:src/slic3r/Utils/PresetUpdater.cpp void PresetUpdater::priv::sync_plugins(std::string http_url, std::string plugin_version) { ... std::string using_version = curr_version.substr(0, 9) + "00"; auto cache_plugin_folder = cache_path / PLUGINS_SUBPATH; // data_dir/ota/plugins ... std::map resources { {"slicer/plugins/cloud", { using_version, "", "", "", false, cache_plugin_folder.string()}} }; sync_resources(http_url, resources, true, plugin_version, "network_plugins.json"); ... if (result) { if (force_upgrade) { app_config->set("update_network_plugin", "true"); } else { // push notification BBLPluginUpdateAvailable } } } ``` `sync_resources` builds the final URL like this: ```581:583:src/slic3r/Utils/PresetUpdater.cpp std::string url = http_url; url += query_params; Slic3r::Http http = Slic3r::Http::get(url); ``` i.e. identically to `get_plugin_url`. ### 2.6. Download entry points - **Background**: `GUI_App::on_init` → `CallAfter` → `preset_updater->sync(http_url, lang, network_ver, ...)` (`src/slic3r/GUI/GUI_App.cpp` 1333–1340). - **"Download Bambu Network Plug-in" dialog**: `GUI_App::updating_bambu_networking()` (line 1975) → `DownloadProgressDialog` → `UpgradeNetworkJob::process()` (`src/slic3r/GUI/Jobs/UpgradeNetworkJob.cpp` 48–130). - **Manual trigger from the WebView**: event `begin_network_plugin_download` (`src/slic3r/GUI/GUI_App.cpp` ~4078–4090) and `ShowDownNetPluginDlg`. - User-facing wiki article shown on failure: `https://wiki.bambulab.com/en/software/bambu-studio/failed-to-get-network-plugin` (`src/slic3r/GUI/DownloadProgressDialog.cpp` 32–33). --- ## 3. Where it is stored and how it is installed ### 3.1. Working directory (active plugin) Studio loads the binary from **`/plugins/`**. The file name varies by OS: | Platform | Path | |----------|------| | Windows | `\plugins\bambu_networking.dll` | | Windows | `\plugins\BambuSource.dll` (optional, camera) | | Windows | `\plugins\live555.dll` (RTSP/media) | | macOS | `/plugins/libbambu_networking.dylib` | | macOS | `/plugins/libBambuSource.dylib` | | macOS | `/plugins/liblive555.dylib` | | Linux | `/plugins/libbambu_networking.so` | | Linux | `/plugins/libBambuSource.so` | | Linux | `/plugins/liblive555.so` | On Linux `` is usually `~/.config/BambuStudio/` (wxWidgets XDG path), on macOS `~/Library/Application Support/BambuStudio/`, on Windows `%AppData%\BambuStudio\`. The path is computed in `NetworkAgent::initialize_network_module`: ```183:245:src/slic3r/Utils/NetworkAgent.cpp auto plugin_folder = data_dir_path / "plugins"; if (using_backup) plugin_folder = plugin_folder/"backup"; ... #if defined(_MSC_VER) || defined(_WIN32) library = plugin_folder.string() + "\\" + std::string(BAMBU_NETWORK_LIBRARY) + ".dll"; ... networking_module = LoadLibrary(lib_wstr); #else #if defined(__WXMAC__) library = plugin_folder.string() + "/" + std::string("lib") + std::string(BAMBU_NETWORK_LIBRARY) + ".dylib"; #else library = plugin_folder.string() + "/" + std::string("lib") + std::string(BAMBU_NETWORK_LIBRARY) + ".so"; #endif networking_module = dlopen(library.c_str(), RTLD_LAZY); #endif ``` The constant `BAMBU_NETWORK_LIBRARY = "bambu_networking"` lives in `src/slic3r/Utils/bambu_networking.hpp:97`. ### 3.2. Backup copy After a successful unpack `install_plugin` copies every top-level file from `/plugins/` into **`/plugins/backup/`**. If at startup the primary plugin fails to load or is version-incompatible, Studio makes a second attempt with `using_backup=true` — the path then becomes `/plugins/backup/`: ```1874:1905:src/slic3r/GUI/GUI_App.cpp fs::path dir_path(plugin_folder); if (fs::exists(dir_path) && fs::is_directory(dir_path)) { ... for (fs::directory_iterator it(dir_path); it != fs::directory_iterator(); ++it) { if (it->path().string() == backup_folder) continue; auto dest_path = backup_folder.string() + "/" + it->path().filename().string(); if (fs::is_regular_file(it->status())) { ... CopyFileResult cfr = copy_file(it->path().string(), dest_path, error_message, false); } else { copy_framework(it->path().string(), dest_path); } } } ``` The retry logic is in `GUI_App::on_init_network` (`src/slic3r/GUI/GUI_App.cpp` 3421–3459). ### 3.3. OTA cache (staging) All background downloads land in **`/ota/plugins/`** (the constant `PLUGINS_SUBPATH` defined at `PresetUpdater.cpp:57`). That folder is expected to contain **all three** libraries plus a JSON manifest: ```1137:1160:src/slic3r/Utils/PresetUpdater.cpp network_library = cache_folder.string() + "/bambu_networking.dll"; // or .dylib / .so player_library = cache_folder.string() + "/BambuSource.dll"; live555_library = cache_folder.string() + "/live555.dll"; std::string changelog_file = cache_folder.string() + "/network_plugins.json"; if (fs::exists(network_library) && fs::exists(player_library) && fs::exists(live555_library) && fs::exists(changelog_file)) { has_plugins = true; parse_ota_files(changelog_file, cached_version, force, description); } ``` If any of the files is missing, the cache is considered incomplete. ### 3.4. `network_plugins.json` format The JSON is produced by `sync_resources` after unpacking the archive: ```712:723:src/slic3r/Utils/PresetUpdater.cpp json j; j["version"] = resource_update->second.version; j["description"] = resource_update->second.description; j["force"] = resource_update->second.force; boost::nowide::ofstream c; c.open(changelog_file, std::ios::out | std::ios::trunc); c << std::setw(4) << j << std::endl; ``` Minimal valid file: ```json { "version": "02.06.00.50", "description": "…", "force": false } ``` ### 3.5. The "download -> install" flow 1. `UpgradeNetworkJob` (with `name="plugins"` and `package_name="networking_plugins.zip"`, `src/slic3r/GUI/Jobs/UpgradeNetworkJob.cpp:19-20`) calls: - `GUI_App::download_plugin("plugins", "networking_plugins.zip", ...)` — drops the ZIP into `temp_directory_path()/networking_plugins.zip` (a parallel branch in `WebDownPluginDlg` / `GuideFrame` uses the name `network_plugin.zip`). - `GUI_App::install_plugin("plugins", "networking_plugins.zip", ...)` — extracts the archive into **`/plugins/`** while preserving its internal directory hierarchy. 2. On success a flag is written: `app_config["app"]["installed_networking"] = "1"` (`src/slic3r/GUI/GUI_App.cpp` 1906–1909). 3. `restart_networking()` (`src/slic3r/GUI/GUI_App.cpp` 1914–1957) restarts the agent: it calls `on_init_network(try_backup=true)`, resets `StaticBambuLib`, re-registers callbacks and kicks off discovery. ### 3.6. Applying OTA at startup If `update_network_plugin == "true"`, on the next launch — **before** network initialization — Studio copies the freshly downloaded libraries in: ```3359:3418:src/slic3r/GUI/GUI_App.cpp void GUI_App::copy_network_if_available() { if (app_config->get("update_network_plugin") != "true") return; auto plugin_folder = data_dir_path / "plugins"; auto cache_folder = data_dir_path / "ota" / "plugins"; #if defined(_MSC_VER) || defined(_WIN32) const char* library_ext = ".dll"; #elif defined(__WXMAC__) const char* library_ext = ".dylib"; #else const char* library_ext = ".so"; #endif for (auto& dir_entry : boost::filesystem::directory_iterator(cache_folder)) { if (boost::algorithm::iends_with(file_path, library_ext)) { copy_file(file_path, (plugin_folder / file_name).string(), error_message, false); fs::permissions(dest_path, fs::owner_read|fs::owner_write|fs::group_read|fs::others_read); } } fs::remove_all(cache_folder); app_config->set("update_network_plugin", "false"); } ``` Note: only **top-level files whose extension matches the library extension** are copied. Subdirectories and auxiliary files (e.g. certificates) are ignored. The shipped plugin must therefore be "flat" — just the library binary (`bambu_networking.{dll|so|dylib}`) plus, optionally, `BambuSource` and `live555`. ### 3.7. Removal `GUI_App::remove_old_networking_plugins` wipes the **whole** `/plugins/` tree: ```1959:1973:src/slic3r/GUI/GUI_App.cpp void GUI_App::remove_old_networking_plugins() { auto plugin_folder = data_dir_path / "plugins"; if (boost::filesystem::exists(plugin_folder)) { fs::remove_all(plugin_folder); } } ``` --- ## 4. What the plugin is, physically It is a plain native dynamic library with C exports. The calling convention is `cdecl` on Windows (`FT_CALL __cdecl` in `FileTransferUtils.hpp:15`) and the standard System V AMD64 ABI on Linux/macOS. - The main module is **`bambu_networking`** — it implements the entire networking API (`bambu_network_*`) and the file-transfer ABI (`ft_*`). **Both symbol sets live in the same library**: immediately after loading, `NetworkAgent::initialize_network_module` calls `InitFTModule(networking_module)` (`src/slic3r/Utils/NetworkAgent.cpp:276`). - Optional companion modules Studio knows how to pick up: - `BambuSource` — the wrapper for the printer camera stream. Loaded separately through `NetworkAgent::get_bambu_source_entry()` (`src/slic3r/Utils/NetworkAgent.cpp:511-562`); if it fails to load, `m_networking_compatible = false` is set and the user sees "please update the plugin" (`src/slic3r/GUI/GUI_App.cpp:3430-3437`). - `live555` — the classic RTSP library used internally by `BambuSource`. Studio never calls it directly but requires it to be present in the OTA cache (see § 3.3). The ZIP is usually a few MiB. Studio imposes no formal size limit; `install_plugin` simply extracts every file through `miniz` (`mz_zip_…`). No `plugins.json`/`manifest.xml` inside the archive is required. After extraction Studio only reads: - the library itself — via `LoadLibrary`/`dlopen`; - `network_plugins.json` **in the OTA cache** (not in the installed folder); - the symbol `bambu_network_get_version` to determine the version. --- ## 5. Validation ### 5.1. Studio <-> plugin version compatibility The main check is that the first **8 characters** of the version string match, i.e. `MAJOR.MINOR.PATCH` without the build suffix: ```1982:1998:src/slic3r/GUI/GUI_App.cpp bool GUI_App::check_networking_version() { std::string network_ver = Slic3r::NetworkAgent::get_version(); std::string studio_ver = SLIC3R_VERSION; // "02.06.00.51" if (network_ver.length() >= 8) { if (network_ver.substr(0,8) == studio_ver.substr(0,8)) { // "02.06.00" m_networking_compatible = true; return true; } } m_networking_compatible = false; return false; } ``` For `SLIC3R_VERSION = "02.06.00.51"` the plugin must return **a string starting with `"02.06.00"`** (e.g. `"02.06.00.50"`). Otherwise Studio marks it incompatible, sets `m_networking_need_update=true` and pops up the update dialog. > Observation: on Linux this version check is effectively the **only** formal compatibility gate — see § 5.2, where the signature check is a no-op on that platform. The plugin exposes its version through the symbol `bambu_network_get_version` (`func_get_version` typed as `std::string(*)(void)`). See `NetworkAgent::get_version`: ```583:603:src/slic3r/Utils/NetworkAgent.cpp std::string NetworkAgent::get_version() { bool consistent = true; if (check_debug_consistent_ptr) { #if defined(NDEBUG) consistent = check_debug_consistent_ptr(false); #else consistent = check_debug_consistent_ptr(true); #endif } if (!consistent) return "00.00.00.00"; if (get_version_ptr) return get_version_ptr(); return "00.00.00.00"; } ``` A separate consistency check is `bambu_network_check_debug_consistent(bool is_debug)` — it lets the plugin reject a mismatched debug/release build. If it returns `false`, Studio treats the version as `"00.00.00.00"` and refuses to proceed. ### 5.2. Binary signature Before calling `LoadLibrary`/`dlopen` Studio compares the module's publisher with Studio's own publisher: ```190:267:src/slic3r/Utils/NetworkAgent.cpp std::optional self_cert_summary, module_cert_summary; if (validate_cert) self_cert_summary = SummarizeSelf(); ... if (self_cert_summary) { module_cert_summary = SummarizeModule(library); if (module_cert_summary) { if (IsSamePublisher(*self_cert_summary, *module_cert_summary)) networking_module = LoadLibrary(lib_wstr); // (or dlopen) else BOOST_LOG_TRIVIAL(info) << "module is from another publisher..."; } } else { networking_module = LoadLibrary(lib_wstr); // self cert unknown -> load as is } ``` `IsSamePublisher`: ```294:300:src/slic3r/Utils/CertificateVerify.cpp bool IsSamePublisher(const SignerSummary& a, const SignerSummary& b) { if (!a.team_id.empty() && a.team_id == b.team_id) return true; // macOS TeamID if (a.spki_sha256 == b.spki_sha256) return true; // same SPKI if (a.cert_sha256 == b.cert_sha256) return true; // same certificate return false; } ``` - **Windows**: the Authenticode signature of the main `bambu-studio.exe` and of `bambu_networking.dll` must share either an SPKI or a certificate. If the plugin is unsigned, `SummarizeModule` returns `nullopt`, the "error" branch is logged, `networking_module` stays `nullptr`, and the module **will not be loaded**. - **macOS**: the comparison uses the `team_id` (Developer ID). - **Linux**: `SummarizeSelf` / `SummarizeModule` **always return `std::nullopt`** — see: ```289:291:src/slic3r/Utils/CertificateVerify.cpp #else std::optional SummarizeSelf() { return std::nullopt; } std::optional SummarizeModule(const std::string&) { return std::nullopt; } #endif ``` Therefore on Linux `if (self_cert_summary)` is false and Studio takes the "load as is" branch — **the signature is effectively not verified on Linux**. ### 5.3. Bypassing the signature check `AppConfig` exposes a flag **`ignore_module_cert`**, which is forwarded to the `validate_cert` parameter: ```3423:3423:src/slic3r/GUI/GUI_App.cpp int load_agent_dll = Slic3r::NetworkAgent::initialize_network_module(false, !app_config->get_bool("ignore_module_cert")); ``` Setting `ignore_module_cert = 1` in `BambuStudio.conf` disables the publisher check on Windows/macOS entirely. ### 5.4. What "plugin installed" looks like to Studio - A boolean **`installed_networking`** key in `app_config` (section `app`) — set to `"1"` after a successful `install_plugin` (`src/slic3r/GUI/GUI_App.cpp:1906-1909`). This flag drives the "show install/update dialog" logic. - The actual "the plugin works" check is this chain: 1. `LoadLibrary`/`dlopen` returns non-null; 2. `bambu_network_check_debug_consistent` returns `true` for the appropriate build flavor; 3. `bambu_network_get_version` returns a string at least 8 chars long with the right version prefix; 4. `BambuSource` also loaded successfully. ### 5.5. Archive integrity (MD5/SHA) **Not checked.** There is no hash verification of the ZIP anywhere in `download_plugin` / `install_plugin` / `sync_resources` (`src/slic3r/GUI/GUI_App.cpp`, `src/slic3r/Utils/PresetUpdater.cpp`). The only defense-in-depth measure is the binary's own signature. Error codes of the form `BAMBU_NETWORK_ERR_CHECK_MD5_FAILED` (see `src/slic3r/Utils/bambu_networking.hpp:29, 54, 70`) belong to MD5 checks **inside the plugin** during print-job uploads, not to verification of the plugin itself. --- ## 6. The full C ABI contract All symbols are resolved through `GetProcAddress` (Windows) / `dlsym` (Linux, macOS) in `NetworkAgent::get_network_function`: ```564:581:src/slic3r/Utils/NetworkAgent.cpp void* NetworkAgent::get_network_function(const char* name) { if (!networking_module) return nullptr; #if defined(_MSC_VER) || defined(_WIN32) return GetProcAddress(networking_module, name); #else return dlsym(networking_module, name); #endif } ``` Symbol names are not mangled — every function must be declared `extern "C"`. > ABI note: even though this is a C-style interface, the signatures use C++ types (`std::string`, `std::vector`, `std::map`, `std::function`, and custom structs `PrintParams`/`BBLModelTask`/…). The plugin must therefore be built with the same compiler and libstdc++/libc++ standard-library ABI as Bambu Studio itself. It is **not** a pure C ABI — mixing compilers/linkers (e.g. GCC vs. MSVC) is not safe. ### 6.1. Initialization and lifecycle | Symbol | Typedef | Description | |--------|---------|-------------| | `bambu_network_check_debug_consistent` | `bool(*)(bool is_debug)` | Returns `true` if the plugin build matches Studio's build flavor (debug/release). Called before `get_version`. | | `bambu_network_get_version` | `std::string(*)(void)` | Returns the version formatted as `NN.NN.NN.NN`. The first 8 characters must match `SLIC3R_VERSION`. | | `bambu_network_create_agent` | `void*(*)(std::string log_dir)` | Creates an agent instance and returns an opaque handle (`void* agent`). | | `bambu_network_destroy_agent` | `int(*)(void* agent)` | Destroys the agent. | | `bambu_network_init_log` | `int(*)(void* agent)` | Initializes the internal log. | | `bambu_network_set_config_dir` | `int(*)(void*, std::string)` | Configures directory (equal to `data_dir()`). | | `bambu_network_set_cert_file` | `int(*)(void*, std::string folder, std::string filename)` | Studio passes `resources_dir()/cert` and `slicer_base64.cer`. | | `bambu_network_set_country_code` | `int(*)(void*, std::string)` | `"US"`, `"CN"`, … | | `bambu_network_start` | `int(*)(void*)` | Starts the agent's event loop / worker threads. | #### Initialization sequence The Studio-side call order after `create_agent` is deterministic and lives in `GUI_App::on_init_network` (`src/slic3r/GUI/GUI_App.cpp:3461-3510`): 1. `set_config_dir(data_dir())` 2. `init_log()` 3. `set_cert_file(resources_dir()+"/cert", "slicer_base64.cer")` 4. `init_http_extra_header` → `set_extra_http_header(...)` 5. the full `set_on_*_fn(...)` battery (see § 6.2) 6. `set_country_code(country_code)` 7. `start()` 8. `start_discovery(true, false)` The plugin must tolerate this exact order (in particular, no networking work should happen before `start()`). #### 6.1.1. Certificate files (`set_cert_file`) Studio ships **two PEM files** next to each other under `/cert/` inside the AppImage / install tree: | File | What it is (observed) | Used by the **open** plugin today | |------|------------------------|-----------------------------------| | **`slicer_base64.cer`** | BBL / RapidSSL-style bundle for **Bambu cloud** hostnames (`*.bambulab.com`, MakerWorld HTTPS, etc.). This is the file Studio **names** in the ABI call. | **LAN: no.** **Linux cloud MQTT/HTTPS: no** (system CA store). **Windows cloud MQTT only:** passed to Mosquitto as a PEM file because static OpenSSL has no default trust dir — with `tls_skip_chain_verify` (MVP workaround; see `agent.cpp` `connect_cloud`). **Future:** proper Windows cloud TLS verify may use this file or another anchor. | | **`printer.cer`** | BBL **CA** bundle (root + intermediate CAs such as `BBL CA`, `BBL CA2 RSA/ECC`). **Not** the printer's device leaf. Observed **5** CA certs in Studio v02.07.0.55; **does not** include per-model device issuers such as `BBL Device CA N7-V2`. | **LAN TLS verify:** loaded as `OBN_LAN_TLS_CA_FILE` for MQTT (8883), FTPS (990), RTSPS (322), MJPEG (6000). Same folder is remembered from `set_cert_file(folder, …)`; the **filename argument is ignored** for LAN — we always open `folder/printer.cer`. | **Why Studio passes `slicer_base64.cer` in the API if we ignore it on LAN:** the stock closed-source plugin uses that second argument as its cloud trust bundle. The ABI is fixed; Studio always calls `set_cert_file(resources_dir()+"/cert", "slicer_base64.cer")`. We store `folder` + `filename` for ABI compatibility and for the Windows cloud path above. **LAN TLS — what the printer sends (verified on P2S, firmware 02.07.x):** - TLS handshake carries **one certificate**: the **device leaf** (`subject=CN=`, e.g. `CN=22E8BJ610801473`). - **No DNS/IP SAN** on the leaf; clients connect by LAN IP but must check **CN = serial** (SNI is set to the serial). - Leaf **issuer** on N7/P2S: `CN=BBL Device CA N7-V2` (other models may use other `BBL Device CA …` names). - **`printer.cer` alone does not chain-verify** that leaf on N7/P2S — the issuer CA is missing from the Studio bundle (confirmed with `openssl s_client` / `openssl verify`). **How LAN verify actually works in the open plugin:** 1. Load **`printer.cer`** into the OpenSSL trust store (best-effort; needed when/if the issuer **is** present in a future Studio bundle or on other models). 2. After `install_device_cert`, also load the **snapshotted leaf** from `/certs/.pem` (env `OBN_LAN_TLS_PEER_`). The snapshot is taken once per session over MQTT :8883 with verify disabled purely to capture the PEM. With `X509_V_FLAG_PARTIAL_CHAIN`, trusting that leaf PEM makes “this exact cert is OK” work even when the intermediate CA is not shipped. 3. Check **CN = serial** after the handshake (`X509_check_host` / Mosquitto hostname hook). 4. Escape hatch: `OBN_SKIP_TLS_VERIFY=1` disables LAN verify (debug only). **Plain-language summary:** `slicer_base64.cer` = **cloud** (mostly unused on Linux today). `printer.cer` = **intended LAN CA pack** from Bambu, but on current N7 firmware the signing CA is **not** in that pack, so we also **pin the device leaf** we snapshot locally. That is weaker than full PKI to a public root, but it still detects a **wrong/random** cert on the wire (MITM must present the same leaf or break CN=serial). ### 6.2. Callbacks (registration) All take a `void* agent` and an `std::function<…>`: | Symbol | Callback type (from `bambu_networking.hpp`) | |--------|---------------------------------------------| | `bambu_network_set_on_ssdp_msg_fn` | `OnMsgArrivedFn = std::function` | | `bambu_network_set_on_user_login_fn` | `OnUserLoginFn = std::function` | | `bambu_network_set_on_printer_connected_fn` | `OnPrinterConnectedFn = std::function` | | `bambu_network_set_on_server_connected_fn` | `OnServerConnectedFn = std::function` | | `bambu_network_set_on_http_error_fn` | `OnHttpErrorFn = std::function` | | `bambu_network_set_get_country_code_fn` | `GetCountryCodeFn = std::function` | | `bambu_network_set_on_subscribe_failure_fn` | `GetSubscribeFailureFn = std::function` | | `bambu_network_set_on_message_fn` | `OnMessageFn = std::function` | | `bambu_network_set_on_user_message_fn` | `OnMessageFn` | | `bambu_network_set_on_local_connect_fn` | `OnLocalConnectedFn = std::function` | | `bambu_network_set_on_local_message_fn` | `OnMessageFn` | | `bambu_network_set_queue_on_main_fn` | `QueueOnMainFn = std::function)>` — "run this lambda on the GUI thread" | | `bambu_network_set_server_callback` | `OnServerErrFn = std::function` | ### 6.3. Cloud — connection and subscriptions | Symbol | Signature | |--------|-----------| | `bambu_network_connect_server` | `int(void*)` | | `bambu_network_is_server_connected` | `bool(void*)` | | `bambu_network_refresh_connection` | `int(void*)` | | `bambu_network_start_subscribe` | `int(void*, std::string module)` | | `bambu_network_stop_subscribe` | `int(void*, std::string module)` | | `bambu_network_add_subscribe` | `int(void*, std::vector dev_list)` | | `bambu_network_del_subscribe` | `int(void*, std::vector dev_list)` | | `bambu_network_enable_multi_machine` | `void(void*, bool)` | | `bambu_network_send_message` | `int(void*, std::string dev_id, std::string json_str, int qos, int flag)` — MQTT-style call | ### 6.4. Local printer connection (LAN) | Symbol | Signature | |--------|-----------| | `bambu_network_connect_printer` | `int(void*, std::string dev_id, std::string dev_ip, std::string username, std::string password, bool use_ssl)` | | `bambu_network_disconnect_printer` | `int(void*)` | | `bambu_network_send_message_to_printer` | `int(void*, std::string dev_id, std::string json_str, int qos, int flag)` | | `bambu_network_update_cert` | `int(void* agent)` — `func_check_cert`; refreshes certificates at runtime | | `bambu_network_install_device_cert` | `void(void*, std::string dev_id, bool lan_only)` | | `bambu_network_start_discovery` | `bool(void*, bool start, bool sending)` — SSDP | ### 6.5. Authentication and user | Symbol | Signature | |--------|-----------| | `bambu_network_change_user` | `int(void*, std::string user_info)` | | `bambu_network_is_user_login` | `bool(void*)` | | `bambu_network_user_logout` | `int(void*, bool request)` | | `bambu_network_get_user_id` | `std::string(void*)` | | `bambu_network_get_user_name` | `std::string(void*)` | | `bambu_network_get_user_avatar` | `std::string(void*)` | | `bambu_network_get_user_nickanme` | `std::string(void*)` *(the "nickanme" typo is part of the actual ABI!)* | | `bambu_network_build_login_cmd` | `std::string(void*)` | | `bambu_network_build_logout_cmd` | `std::string(void*)` | | `bambu_network_build_login_info` | `std::string(void*)` | | `bambu_network_get_my_profile` | `int(void*, std::string token, unsigned int* http_code, std::string* http_body)` | | `bambu_network_get_my_token` | `int(void*, std::string ticket, unsigned int* http_code, std::string* http_body)` | | `bambu_network_get_user_info` | `int(void*, int* identifier)` | > Known Studio bug (`src/slic3r/Utils/NetworkAgent.cpp:368`): the `get_my_token_ptr` pointer is mistakenly resolved via the string `"bambu_network_get_my_profile"` instead of `"bambu_network_get_my_token"`. Studio still tries to read the `bambu_network_get_my_token` symbol as well, so a compatible plugin must export **both**. Through that pointer Studio will in practice execute the `get_my_profile` body — the two functions must therefore share identical signatures, and any real token-fetching logic ends up running from `get_my_profile`. ### 6.6. Binding / bind | Symbol | Signature | |--------|-----------| | `bambu_network_ping_bind` | `int(void*, std::string ping_code)` | | `bambu_network_bind_detect` | `int(void*, std::string dev_ip, std::string sec_link, detectResult& detect)` | | `bambu_network_bind` | `int(void*, std::string dev_ip, std::string dev_id, std::string sec_link, std::string timezone, bool improved, OnUpdateStatusFn update_fn)` | | `bambu_network_unbind` | `int(void*, std::string dev_id)` | | `bambu_network_request_bind_ticket` | `int(void*, std::string* ticket)` | | `bambu_network_query_bind_status` | `int(void*, std::vector query_list, unsigned int* http_code, std::string* http_body)` | The `detectResult` struct (`src/slic3r/Utils/bambu_networking.hpp:180-189`): ```cpp struct detectResult { std::string result_msg, command, dev_id, model_id, dev_name, version, bind_state, connect_type; }; ``` ### 6.7. Printer selection and metadata | Symbol | Signature | |--------|-----------| | `bambu_network_get_bambulab_host` | `std::string(void*)` | | `bambu_network_get_user_selected_machine` | `std::string(void*)` | | `bambu_network_set_user_selected_machine` | `int(void*, std::string dev_id)` | | `bambu_network_modify_printer_name` | `int(void*, std::string dev_id, std::string dev_name)` | | `bambu_network_get_printer_firmware` | `int(void*, std::string dev_id, unsigned* http_code, std::string* http_body)` | `get_printer_firmware` is invoked from `MachineObject::get_firmware_info` (`src/slic3r/GUI/DeviceManager.cpp:3764`) on a background thread when the user opens **Device → Update**. A return value `< 0` makes Studio silently hide the firmware list (`m_firmware_valid = false`). Otherwise `http_body` is parsed as JSON with the following schema: ```json { "devices": [{ "dev_id": "", "firmware": [ { "version": "01.08.02.00", "url": "https://public-cdn.bblmw.com/upgrade/.../ota.zip", "description": "optional release notes text (plain/markdown)" } ], "ams": [{ "firmware": [ { "version": "00.00.07.89", "url": "https://.../ams.bin", "description": "..." } ] }] }] } ``` Studio creates a `FirmwareInfo item` per entry in `firmware[]` / `ams[].firmware[]` and derives the file name from the tail of `url` (`item.name = url.substr(url.find_last_of('/') + 1)`). If the name cannot be extracted, the entry is skipped. The `description` field is the text displayed in the **Release Notes** dialog. Important: Studio does **not** read the currently installed version from this response — that arrives separately, through the MQTT `info.command=get_version` payload (array `info.module[]`, field `sw_ver`) and `push_status.upgrade_state.new_ver_list`. This ABI call answers only "what can be flashed" (plus, optionally, release notes for those versions). The **Update** button ultimately publishes `{"upgrade":{"command":"upgrade_confirm"}}` over LAN MQTT — the printer itself downloads the firmware from the CDN, and Studio uses the URL in `firmware[].url` only for the displayed file name. When `devices[0].firmware[]` is empty (the currently installed firmware is already the newest one known to the printer), the Release Notes dialog opens empty — this is normal stock behaviour, not a bug. ### 6.8. Submitting a print job Types: - `OnUpdateStatusFn = std::function` - `WasCancelledFn = std::function` - `OnWaitFn = std::function` The `PrintParams` struct (`src/slic3r/Utils/bambu_networking.hpp:192-241`) carries these fields: `dev_id`, `task_name`, `project_name`, `preset_name`, `filename`, `config_filename`, `plate_index`, `ftp_folder`, `ftp_file`, `ftp_file_md5`, `nozzle_mapping`, `ams_mapping`, `ams_mapping2`, `ams_mapping_info`, `nozzles_info`, `connection_type`, `comments`, `origin_profile_id`, `stl_design_id`, `origin_model_id`, `print_type`, `dst_file`, `dev_name`, `dev_ip`, `use_ssl_for_ftp`, `use_ssl_for_mqtt`, `username`, `password`, `task_bed_leveling`, `task_flow_cali`, `task_vibration_cali`, `task_layer_inspect`, `task_record_timelapse`, `task_timelapse_use_internal`, `task_use_ams`, `task_bed_type`, `extra_options`, `auto_bed_leveling`, `auto_flow_cali`, `auto_offset_cali`, `extruder_cali_manual_mode`, `task_ext_change_assist`, `try_emmc_print`. | Symbol | Signature | |--------|-----------| | `bambu_network_start_print` | `int(void*, PrintParams, OnUpdateStatusFn, WasCancelledFn, OnWaitFn)` — cloud | | `bambu_network_start_local_print_with_record` | `int(void*, PrintParams, OnUpdateStatusFn, WasCancelledFn, OnWaitFn)` — LAN + metadata upload | | `bambu_network_start_send_gcode_to_sdcard` | `int(void*, PrintParams, OnUpdateStatusFn, WasCancelledFn, OnWaitFn)` | | `bambu_network_start_local_print` | `int(void*, PrintParams, OnUpdateStatusFn, WasCancelledFn)` — LAN only | | `bambu_network_start_sdcard_print` | `int(void*, PrintParams, OnUpdateStatusFn, WasCancelledFn)` | Print-job stages — the `SendingPrintJobStage` enum (`bambu_networking.hpp:146-156`): `Create=0, Upload=1, Waiting=2, Sending=3, Record=4, WaitPrinter=5, Finished=6, ERROR=7, Limit=8`. #### 6.8.1. Cloud upload flow in the plugin (what actually happens) Both cloud-facing print entry points converge into the same implementation path: - `bambu_network_start_print` -> `Agent::run_cloud_print_job(..., use_lan_channel=false)` - `bambu_network_start_local_print_with_record` -> `Agent::run_cloud_print_job(..., use_lan_channel=true)` The cloud side of the upload then follows this sequence: 1. `POST /v1/iot-service/api/user/project` returns `project_id`, `model_id`, `profile_id`, plus the first presigned `upload_url` and `upload_ticket`. 2. `PUT ` uploads the config 3mf. 3. `PUT /v1/iot-service/api/user/notification` and poll `GET /v1/iot-service/api/user/notification?action=upload&ticket=`. 4. `PATCH /v1/iot-service/api/user/project/` first patch with placeholder `ftp://...` URL (mirrors stock plugin behaviour). 5. `GET /v1/iot-service/api/user/upload?models=__.3mf` returns the second presigned URL for the main print-ready 3mf. 6. `PUT ` uploads the main 3mf. 7. `PATCH /v1/iot-service/api/user/project/` second patch with the real uploaded URL. 8. `POST /v1/user-service/my/task`, then MQTT `project_file` publish. Terminology note: - ABI names still use `OSS` in several places (`bambu_network_get_oss_config`, `...UPLOAD_3MF_TO_OSS...` error codes), but the observed cloud print upload transport in this implementation is presigned object-storage `PUT` URLs (S3-style semantics in code/comments), not a plugin-side fixed OSS endpoint. #### 6.8.2. The MQTT `project_file` command (wire format) The print-job submission ends with a single MQTT command published to the printer's request channel (`device//request` on LAN, identical envelope on the cloud forwarder). The frame the firmware actually parses lives under the `print` object. The example below was captured on a real P2S running stock firmware paired with the stock Studio + plugin; X1 / P1S share the schema with a few extra optional members. ```json { "print": { "sequence_id": "20006", "command": "project_file", "param": "Metadata/plate_1.gcode", "project_id": "0", "profile_id": "0", "task_id": "0", "subtask_id": "0", "subtask_name": "test", "file": "test.gcode.3mf", "url_enc": "bcXiq4/uHGqgb4DXVihrpQOR…", "md5": "7947606528CE6E00219496B51D5D13D1", "bed_type": "textured_plate", "bed_leveling": false, "flow_cali": false, "vibration_cali": false, "layer_inspect": true, "timelapse": true, "use_ams": true, "ams_mapping": [3,-1,-1], "ams_mapping2": [{"ams_id":0,"slot_id":3},{"ams_id":255,"slot_id":255},{"ams_id":255,"slot_id":255}], "auto_bed_leveling": 0, "cfg": "4", "extrude_cali_flag": 0, "extrude_cali_manual_mode":0, "nozzle_offset_cali": 2 } } ``` | Field | Type | Source in `PrintParams` (Studio -> ABI) | Notes | |---|---|---|---| | `sequence_id` | string (decimal) | plugin-generated | Wall-clock millisecond counter; the printer echoes it on the matching ack. | | `command` | string | constant `"project_file"` | Selects the firmware handler. | | `param` | string | derived from `plate_index` | Always `Metadata/plate_.gcode`; resolves to a path inside the uploaded 3mf. | | `project_id`, `profile_id`, `task_id`, `subtask_id` | strings | cloud task IDs from `POST /v1/user-service/my/task` (cloud) or `"0"` placeholder (LAN / Developer Mode) | Sent as **strings**, not numbers. | | `subtask_name` | string | `project_name` (falls back to `task_name`) | Shown on the printer screen. | | `file` | string | `opts.file_path` | Filename (LAN: full FTP path of the uploaded 3mf; cloud: object name in the presigned bucket). | | `url` / `url_enc` | string | `opts.url` | The printer needs to know where to fetch the 3mf from. Two mutually exclusive shapes: `url` is the cleartext source (`ftp://` for LAN, presigned `https://…` for cloud); `url_enc` is the same URL encrypted with AES under a per-print key, the AES key in turn wrapped with RSA-OAEP against the printer's device certificate, and the result Base64-encoded. Stock-firmware printers outside Developer Mode require `url_enc`; Developer Mode disables that check and accepts plain `url`. | | `md5` | string | `opts.md5` (uppercase hex) | Printer cross-checks the 3mf integrity before slicing it. | | `bed_type` | string | `task_bed_type` (defaults to `"auto"`) | One of the `MachineBedTypeString` values. | | `bed_leveling`, `flow_cali`, `vibration_cali`, `layer_inspect`, `timelapse`, `use_ams` | bool | matching `task_*` flags | Per-print toggles from the `SelectMachineDialog` checkboxes. | | `ams_mapping` | int array | `ams_mapping` (Studio passes a JSON array string `[0,-1,...]`) | Index = filament slot in the 3mf, value = AMS tray id (`-1` = no mapping, `255` = virtual tray). Legacy single-AMS shape. | | `ams_mapping2` | object array | `ams_mapping2` (Studio passes a JSON-array string of `{ams_id, slot_id}` objects) | Newer multi-AMS schema, one entry per filament. `255/255` means "external spool / not via AMS". Required for printers with >1 AMS unit; firmware that supports both fields prefers `ams_mapping2` over `ams_mapping`. | | `nozzle_mapping` | int array | `nozzle_mapping` (Studio passes a JSON-array string of ints, e.g. `"[0,1]"`) | Index = filament slot in the 3mf, value = physical nozzle/extruder position id. **Emitted only on multi-extruder printers** — Studio gates every assignment to `task_nozzle_mapping` on `MachineObject::GetNozzleRack()->IsSupported()` (see `SelectMachine.cpp:3107` and `CalibUtils.cpp:2225`/`2358`), so on single-nozzle hardware (P2S, X1C, A1, etc.) the field is omitted entirely from `project_file`. The Studio-side value is sourced verbatim from the printer's reply to the `get_auto_nozzle_mapping` MQTT query (`DevMappingNozzle.cpp:277-282` deserialises the `mapping` field into `std::vector`), so the type is **always** a flat `int` array — no objects, no string-wrapped numbers, no `[0,-1,…]` sentinels analogous to `ams_mapping`. Stock-plugin observation: when Studio hands it a string with whitespace (e.g. `"[0,1, 2]"`), the stock plugin re-emits it as canonical `"[0,1,2]"`; this normalization is cosmetic and not required for firmware acceptance. | | `auto_bed_leveling` | int | `auto_bed_leveling` | Bed-leveling option as an int (0 = off, 1 = on, 2 = auto). Coexists with the boolean `bed_leveling`: the boolean is the user toggle, the int is the resolved policy after taking firmware capabilities into account. | | `nozzle_offset_cali` | int | `auto_offset_cali` (**name mismatch is intentional in upstream**) | Nozzle-offset calibration option (0 = off, 2 = auto). | | `extrude_cali_manual_mode` | int | `extruder_cali_manual_mode` (**`extrude` vs `extruder` asymmetry is intentional in upstream**) | PA calibration mode (0 = automatic, 1 = manual). **Field is gated on the value, not the ABI** — the stock plugin emits it only when `extruder_cali_manual_mode != -1` (the `PrintParams` default). With the default sentinel it is omitted entirely from `project_file`. Confirmed across ABI `02.05.00`, `02.05.03`, `02.06.01`. | | `cfg` | string (decimal int) | derived from `task_timelapse_use_internal` (other bits unknown) | Bitmask the stock plugin builds from `PrintParams` flags that don't have a dedicated MQTT field. **Bit 2 (`0x4`) = use internal storage for timelapse**, driven by `task_timelapse_use_internal` (`task_timelapse_use_internal=true` -> `"4"`, `false` -> `"0"`). Always emitted as a string, not a number. **Field is present in `project_file` for *every* observed ABI from `02.05.00` upwards** — but on builds older than `02.05.03` (where `PrintParams::task_timelapse_use_internal` does not exist yet) the value is permanently `"0"` and the plugin has no way to surface a `"4"`. Cross-ABI confirmation: `02.05.00`/`02.05.01`/`02.05.02` always emit `cfg="0"`; `02.05.03`/`02.06.00`/`02.06.01` emit `cfg="4"` when `task_timelapse_use_internal=true` and `cfg="0"` otherwise. **`task_record_timelapse` does NOT influence `cfg`** — only the `timelapse` boolean in the same payload. No other bit values have been observed in the wild yet — a future capture showing e.g. `"1"`, `"2"` or `"5"` would identify another flag's meaning. | | `extrude_cali_flag` | int | derived from `auto_flow_cali` (1 = enabled, 0 = disabled) | Stock-plugin-only field present in every observed `project_file`. **The value is taken straight from `PrintParams::auto_flow_cali`**: setting `auto_flow_cali=1` flips `extrude_cali_flag` from `0` to `1` across ABI `02.05.00` and `02.06.01`. Captured Studio sessions almost always ship `auto_flow_cali=0`, which is why the field looks hardcoded at first glance. Likely a "PA cali requested" guard the firmware uses to short-circuit redundant calibration runs. | Sibling `header` object the stock plugin wraps the command in (cloud and LAN alike when paired against signature-checking firmware): ```json { "header": { "cert_id": "a4e8faaa…CN=GLOF3813734089.bambulab.com", "payload_len": 1313, "sign_alg": "RSA_SHA256", "sign_string": "ycWyeOUZFB…==", "sign_ver": "v1.0" }, "print": { … } } ``` `cert_id` identifies the device certificate used for signing (its Subject DN includes `CN=.bambulab.com`); `payload_len` is the byte length of the serialized `print` object; `sign_string` is the Base64 RSA-SHA256 signature of that exact payload computed with the per-install private key shipped inside the stock plugin's obfuscated blob; `sign_ver` versions the canonicalization rules. Non-Developer-Mode firmware rejects unsigned `project_file` (and other privileged) commands with `MQTT Command verification failed` (error `84033543`). Developer Mode bypasses the check entirely (see "Developer Mode requirement" in `README.md`), making the `header` envelope optional. Other `PrintParams` members Studio populates but **does not put into the MQTT command** itself: `nozzles_info`, `ams_mapping_info`, `extra_options`, `task_ext_change_assist`, `try_emmc_print`, `comments`. They feed the cloud-side `POST /v1/user-service/my/task` body and the timelapse-storage preflight (see below), not `project_file`. (`task_timelapse_use_internal` *does* reach the MQTT command, but indirectly — see the `cfg` row in the table above.) ##### Cross-ABI `project_file` reverse-engineering matrix The mapping above was verified by loading the stock `libbambu_networking.so` of each version below into a `BBL::PrintParams`-driven harness and capturing the resulting LAN MQTT `project_file` payload against an N7 in Developer Mode: | ABI tag | Plugin file | `cfg` baseline behaviour | New schema field(s) vs the prior tag | |---|---|---|---| | `02.05.00` | `02.05.00.56` | always `"0"` | (reference) | | `02.05.01` | `02.05.01.52` | always `"0"` | none | | `02.05.02` | `02.05.02.58` | always `"0"` | none | | `02.05.03` | `02.05.03.63` | `"4"` if `task_timelapse_use_internal=true`, else `"0"` | `cfg` becomes settable (the `PrintParams::task_timelapse_use_internal` ABI break) | | `02.06.00` | `02.06.00.50` | same as `02.05.03` | none in `project_file`; only new exports are filament-spool CRUD (`bambu_network_get_filament_spools`, `bambu_network_create_filament_spool`, `bambu_network_update_filament_spool`, `bambu_network_delete_filament_spools`, `bambu_network_get_filament_config`) — those are HTTP plumbing, not LAN MQTT | | `02.06.01` | `02.06.01.50` | same as `02.05.03` | none | **Bottom line:** the on-the-wire `project_file` schema is essentially frozen across `02.05.x` -> `02.06.x`. The only ABI-significant change for LAN print submission is the addition of `task_timelapse_use_internal` in `02.05.03`, which feeds the `cfg` bitmask (and only bit `0x4` has been observed so far). All field mappings below were also confirmed identical between `02.05.00` and `02.06.01` — only the `cfg` value differs between the two ABI families. ##### Per-`PrintParams`-field mapping (overlay matrix on ABI `02.05.03`) The following matrix was collected by toggling one `PrintParams` field at a time before handing the struct to the stock plugin and diffing the resulting `project_file` against an all-defaults baseline. It confirms — and in some cases corrects — the per-row notes in the table above. | `PrintParams` field overridden | Resulting `project_file` change | |---|---| | `task_use_ams=true`, `ams_mapping="[0]"`, `ams_mapping2='[{"ams_id":0,"slot_id":0}]'` | `use_ams: false → true`, `ams_mapping: [] → [0]`, `ams_mapping2: [] → [{"ams_id":0,"slot_id":0}]` | | `task_record_timelapse=false` (and `task_timelapse_use_internal=false`) | `timelapse: true → false`, `cfg: "4" → "0"` | | `task_timelapse_use_internal=false` (alone) | `cfg: "4" → "0"` (the `timelapse` boolean stays `true`) | | `task_bed_type="textured_plate"` | `bed_type: "auto" → "textured_plate"` | | `task_bed_type="supertack_plate"` | `bed_type: "auto" → "supertack_plate"` | | `task_bed_leveling=false`, `task_flow_cali=false`, `task_vibration_cali=false`, `task_layer_inspect=false` | All four matching booleans flip to `false` (`bed_leveling`, `flow_cali`, `vibration_cali`, `layer_inspect`) — direct one-to-one mapping | | `extruder_cali_manual_mode=0` | `extrude_cali_manual_mode` **appears** with value `0` (absent in baseline because `-1` is the default) | | `extruder_cali_manual_mode=1` | `extrude_cali_manual_mode` appears with value `1` | | `auto_offset_cali=2` | `nozzle_offset_cali: 0 → 2` (note the upstream rename) | | `auto_bed_leveling=2` | `auto_bed_leveling: 0 → 2` (coexists with the `bed_leveling` boolean) | | `auto_flow_cali=1` | `extrude_cali_flag: 0 → 1` (this is how the previously mysterious `extrude_cali_flag` is populated) | | `project_name="my-print"` | `subtask_name`, `file`, and `url` all switch in lock-step (``, `.gcode.3mf`, `ftp://.gcode.3mf`) — the FTP upload path follows suit | | `plate_index=2` | `param: "Metadata/plate_1.gcode" → "Metadata/plate_2.gcode"` | Fields that did **not** appear on this hardware (P2S/N7 single-extruder) regardless of overrides: `nozzle_mapping` (gated on multi-extruder, see the dedicated row above), `url_enc` (Developer Mode bypasses the encrypted-URL requirement), and the entire `header` envelope (Developer Mode disables signature verification — see §6.8.2 above). #### 6.8.3. Timelapse-storage preflight (`ipcam_get_media_info`) Right before `project_file` Studio runs a preflight against the printer's storage when the user enabled timelapse and the printer reports `is_support_internal_timelapse`. The check is **driven by Studio, not by the plugin**: `SelectMachineDialog::start_timelapse_storage_check()` -> `MachineObject::command_ipcam_check_timelapse_storage(storage, total_layer)` -> `MachineObject::publish_json()`, which routes through `bambu_network_send_message_to_printer` (LAN) or `bambu_network_send_message` (cloud) with this opaque payload: ```json { "camera": { "command": "ipcam_get_media_info", "sub_command": "is_timelapse_storage_enough", "sequence_id": "20021", "storage": "internal", "total_layer": 50 } } ``` The printer answers on the same channel with `result`, `is_enough`, `file_count`; Studio reads them in `DeviceManager.cpp:3548-3553` and either proceeds with `on_send_print()` or shows the "free up storage" dialog. From the plugin's perspective the frame is opaque — it is neither parsed nor formatted plugin-side, only forwarded byte-for-byte by the generic `send_message*` ABI calls. ### 6.9. User presets | Symbol | Signature | |--------|-----------| | `bambu_network_get_user_presets` | `int(void*, std::map>* user_presets)` | | `bambu_network_request_setting_id` | `std::string(void*, std::string name, std::map* values_map, unsigned int* http_code)` | | `bambu_network_put_setting` | `int(void*, std::string setting_id, std::string name, std::map* values_map, unsigned int* http_code)` | | `bambu_network_get_setting_list` | `int(void*, std::string bundle_version, ProgressFn, WasCancelledFn)` | | `bambu_network_get_setting_list2` | `int(void*, std::string bundle_version, CheckFn, ProgressFn, WasCancelledFn)` | | `bambu_network_delete_setting` | `int(void*, std::string setting_id)` | `CheckFn = std::function)>`, `ProgressFn = std::function`. All six entry points are thin wrappers over one REST resource — ``` /v1/iot-service/api/slicer/setting[/]?version=&public=false ``` — on the cloud API host; see §6.10.1 for base URL, headers and the common response envelope that apply here and to every other HTTP endpoint the plugin touches. Preset IDs are prefixed by type (observed in live responses): | Type | ID prefix | Public counterpart | |------|-----------|---------------------| | `print` (process) | `PPUS…` | `GP…` | | `filament` | `PFUS…` | `GFS…` / `GFL…` | | `printer` (machine) | `PMUS…` | `GM…` | #### 6.9.1. Per-method schema **`GET /slicer/setting?public=false&version=` — list metadata** (called from `get_setting_list` / `get_setting_list2`): ```json { "message": "success", "code": null, "error": null, "print": { "private": [ /* Meta, … */ ], "public": [] }, "printer": { "private": [ /* Meta, … */ ], "public": [] }, "filament": { "private": [ /* Meta, … */ ], "public": [] }, "settings": [] } ``` Every entry in `private[]` is metadata only — no `setting` payload: ```json { "setting_id": "PFUS7bf6d4b8df15d8", "name": "Bambu PLA Tough @BBL P1P 0.2 nozzle", "version": "0.0.0.0", "update_time": "2026-04-06 19:03:50", "base_id": null, "filament_id": null, "filament_vendor": null, "filament_type": null, "filament_is_support": null, "nozzle_temperature": null, "nozzle_hrc": null, "inherits": null, "nickname": null } ``` `update_time` is rendered as `"YYYY-MM-DD HH:MM:SS"` in UTC; `load_user_preset()` expects unix seconds, so the plugin converts. **`GET /slicer/setting/` — full preset** (observed only by direct probe; the stock plugin does **not** call it): ```json { "message": "success", "code": null, "error": null, "setting_id": "PFUS7bf6d4b8df15d8", "name": "Bambu PLA Tough @BBL P1P 0.2 nozzle", "type": "filament", "version": "0.0.0.0", "base_id": null, "filament_id": null, "nickname": null, "update_time": "2026-04-06 19:03:50", "public": false, "setting": { "activate_air_filtration": 0, "compatible_printers": "\"Bambu Lab P1P 0.2 nozzle\"", "filament_type": "\"PLA\"", "...": "..." } } ``` Values inside `setting` are already in the `ConfigOption::serialize()` form Studio's loader expects (quoted scalars, semicolon-separated lists, etc.). Some keys are echoed as native JSON numbers instead of strings; callers coerce. The `user_id` of the owner is **not** returned by either endpoint — `PresetCollection::load_user_preset()` requires it, so callers must inject their own from the authenticated session. **`POST /slicer/setting` — create** (called from `request_setting_id`). Request: ```json { "name": "", "type": "filament|print|printer", "version": "", "base_id": "", "filament_id": "", "setting": { "