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:
- A read-through of the upstream bambulab/BambuStudio (and the closely related SoftFever/OrcaSlicer) trees — every Studio-side claim in this document is backed by a concrete file and line range in those sources.
- MITM captures of the stock
libbambu_networking.soagainstapi.bambulab.com, MakerWorld and the printer's LAN MQTT / FTPS / RTSPS endpoints, used to reverse the wire format the closed-sourcebambu_networkingbinary actually produces (HTTPS bodies, MQTT JSON envelopes, FTPS dialect quirks, etc.). - Cross-ABI matrix runs against the stock plugin in versions
02.05.00…02.06.01to track how a givenBBL::PrintParamsvalue is rendered onto the LAN MQTTprint:project_filepayload, and how that mapping shifts as fields are added toPrintParamsover 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.
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,
BambuSourceandlive555. 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 isbambu_networking, but a usable Studio installation ALSO needs a workinglibBambuSourcefor the camera live view and the printer file browser. ThelibBambuSourceABI is its own beast (different symbol prefixBambu_*, 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).
The URL is built by GUI_App::get_http_url based on the country_code stored in app_config:
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).
GUI_App::get_plugin_url assembles the query parameter slicer/plugins/cloud=<ver>:
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
The response is parsed in GUI_App::download_plugin (see src/slic3r/GUI/GUI_App.cpp around lines 1617–1649). The expected shape:
{
"message": "success",
"resources": [
{
"type": "slicer/plugins/cloud",
"version": "02.05.03.xx",
"description": "…changelog…",
"url": "https://<cdn>/<path>/plugin.zip",
"force_update": false
}
]
}Studio consumes only version, description, url and force_update. url points at a ZIP archive that is fetched next.
X-BBL-OS-Typeis temporarily set to"windows_arm"when downloading the plugin on Windows ARM64 and restored to"windows"after the request:src/slic3r/GUI/GUI_App.cpp1597–1605, 1665–1672 andsrc/slic3r/Utils/PresetUpdater.cpp1209–1237.- All other "sticky" headers (User-Agent etc.) are registered through
Slic3r::Http::set_extra_headersand forwarded into the plugin viabambu_network_set_extra_http_header.
PresetUpdater::priv::sync_plugins hits the same HTTP API, but its purpose is to populate the OTA cache rather than install the plugin immediately:
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<std::string, Resource> 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:
std::string url = http_url;
url += query_params;
Slic3r::Http http = Slic3r::Http::get(url);i.e. identically to get_plugin_url.
- Background:
GUI_App::on_init→CallAfter→preset_updater->sync(http_url, lang, network_ver, ...)(src/slic3r/GUI/GUI_App.cpp1333–1340). - "Download Bambu Network Plug-in" dialog:
GUI_App::updating_bambu_networking()(line 1975) →DownloadProgressDialog→UpgradeNetworkJob::process()(src/slic3r/GUI/Jobs/UpgradeNetworkJob.cpp48–130). - Manual trigger from the WebView: event
begin_network_plugin_download(src/slic3r/GUI/GUI_App.cpp~4078–4090) andShowDownNetPluginDlg. - User-facing wiki article shown on failure:
https://wiki.bambulab.com/en/software/bambu-studio/failed-to-get-network-plugin(src/slic3r/GUI/DownloadProgressDialog.cpp32–33).
Studio loads the binary from <data_dir>/plugins/. The file name varies by OS:
| Platform | Path |
|---|---|
| Windows | <data_dir>\plugins\bambu_networking.dll |
| Windows | <data_dir>\plugins\BambuSource.dll (optional, camera) |
| Windows | <data_dir>\plugins\live555.dll (RTSP/media) |
| macOS | <data_dir>/plugins/libbambu_networking.dylib |
| macOS | <data_dir>/plugins/libBambuSource.dylib |
| macOS | <data_dir>/plugins/liblive555.dylib |
| Linux | <data_dir>/plugins/libbambu_networking.so |
| Linux | <data_dir>/plugins/libBambuSource.so |
| Linux | <data_dir>/plugins/liblive555.so |
On Linux <data_dir> 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:
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);
#endifThe constant BAMBU_NETWORK_LIBRARY = "bambu_networking" lives in src/slic3r/Utils/bambu_networking.hpp:97.
After a successful unpack install_plugin copies every top-level file from <data_dir>/plugins/ into <data_dir>/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 <data_dir>/plugins/backup/:
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).
All background downloads land in <data_dir>/ota/plugins/ (the constant PLUGINS_SUBPATH defined at PresetUpdater.cpp:57). That folder is expected to contain all three libraries plus a JSON manifest:
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.
The JSON is produced by sync_resources after unpacking the archive:
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:
{
"version": "02.06.00.50",
"description": "…",
"force": false
}UpgradeNetworkJob(withname="plugins"andpackage_name="networking_plugins.zip",src/slic3r/GUI/Jobs/UpgradeNetworkJob.cpp:19-20) calls:GUI_App::download_plugin("plugins", "networking_plugins.zip", ...)— drops the ZIP intotemp_directory_path()/networking_plugins.zip(a parallel branch inWebDownPluginDlg/GuideFrameuses the namenetwork_plugin.zip).GUI_App::install_plugin("plugins", "networking_plugins.zip", ...)— extracts the archive into<data_dir>/plugins/while preserving its internal directory hierarchy.
- On success a flag is written:
app_config["app"]["installed_networking"] = "1"(src/slic3r/GUI/GUI_App.cpp1906–1909). restart_networking()(src/slic3r/GUI/GUI_App.cpp1914–1957) restarts the agent: it callson_init_network(try_backup=true), resetsStaticBambuLib, re-registers callbacks and kicks off discovery.
If update_network_plugin == "true", on the next launch — before network initialization — Studio copies the freshly downloaded libraries in:
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.
GUI_App::remove_old_networking_plugins wipes the whole <data_dir>/plugins/ tree:
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);
}
}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_modulecallsInitFTModule(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 throughNetworkAgent::get_bambu_source_entry()(src/slic3r/Utils/NetworkAgent.cpp:511-562); if it fails to load,m_networking_compatible = falseis set and the user sees "please update the plugin" (src/slic3r/GUI/GUI_App.cpp:3430-3437).live555— the classic RTSP library used internally byBambuSource. 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.jsonin the OTA cache (not in the installed folder);- the symbol
bambu_network_get_versionto determine the version.
The main check is that the first 8 characters of the version string match, i.e. MAJOR.MINOR.PATCH without the build suffix:
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:
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.
Before calling LoadLibrary/dlopen Studio compares the module's publisher with Studio's own publisher:
std::optional<SignerSummary> 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:
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.exeand ofbambu_networking.dllmust share either an SPKI or a certificate. If the plugin is unsigned,SummarizeModulereturnsnullopt, the "error" branch is logged,networking_modulestaysnullptr, and the module will not be loaded. - macOS: the comparison uses the
team_id(Developer ID). - Linux:
SummarizeSelf/SummarizeModulealways returnstd::nullopt— see:
#else
std::optional<SignerSummary> SummarizeSelf() { return std::nullopt; }
std::optional<SignerSummary> SummarizeModule(const std::string&) { return std::nullopt; }
#endifTherefore on Linux if (self_cert_summary) is false and Studio takes the "load as is" branch — the signature is effectively not verified on Linux.
AppConfig exposes a flag ignore_module_cert, which is forwarded to the validate_cert parameter:
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.
- A boolean
installed_networkingkey inapp_config(sectionapp) — set to"1"after a successfulinstall_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:
LoadLibrary/dlopenreturns non-null;bambu_network_check_debug_consistentreturnstruefor the appropriate build flavor;bambu_network_get_versionreturns a string at least 8 chars long with the right version prefix;BambuSourcealso loaded successfully.
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.
All symbols are resolved through GetProcAddress (Windows) / dlsym (Linux, macOS) in NetworkAgent::get_network_function:
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 structsPrintParams/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.
| 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. |
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):
set_config_dir(data_dir())init_log()set_cert_file(resources_dir()+"/cert", "slicer_base64.cer")init_http_extra_header→set_extra_http_header(...)- the full
set_on_*_fn(...)battery (see § 6.2) set_country_code(country_code)start()start_discovery(true, false)
The plugin must tolerate this exact order (in particular, no networking work should happen before start()).
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<void(std::string dev_info_json_str)> |
bambu_network_set_on_user_login_fn |
OnUserLoginFn = std::function<void(int online_login, bool login)> |
bambu_network_set_on_printer_connected_fn |
OnPrinterConnectedFn = std::function<void(std::string topic_str)> |
bambu_network_set_on_server_connected_fn |
OnServerConnectedFn = std::function<void(int return_code, int reason_code)> |
bambu_network_set_on_http_error_fn |
OnHttpErrorFn = std::function<void(unsigned http_code, std::string http_body)> |
bambu_network_set_get_country_code_fn |
GetCountryCodeFn = std::function<std::string()> |
bambu_network_set_on_subscribe_failure_fn |
GetSubscribeFailureFn = std::function<void(std::string topic)> |
bambu_network_set_on_message_fn |
OnMessageFn = std::function<void(std::string dev_id, std::string msg)> |
bambu_network_set_on_user_message_fn |
OnMessageFn |
bambu_network_set_on_local_connect_fn |
OnLocalConnectedFn = std::function<void(int status, std::string dev_id, std::string msg)> |
bambu_network_set_on_local_message_fn |
OnMessageFn |
bambu_network_set_queue_on_main_fn |
QueueOnMainFn = std::function<void(std::function<void()>)> — "run this lambda on the GUI thread" |
bambu_network_set_server_callback |
OnServerErrFn = std::function<void(std::string url, int status)> |
| 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<std::string> dev_list) |
bambu_network_del_subscribe |
int(void*, std::vector<std::string> 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 |
| 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 |
| 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): theget_my_token_ptrpointer is mistakenly resolved via the string"bambu_network_get_my_profile"instead of"bambu_network_get_my_token". Studio still tries to read thebambu_network_get_my_tokensymbol as well, so a compatible plugin must export both. Through that pointer Studio will in practice execute theget_my_profilebody — the two functions must therefore share identical signatures, and any real token-fetching logic ends up running fromget_my_profile.
| 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<std::string> query_list, unsigned int* http_code, std::string* http_body) |
The detectResult struct (src/slic3r/Utils/bambu_networking.hpp:180-189):
struct detectResult {
std::string result_msg, command, dev_id, model_id, dev_name, version, bind_state, connect_type;
};| 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:
{
"devices": [{
"dev_id": "<printer serial>",
"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.
Types:
OnUpdateStatusFn = std::function<void(int status, int code, std::string msg)>WasCancelledFn = std::function<bool()>OnWaitFn = std::function<bool(int status, std::string job_info)>
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.
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:
POST /v1/iot-service/api/user/project
returnsproject_id,model_id,profile_id, plus the first presignedupload_urlandupload_ticket.PUT <upload_url>
uploads the config 3mf.PUT /v1/iot-service/api/user/notificationand poll
GET /v1/iot-service/api/user/notification?action=upload&ticket=<ticket>.PATCH /v1/iot-service/api/user/project/<project_id>
first patch with placeholderftp://...URL (mirrors stock plugin behaviour).GET /v1/iot-service/api/user/upload?models=<model_id>_<profile_id>_<plate>.3mf
returns the second presigned URL for the main print-ready 3mf.PUT <second presigned URL>
uploads the main 3mf.PATCH /v1/iot-service/api/user/project/<project_id>
second patch with the real uploaded URL.POST /v1/user-service/my/task, then MQTTproject_filepublish.
Terminology note:
- ABI names still use
OSSin 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-storagePUTURLs (S3-style semantics in code/comments), not a plugin-side fixed OSS endpoint.
The print-job submission ends with a single MQTT command published to the printer's request channel (device/<dev_id>/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.
{
"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_<N>.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://<path> 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<int>), 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):
{
"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=<dev_id>.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.)
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.
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 (<project_name>, <project_name>.gcode.3mf, ftp://<project_name>.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).
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:
{
"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.
| Symbol | Signature |
|---|---|
bambu_network_get_user_presets |
int(void*, std::map<std::string, std::map<std::string, std::string>>* user_presets) |
bambu_network_request_setting_id |
std::string(void*, std::string name, std::map<std::string, std::string>* values_map, unsigned int* http_code) |
bambu_network_put_setting |
int(void*, std::string setting_id, std::string name, std::map<std::string, std::string>* 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<bool(std::map<std::string,std::string>)>, ProgressFn = std::function<void(int)>.
All six entry points are thin wrappers over one REST resource —
<method> /v1/iot-service/api/slicer/setting[/<setting_id>]?version=<bundle>&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… |
GET /slicer/setting?public=false&version=<bundle> — list metadata (called from get_setting_list / get_setting_list2):
{
"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:
{
"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/<setting_id> — full preset (observed only by direct probe; the stock plugin does not call it):
{
"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:
{
"name": "<preset name>",
"type": "filament|print|printer",
"version": "<bundle version>",
"base_id": "<parent system preset id or empty>",
"filament_id": "<filament id or empty>",
"setting": { "<option>": "<serialized value>", "...": "..." }
}Response on success:
{ "message": "success", "code": null, "error": null,
"setting_id": "PFUSdce8291f0b44ab",
"update_time": "2026-04-21 17:56:43" }Missing mandatory fields return HTTP 400 with a plain-text error (e.g. field "version" is not set); type outside {print,filament,printer} returns HTTP 422 with {"detail":"Invalid input parameters"}.
PATCH /slicer/setting/<setting_id> — update (called from put_setting): same body shape as POST; same response. PATCH against a non-existent id returns HTTP 422.
DELETE /slicer/setting/<setting_id> — remove (called from delete_setting): {"message":"success","code":null,"error":null}. Idempotent: DELETE of a missing id still answers 200.
PresetCollection::load_user_preset(name, values_map, ...) rejects a preset unless values_map contains, at minimum:
| Key | Source | Notes |
|---|---|---|
version |
response version |
Must be parseable by Semver::parse; preset is skipped if cloud major > Studio major. |
setting_id |
response setting_id |
Used as the stable identifier Studio writes back into the preset file. |
updated_time |
response update_time |
Unix seconds as a decimal string, not the ISO string the server returns. |
user_id |
authenticated session | Server does not include it; caller must inject. |
base_id |
response base_id |
Empty string when the preset is a custom root. |
type |
response type |
print / filament / printer; top-level collection key in list response. |
filament_id |
response filament_id |
Only mandatory when type == "filament" and base_id is empty. |
inherits |
inside setting |
Pass-pass from cloud; parent lookup during load. |
| (all other preset options) | inside setting |
Merged into DynamicPrintConfig via load_string_map. |
On a fresh machine Studio's local preset cache is empty, so the stock plugin's metadata-only list walk produces no visible presets — the loader has a setting_id but no setting map to merge in. Cross-device sync therefore only works if the plugin also issues GET /slicer/setting/<id> per entry and builds the full values_map itself.
GUI_App::start_sync_user_preset() drives the whole thing on a worker thread (src/slic3r/GUI/GUI_App.cpp):
- One-shot catalogue walk:
m_agent->get_setting_list2(bundle_version, check_fn, progress_fn, cancel_fn)— enumerates all user presets. For each catalogue entry the plugin invokescheck_fnwith{type, name, setting_id, updated_time}; the closure returnstruewhen the localPresetCollection::need_sync()says this row is newer than the on-disk copy. Progress 0-100 drives a modalProgressDialog.- On success Studio calls
reload_settings(), which callsm_agent->get_user_presets(&map)and feeds the map intopreset_bundle->load_user_presets(app_config, map, ...).
- Continuous background loop, 100 ms tick, every 20 ticks:
- For each of
print/filament/printercollections,PresetCollection::get_user_presets(&result_presets)produces the dirty local presets. - Each dirty preset is handed to
sync_preset(preset), which callsget_differed_values_to_updateto produce avalues_map, then:preset->sync_info == "create"or empty →request_setting_id(name, &values_map, &http_code)(POST).preset->sync_info == "update"→put_setting(setting_id, name, &values_map, &http_code)(PATCH).
delete_cache_presetslist (presets removed locally) →delete_setting(id)one-by-one.
- For each of
The sync loop checks values_map["code"] == "14" to detect the server's "preset quota exceeded" response and shows a BBLUserPresetExceedLimit notification without retrying further creates for that preset type.
| Symbol | Signature |
|---|---|
bambu_network_get_studio_info_url |
std::string(void*) |
bambu_network_set_extra_http_header |
int(void*, std::map<std::string, std::string>) |
bambu_network_get_my_message |
int(void*, int type, int after, int limit, unsigned int* http_code, std::string* http_body) |
bambu_network_check_user_task_report |
int(void*, int* task_id, bool* printable) |
bambu_network_get_user_print_info |
int(void*, unsigned int* http_code, std::string* http_body) |
bambu_network_get_user_tasks |
int(void*, TaskQueryParams, std::string* http_body) |
bambu_network_get_task_plate_index |
int(void*, std::string task_id, int* plate_index) |
bambu_network_get_subtask_info |
int(void*, std::string subtask_id, std::string* task_json, unsigned int* http_code, std::string* http_body) |
bambu_network_get_slice_info |
int(void*, std::string project_id, std::string profile_id, int plate_index, std::string* slice_json) |
bambu_network_report_consent |
int(void*, std::string expand) |
TaskQueryParams (bambu_networking.hpp:243-249): dev_id, status, offset, limit.
Every REST call the plugin makes — authentication, bind, print-job orchestration, preset sync, device firmware, MakerWorld — lands on the same regional API host, chosen by the user's country_code in app_config (the same switch GUI_App::get_http_url uses for the plugin manifest, see §2.1):
| Region | API host | Web host |
|---|---|---|
US / default |
https://api.bambulab.com |
https://bambulab.com |
CN |
https://api.bambulab.cn |
https://bambulab.cn |
All authenticated endpoints require exactly one mandatory header:
Authorization: Bearer <access_token>
MITM dumps of the stock plugin show it also sending the full Studio fingerprint on every request — User-Agent: bambu_network_agent/<ver>, plus X-BBL-Client-ID, X-BBL-Client-Name, X-BBL-Client-Type, X-BBL-Client-Version, X-BBL-Device-ID, X-BBL-Language, X-BBL-OS-Type, X-BBL-OS-Version, X-BBL-Agent-Version, X-BBL-Executable-info, X-BBL-Agent-OS-Type, and anything Studio injects through bambu_network_set_extra_http_header. Direct probes against the production server confirm that none of the X-BBL-* headers, nor even the custom User-Agent, are required for the API to accept the call. They influence analytics only.
Most JSON responses share a common envelope:
{
"message": "success" | "<human message>",
"code": null | <integer error>,
"error": null | "<string>",
"...endpoint-specific fields..."
}code is the "business" error code the GUI inspects (for example 14 for preset quota exceeded, 2 for missing resources). Transport-level failures surface as non-2xx HTTP codes — typically 400 for malformed bodies, 401 for a missing/expired bearer, 422 for invalid-input (e.g. PATCH against an unknown ID), 5xx for server-side failures.
For endpoints that return a plain-text error (notably POST /slicer/setting with a missing mandatory field) the body is a bare string — the envelope is absent.
All paths below are relative to the regional API host from §6.10.1. The "evidence" column states how firm the mapping is — either MITM (seen in a live dump of the stock plugin), probe (issued by hand with curl against production), source (read out of Studio's own code) or stub (the plugin never hits the network and Studio is happy with a canned response).
get_studio_info_url— string accessor, no HTTP call. The stock plugin returns a URL for the "news / banner" side panel (usually a MakerWorld page); an empty string disables the panel. Evidence: source.set_extra_http_header— pure state update. Studio calls it during startup and on region/language switches to attach fingerprint headers to every subsequent request. The stock plugin stores the map and folds it into outgoing header sets; the server ignores the contents. Evidence: source.get_my_message— the Message Centre bell polls this for(type, after, limit). Studio parseshttp_bodyas JSON and expects an envelope with amessages[]array. The exact URL was not captured in available MITM dumps (the stock plugin only emits it when there is something in the cloud inbox for the user); the most likely candidate from community traces isGET /v1/user-service/my/messages?type=<t>&after=<unix>&limit=<n>. Returning an empty body withhttp_code = 0makes Studio's parser treat the response as "no messages" and the bell stays clear. Evidence: source; URL unconfirmed.check_user_task_report— polled after every print to decide whether to show the "rate this print" prompt. The output contract is*task_id(zero means "nothing to report") and*printable. Stock endpoint was not captured; returning0 / falseis the documented way to suppress the popup. Evidence: source; URL unconfirmed.get_user_print_info—GET /v1/iot-service/api/user/bind. This is the single source for the cloud side of the Devices tab. Response shape (from MITM plus direct probes):{"devices":[{ "dev_id", "name", "online", "print_status", "dev_model_name", "dev_product_name", "dev_access_code", ... }]}. Studio'sDeviceManager::parse_user_print_inforeads slightly different field names —dev_name,dev_online,task_status— so a clean implementation has to remap on the way out. Evidence: MITM + probe.get_user_tasks— the Cloud Task / History grid. Studio passes the wholehttp_bodythrough to its JSON parser. The stock endpoint is not captured. Evidence: source; URL unconfirmed.get_task_plate_index— looks up which plate a given cloudtask_idran on. Studio falls back to plate0on failure. Evidence: source; URL unconfirmed.get_subtask_info— MakerWorld subtask detail fetch; Studio pulls the printer-card hero image fromcontext.plates[<plate_idx>].thumbnail.urlin the response.contentis a JSON string holding an inner{info:{plate_idx}}envelope — both shapes are inDeviceManager.cpp. The stock cloud URL is unconfirmed. Evidence: source; cloud URL unconfirmed.get_slice_info— slice summary (time / weight / material cost / layer thumbnails) for a cloud task. Evidence: source; URL unconfirmed.report_consent— one-shot "I accepted the privacy / telemetry dialog" notification, body{"expand":"<flag>"}. Studio ignores the return value. Evidence: source; URL unconfirmed.
The plugin's other HTTP-heavy surfaces follow the same transport and envelope rules but live in their own sections because of their size. The endpoints below are all verified against real traffic unless marked:
| Concern | Endpoint(s) | Section | Evidence |
|---|---|---|---|
| Bearer-token login / refresh / profile | POST /v1/user-service/user/ticket/<T>, POST /v1/user-service/user/refreshtoken, GET /v1/user-service/my/profile |
§6.5 | MITM + probe |
| Device bind / unbind / rename | POST /v1/iot-service/api/user/bind, GET /v1/iot-service/api/user/bind, PATCH /v1/iot-service/api/user/device/info, DELETE /v1/iot-service/api/user/bind?dev_id=<id> |
§6.6 | MITM + probe |
| Printer firmware catalogue | stock: unknown cloud catalogue call; ours: synthesised from MQTT state | §6.7 | source only (stock) |
| Cloud print-job pipeline | POST /v1/iot-service/api/user/project, PUT <presigned>, PUT /v1/iot-service/api/user/notification, GET /v1/iot-service/api/user/notification?action=upload&ticket=<t>, PATCH /v1/iot-service/api/user/project/<pid>, GET /v1/iot-service/api/user/upload?models=<mid>_<plate>.3mf, POST /v1/user-service/my/task |
§6.8 | MITM |
| User presets sync | <m> /v1/iot-service/api/slicer/setting[/<id>]?public=false&version=<bundle> |
§6.9 | MITM + probe |
| Filament Manager (spool catalogue) | <m> /v1/design-user-service/my/filament/v2[/batch], GET /v1/design-user-service/filament/config |
§6.15 | MITM |
| MakerWorld / Mall, OSS upload | various design-service / iot-service / OSS paths |
§6.12 | not captured |
| Camera / live view / HMS snapshot | not captured | §6.11 | — |
| Analytics / telemetry | not captured | §6.13 | — |
| Symbol | Signature |
|---|---|
bambu_network_get_camera_url |
int(void*, std::string dev_id, std::function<void(std::string)>) |
bambu_network_get_camera_url_for_golive |
int(void*, std::string dev_id, std::string sdev_id, std::function<void(std::string)>) |
bambu_network_get_hms_snapshot |
int(void*, std::string& dev_id, std::string& file_name, std::function<void(std::string, int)>) |
| Symbol | Signature |
|---|---|
bambu_network_get_design_staffpick |
int(void*, int offset, int limit, std::function<void(std::string)>) |
bambu_network_start_publish |
int(void*, PublishParams, OnUpdateStatusFn, WasCancelledFn, std::string* out) |
bambu_network_get_model_publish_url |
int(void*, std::string* url) |
bambu_network_get_subtask |
int(void*, BBLModelTask* task, OnGetSubTaskFn) |
bambu_network_get_model_mall_home_url |
int(void*, std::string* url) |
bambu_network_get_model_mall_detail_url |
int(void*, std::string* url, std::string id) |
bambu_network_put_model_mall_rating |
int(void*, int rating_id, int score, std::string content, std::vector<std::string> images, unsigned int& http_code, std::string& http_error) |
bambu_network_get_oss_config |
int(void*, std::string& config, std::string country_code, unsigned int& http_code, std::string& http_error) |
bambu_network_put_rating_picture_oss |
int(void*, std::string& config, std::string& pic_oss_path, std::string model_id, int profile_id, unsigned int& http_code, std::string& http_error) |
bambu_network_get_model_mall_rating |
int(void*, int job_id, std::string& rating_result, unsigned int& http_code, std::string& http_error) |
bambu_network_get_mw_user_preference |
int(void*, std::function<void(std::string)>) |
bambu_network_get_mw_user_4ulist |
int(void*, int seed, int limit, std::function<void(std::string)>) |
PublishParams (bambu_networking.hpp:251-258): project_name, project_3mf_file, preset_name, project_model_id, design_id, config_filename.
| Symbol | Signature |
|---|---|
bambu_network_track_enable |
int(void*, bool enable) |
bambu_network_track_remove_files |
int(void*) |
bambu_network_track_event |
int(void*, std::string evt_key, std::string content) |
bambu_network_track_header |
int(void*, std::string header) |
bambu_network_track_update_property |
int(void*, std::string name, std::string value, std::string type) |
bambu_network_track_get_property |
int(void*, std::string name, std::string& value, std::string type) |
These are used only for analytics — a plugin that simply returns 0 from all of them is functionally indistinguishable for Studio's own code paths.
This subsystem is initialized right after bambu_networking loads, via InitFTModule(networking_module), and resolves its symbols from the same module (src/slic3r/Utils/FileTransferUtils.hpp, FileTransferUtils.cpp):
using fn_ft_abi_version = int(FT_CALL *)();
using fn_ft_free = void(FT_CALL *)(void *);
using fn_ft_job_result_destroy = void(FT_CALL *)(ft_job_result *);
using fn_ft_job_msg_destroy = void(FT_CALL *)(ft_job_msg *);
using fn_ft_tunnel_create = ft_err(FT_CALL *)(const char *url, FT_TunnelHandle **out);
using fn_ft_tunnel_retain = void(FT_CALL *)(FT_TunnelHandle *);
using fn_ft_tunnel_release = void(FT_CALL *)(FT_TunnelHandle *);
using fn_ft_tunnel_start_connect = ft_err(FT_CALL *)(FT_TunnelHandle *, void(FT_CALL *)(void *user, int ok, int err, const char *msg), void *user);
using fn_ft_tunnel_sync_connect = ft_err(FT_CALL *)(FT_TunnelHandle *);
using fn_ft_tunnel_set_status_cb = ft_err(FT_CALL *)(FT_TunnelHandle *, void(FT_CALL *)(void *user, int old_status, int new_status, int err, const char *msg), void *user);
using fn_ft_tunnel_shutdown = ft_err(FT_CALL *)(FT_TunnelHandle *);
using fn_ft_job_create = ft_err(FT_CALL *)(const char *params_json, FT_JobHandle **out);
using fn_ft_job_retain = void(FT_CALL *)(FT_JobHandle *);
using fn_ft_job_release = void(FT_CALL *)(FT_JobHandle *);
using fn_ft_job_set_result_cb = ft_err(FT_CALL *)(FT_JobHandle *, void(FT_CALL *)(void *user, ft_job_result result), void *user);
using fn_ft_job_get_result = ft_err(FT_CALL *)(FT_JobHandle *, uint32_t timeout_ms, ft_job_result *out_result);
using fn_ft_tunnel_start_job = ft_err(FT_CALL *)(FT_TunnelHandle *, FT_JobHandle *);
using fn_ft_job_cancel = ft_err(FT_CALL *)(FT_JobHandle *);
using fn_ft_job_set_msg_cb = ft_err(FT_CALL *)(FT_JobHandle *, void(FT_CALL *)(void *user, ft_job_msg msg), void *user);
using fn_ft_job_try_get_msg = ft_err(FT_CALL *)(FT_JobHandle *, ft_job_msg *out_msg);
using fn_ft_job_get_msg = ft_err(FT_CALL *)(FT_JobHandle *, uint32_t timeout_ms, ft_job_msg *out_msg);Unlike bambu_network_*, this is a pure C ABI. Calling convention: __cdecl on Windows.
ft_err:
typedef enum { FT_OK = 0, FT_EINVAL = -1, FT_ESTATE = -2, FT_EIO = -3,
FT_ETIMEOUT = -4, FT_ECANCELLED = -5, FT_EXCEPTION = -6,
FT_EUNKNOWN = -128 } ft_err;Result / message structs:
struct ft_job_result { int ec; int resp_ec; const char *json; const void *bin; uint32_t bin_size; };
struct ft_job_msg { int kind; const char *json; };Studio expects ft_abi_version() == 1 (the default abi_required in InitFTModule).
Semantically, this ABI describes a "tunnel + job" bus: open a connection to the printer (ft_tunnel_create from a url), start jobs on it, listen for results and messages.
Bambu Studio 02.06.01 introduced the Filament Manager tab — a WebView-driven dashboard that tracks every spool the user owns (RFID, vendor, type, current weight, color, AMS slot binding, …). The list lives in the cloud; the network plugin exposes five entry points that Studio's wgtFilaManagerCloudClient (src/slic3r/GUI/fila_manager/wgtFilaManagerCloudClient.cpp) drives all reads and writes through.
| Symbol | Signature |
|---|---|
bambu_network_get_filament_spools |
int(void*, FilamentQueryParams, std::string* http_body) |
bambu_network_create_filament_spool |
int(void*, std::string request_body, std::string* http_body) |
bambu_network_update_filament_spool |
int(void*, std::string spool_id, std::string request_body, std::string* http_body) |
bambu_network_delete_filament_spools |
int(void*, FilamentDeleteParams, std::string* http_body) |
bambu_network_get_filament_config |
int(void*, std::string* http_body) |
FilamentQueryParams and FilamentDeleteParams are defined in bambu_networking.hpp:260-275:
struct FilamentQueryParams {
std::string category; // e.g. "PLA", "PETG"
std::string status; // "0" = active, "1" = info_needed
std::string spool_id; // single id (or comma-list) — sent as ?ids=
std::string rfid; // single RFID (or comma-list) — sent as ?RFIDs=
int offset = 0;
int limit = 20;
};
struct FilamentDeleteParams {
std::vector<std::string> ids;
std::vector<std::string> rfids;
};All paths are relative to the regional API host from §6.10.1, under the design-user-service subtree:
| ABI call | HTTP | Path | Body | Evidence |
|---|---|---|---|---|
get_filament_config |
GET |
/v1/design-user-service/filament/config |
— | MITM |
get_filament_spools |
GET |
/v1/design-user-service/my/filament/v2?offset=…&limit=…[&category=…&status=…&ids=…&RFIDs=…] |
— | MITM |
create_filament_spool |
POST |
/v1/design-user-service/my/filament/v2 |
CreateFilamentV2Req |
MITM |
update_filament_spool |
PUT |
/v1/design-user-service/my/filament/v2 (id is in body, not path) |
UpdateFilamentV2Req |
MITM |
delete_filament_spools |
DELETE |
/v1/design-user-service/my/filament/v2/batch |
BatchDeleteFilamentV2Req |
MITM |
Auth and transport are the §6.10.1 defaults — Authorization: Bearer <access_token>, Content-Type: application/json. Stock bambu_network_agent/02.06.01.50 overrides User-Agent for this surface (the only place it does so in the entire plugin), but the server accepts the generic BBL-Slicer/v… UA too — direct probes confirm there's no UA gating.
Field names follow the cloud-side swagger (design-user.api, schemas CreateFilamentV2Req / UpdateFilamentV2Req / ListFilamentV2Resp / BatchDeleteFilamentV2Req); they are camelCase (filamentVendor, netWeight, totalNetWeight, createdAt, …) — distinct from Studio's local snake_case spool schema. Studio's wgtFilaManagerCloudSync (src/slic3r/GUI/fila_manager/wgtFilaManagerCloudSync.cpp) translates between the two with cloud_json_to_spool / spool_to_cloud_json.
GET /my/filament/v2 — list user's spools. The plugin forwards the response body verbatim; Studio parses it. Empty list:
{"hits":[]}Populated list (one entry shown):
{
"hits": [{
"id": 4986700,
"createType": "ams" | "manual",
"filamentVendor": "Bambu Lab",
"filamentType": "PETG",
"filamentName": "PETG Basic",
"filamentId": "GFG00",
"RFID": "0000000000000000",
"color": "#898989",
"colorType": 2,
"colors": null,
"netWeight": 975,
"totalNetWeight": 1000,
"note": "",
"createdAt": 1777418842,
"updatedAt": 1777418842,
"status": 0,
"isSupport": false,
"trayIdName": "0",
"category": "PETG"
}]
}id is int64; createdAt / updatedAt are unix seconds; status is 0 (active) or 1 (info-needed). colorType is 0 (gradient), 1 (mixed) or 2 (solid). trayIdName is the AMS tray label when the spool was synced from a printer; empty for manual entries.
POST /my/filament/v2 — create a spool. Request body Studio assembles for an AMS-sourced spool (manual entries use "createType":"manual" and omit RFID / trayIdName / rolls):
{
"RFID": "0000000000000000",
"color": "#898989",
"colorType": 2,
"createType": "ams",
"filamentId": "GFG00",
"filamentName": "PETG Basic",
"filamentType": "PETG",
"filamentVendor": "Bambu Lab",
"isSupport": false,
"netWeight": 975,
"rolls": 1,
"totalNetWeight": 1000,
"trayIdName": "0"
}Response on success is just {} (200 OK) — the server does not echo the new id. Studio re-issues GET /my/filament/v2 afterwards to learn the assigned id and createdAt. Plugins that synthesise a body must keep this contract intact: returning a non-{} payload won't break Studio (it still re-lists), but it will look anomalous in the log.
PUT /my/filament/v2 — partial update. The body must always include id (int64) and filamentName (cloud requires it on every edit, even when nothing else changes — see Studio's spool_to_cloud_update_json); other fields are optional and only sent when modified. Request seen in MITM:
{"filamentName": "PETG Basic", "id": 4986771, "netWeight": 500, "note": "test note"}Response wraps the updated spool under filamentV2:
{"filamentV2": {"id": 4986771, "createType": "ams", "...": "...", "netWeight": 500, "note": "test note"}}A 404 response means "id not found"; Studio falls back to POST (create) on that path, see wgtFilaManagerCloudSync::push_update_to_cloud.
DELETE /my/filament/v2/batch — batch remove. Body is {"ids":[…]} and/or {"RFIDs":[…]}; ids are JSON strings on the wire even though the schema is int64 (the server accepts both forms). Response is {} (200 OK).
{"ids": ["4986771"]}GET /filament/config — catalogue / dropdowns. Returns the canonical filament list Studio uses to populate "Add spool" form pickers:
{
"categories": ["PLA","PETG","TPU","ABS","ASA","PA","PC","PET","PPS","Support"],
"filamentSettings": [
{"filamentVendor":"Bambu Lab", "filamentType":"PLA", "filamentName":"PLA Basic", "filamentId":"GFA00", "isSupport":false},
{"filamentVendor":"Bambu Lab", "filamentType":"PLA", "filamentName":"PLA Matte", "filamentId":"GFA01", "isSupport":false},
"...": "... (~110 entries, ~11 KB)"
]
}Studio caches the response for the lifetime of the WebView and keys vendor/type/name pickers off the same filamentId quadruples that show up under each spool.
wgtFilaManagerCloudDispatcher serialises every cloud operation onto a single in-flight queue (enqueue_pull / enqueue_push_create / enqueue_push_update / enqueue_push_delete) so the server never sees concurrent writes from the same client. The triggers are:
- Login.
GUI_App::on_user_logincallsm_fila_manager_cloud_disp->enqueue_pull()once an access token is available — this is the very first call most plugins ever see, before the user even opens the Filament Manager tab. - Filament Manager panel mount.
FilaManagerVM::OnPanelShownre-issues a pull and fetchesget_filament_config. Repeated tab focuses are debounced through the dispatcher. - User actions. "Add spool" →
create_filament_spool; field edits →update_filament_spool(with the fallback-to-create on 404); single or multi-select delete →delete_filament_spools. - AMS sync. When the printer reports a new RFID-tagged spool, Studio synthesises a
createType:"ams"POST with the matchingRFIDandtrayIdName.
Every successful pull rewrites the local store: cloud is the source of truth, and any local-only entries that didn't make it to the server (e.g. a failed previous push) are dropped on each refresh.
The complete list of error values the plugin is expected to return through int lives in src/slic3r/Utils/bambu_networking.hpp:13-94 (general, bind, start_local_print_with_record, start_print, start_local_print, start_send_gcode_to_sdcard, connection). Five additional BAMBU_NETWORK_ERR_{GET_FILAMENTS,CREATE_FILAMENT,UPDATE_FILAMENT,DELETE_FILAMENT,GET_FILAMENT_CONFIG}_FAILED codes (-27..-31) are returned on transport / HTTP-error paths from the §6.15 endpoints so Studio's UI can surface a meaningful toast instead of a silent retry-loop.
This second module is the one Studio talks to whenever the user opens a printer's camera live view or the on-printer file browser (under "Device" → "SD Card / USB"). It has nothing in common with bambu_networking apart from packaging — different symbol prefix (Bambu_*), different loader, different per-platform back-ends. Bambu's stock shipment puts it at the same <data_dir>/plugins/ path as the main networking plugin, but a missing or stub libBambuSource does not stop Studio from starting; only camera/file-browser features get disabled.
This entire section is reverse-engineered from Studio's own source tree; references below all point at the OrcaSlicer subtree (3rd_party/OrcaSlicer/src/...), which is used as the ground-truth checkout.
Studio resolves libBambuSource lazily, on the first time a camera or file-browser tab is shown:
#if defined(_MSC_VER) || defined(_WIN32)
HMODULE BBLNetworkPlugin::get_source_module()
#else
void* BBLNetworkPlugin::get_source_module()
#endif
{
if ((m_source_module) || (!m_networking_module))
return m_source_module;
std::string library;
std::string data_dir_str = data_dir();
boost::filesystem::path data_dir_path(data_dir_str);
auto plugin_folder = data_dir_path / "plugins";
#if defined(_MSC_VER) || defined(_WIN32)
library = plugin_folder.string() + "/" + std::string(BAMBU_SOURCE_LIBRARY) + ".dll";
...
m_source_module = LoadLibrary(lib_wstr);
...
#else
#if defined(__WXMAC__)
library = plugin_folder.string() + "/" + std::string("lib") + std::string(BAMBU_SOURCE_LIBRARY) + ".dylib";
#else
library = plugin_folder.string() + "/" + std::string("lib") + std::string(BAMBU_SOURCE_LIBRARY) + ".so";
#endif
m_source_module = dlopen(library.c_str(), RTLD_LAZY);
#endif
return m_source_module;
}So the resolved file names are:
| Platform | Path |
|---|---|
| Windows | <data_dir>\plugins\BambuSource.dll |
| macOS | <data_dir>/plugins/libBambuSource.dylib |
| Linux | <data_dir>/plugins/libBambuSource.so |
Notable side effects:
- The function early-returns
nullptrwhenm_networking_module == nullptr, solibBambuSourceis never loaded standalone —bambu_networkingmust be loaded first. - There is no signature check on this module, no version-prefix gate, no fall-back to
<data_dir>/plugins/backup/. Studio either gets a non-null module, fishes out C symbols viadlsym/GetProcAddress(§7.2), and never touches it again, or it falls back to aFake_Bambu_Createstub (§7.2) and the whole feature surface is disabled. - The single public accessor is
Slic3r::NetworkAgent::get_bambu_source_entry()(src/slic3r/Utils/NetworkAgent.cpp:67-75); it is the entry point the camera UI and the file browser both call when they want to talk to this library.
The header lives at 3rd_party/OrcaSlicer/src/slic3r/GUI/Printer/BambuTunnel.h and ships both as a static-link header (#define BAMBU_DYNAMIC off) and as a dlopen function-pointer table (BAMBU_DYNAMIC on, typedef struct __BambuLib { ... } BambuLib). Studio uses the dlopen path: PrinterFileSystem.cpp defines
class PrinterFileSystem : ..., BambuLib { ... };i.e. it inherits the function-pointer table directly into the file-browser object. The pointers are wired up by StaticBambuLib:
StaticBambuLib &StaticBambuLib::get(BambuLib *copy)
{
static StaticBambuLib lib;
if (lib.Bambu_Create)
return lib;
if (!module) {
module = Slic3r::NetworkAgent::get_bambu_source_entry();
}
GET_FUNC(Bambu_Create);
GET_FUNC(Bambu_Open);
GET_FUNC(Bambu_StartStream);
GET_FUNC(Bambu_StartStreamEx);
GET_FUNC(Bambu_GetStreamCount);
GET_FUNC(Bambu_GetStreamInfo);
GET_FUNC(Bambu_SendMessage);
GET_FUNC(Bambu_ReadSample);
GET_FUNC(Bambu_Close);
GET_FUNC(Bambu_Destroy);
GET_FUNC(Bambu_SetLogger);
GET_FUNC(Bambu_FreeLogMsg);
GET_FUNC(Bambu_Deinit);
if (!lib.Bambu_Create) {
lib.Bambu_Create = Fake_Bambu_Create;
...
}
return lib;
}Fake_Bambu_Create (PrinterFileSystem.cpp:71) returns -2, which propagates as m_last_error and surfaces in the UI as "library missing".
The full set of symbols Studio looks up — declared in BambuTunnel.h, sorted by consumer — is:
| Symbol | Signature | Used by |
|---|---|---|
Bambu_Init |
int (void) |
one-shot global init (rarely called — observed shipping libs do nothing here) |
Bambu_Deinit |
void (void) |
one-shot global teardown; called once on agent reset (StaticBambuLib::release()) |
Bambu_Create |
int (Bambu_Tunnel*, const char* url) |
every tunnel |
Bambu_Destroy |
void (Bambu_Tunnel) |
every tunnel |
Bambu_SetLogger |
void (Bambu_Tunnel, Logger, void* ctx) |
every tunnel |
Bambu_Open |
int (Bambu_Tunnel) |
every tunnel; returns Bambu_would_block until ready |
Bambu_Close |
void (Bambu_Tunnel) |
every tunnel |
Bambu_StartStream |
int (Bambu_Tunnel, bool video) |
camera (legacy entry point) |
Bambu_StartStreamEx |
int (Bambu_Tunnel, int type) |
camera + file-browser; type = CTRL_TYPE = 0x3001 switches the tunnel into JSON-RPC mode (§7.5) |
Bambu_GetStreamCount / Bambu_GetStreamInfo |
int (...) |
camera; describe the video / audio tracks once StartStream has succeeded |
Bambu_GetDuration / Bambu_Seek |
unsigned long (...) / int (...) |
declared but not exercised on a live LAN stream |
Bambu_ReadSample |
int (Bambu_Tunnel, Bambu_Sample*) |
camera (one MJPG / H.264 access unit per call) and file-browser (one JSON response per call) |
Bambu_SendMessage |
int (Bambu_Tunnel, int ctrl, const char* data, int len) |
file-browser only — sends a CTRL JSON request (§7.5) |
Bambu_RecvMessage |
int (Bambu_Tunnel, int* ctrl, char* data, int* len) |
declared but not actually called by Studio for either feature |
Bambu_GetLastErrorMsg |
const char* (void) |
error-reporting fallback |
Bambu_FreeLogMsg |
void (const tchar* msg) |
log-callback companion |
Bambu_Tunnel is an opaque pointer; Bambu_Sample, Bambu_StreamInfo and the Bambu_Error enum are defined in BambuTunnel.h:36-110. The relevant error values are:
typedef enum {
Bambu_success = 0,
Bambu_stream_end = 1,
Bambu_would_block = 2,
Bambu_buffer_limit = 3,
} Bambu_Error;Negative return values are treated as fatal (the caller calls Bambu_Close + Bambu_Destroy and surfaces the code).
ABI footgun (same as
bambu_networking): even though every entry point isextern "C", several signatures handBambu_Sample/Bambu_StreamInfostructs by value or by pointer. The plugin must therefore be built with the same C compiler ABI Studio was built with. There is nostd::*at the boundary here, so cross-toolchain mixing is somewhat safer than forbambu_networking, buttchar(wchar_ton Windows,charelsewhere) and the calling convention still need to match.
Studio's two consumers each build their own URL.
Built in MediaPlayCtrl::Play (src/slic3r/GUI/MediaPlayCtrl.cpp:307-318) and MediaPlayCtrl::ToggleStream (...:551-559):
std::string url;
if (m_lan_proto == MachineObject::LVL_Local)
url = "bambu:///local/" + m_lan_ip + ".?port=6000&user=" + m_lan_user + "&passwd=" + m_lan_passwd;
else if (m_lan_proto == MachineObject::LVL_Rtsps)
url = "bambu:///rtsps___" + m_lan_user + ":" + m_lan_passwd + "@" + m_lan_ip + "/streaming/live/1?proto=rtsps";
else if (m_lan_proto == MachineObject::LVL_Rtsp)
url = "bambu:///rtsp___" + m_lan_user + ":" + m_lan_passwd + "@" + m_lan_ip + "/streaming/live/1?proto=rtsp";
url += "&device=" + m_machine;
url += "&net_ver=" + agent_version;
url += "&dev_ver=" + m_dev_ver;
url += "&cli_id=" + wxGetApp().app_config->get("slicer_uuid");
url += "&cli_ver=" + std::string(SLIC3R_VERSION);So the three accepted forms are:
| Form | Used by | Wire protocol |
|---|---|---|
bambu:///local/<ip>.?port=6000&user=<u>&passwd=<p>&... |
A1 / A1 mini / P1 / P1P | TLS over TCP/6000, 80-byte auth packet, then 16-byte framed JPEG samples |
bambu:///rtsps___<u>:<p>@<ip>/streaming/live/1?proto=rtsps&... |
X1 / X1C / X1E / P1S / P2S / H-series | RTSP over TLS on port 322 |
bambu:///rtsp___<u>:<p>@<ip>/streaming/live/1?proto=rtsp&... |
development / unencrypted variant | plain RTSP |
The trailing query parameters (device, net_ver, dev_ver, cli_id, cli_ver, plus optional dump_h264=<FILE*> / dump_info=<FILE*> for internal_developer_mode) are pure metadata — printers only authenticate on user/passwd, the rest is for analytics and debugging.
Built in PrinterFileSystem::Reconnect via MediaFilePanel. The format is identical to the MJPEG-camera URL (bambu:///local/<ip>.?port=6000&user=<u>&passwd=<p>&...) — same TLS port, same auth packet — but Studio immediately follows Bambu_Open with Bambu_StartStreamEx(tunnel, 0x3001) to switch the tunnel into CTRL / JSON-RPC mode (§7.5). On printers that lack StartStreamEx (older firmwares), Studio falls back to Bambu_StartStream(tunnel, false) (PrinterFileSystem.cpp:1739).
This used to be the section where the three platforms diverged sharply, with Windows routing the camera through a DirectShow source filter inside BambuSource.dll. That changed upstream in BambuStudio commit 94d91be60 ("NEW: reimpl wxMediaCtrl from ffmpeg", June 2024), which introduced src/slic3r/GUI/wxMediaCtrl3.{cpp,h}. Studio's MediaPlayCtrl was migrated to wxMediaCtrl3 as part of the same series, and from that point on Windows Studio uses the Bambu_* C ABI directly, exactly like Linux: wxMediaCtrl3::PlayThread calls Bambu_Create → Bambu_Open → Bambu_StartStream → Bambu_ReadSample, then decodes each access unit with FFmpeg via AVVideoDecoder and blits the resulting wxBitmap into a plain wxWindow.
wxMediaCtrl2.{cpp,h} is still in the BambuStudio tree and still has the Windows DirectShow stub from before the migration. It is no longer instantiated by Studio. The OrcaSlicer fork has not picked up the FFmpeg-based widget — it still uses wxMediaCtrl2 and therefore still requires the DirectShow filter on Windows.
So the matrix is now:
| Slicer / version | Linux | Windows | macOS |
|---|---|---|---|
BambuStudio post 94d91be60 (≈ v01.10+ / current v02.06.x) |
wxMediaCtrl3 → Bambu_* C ABI → AVVideoDecoder (FFmpeg) → wxWindow |
same: wxMediaCtrl3 → Bambu_* C ABI → AVVideoDecoder → wxWindow |
wxMediaCtrl2.mm → Objective-C BambuPlayer class via dlsym |
BambuStudio pre-94d91be60 and OrcaSlicer (current) |
wxMediaCtrl2 → gstbambusrc (statically linked into Studio) → bambulib_get() → Bambu_* C ABI |
wxMediaCtrl2 → wxMediaCtrl::Load(wxURI("bambu:...")) → wxWidgets DirectShow backend → COM source filter (CLSID {233E64FB-…}) inside BambuSource.dll |
wxMediaCtrl2.mm → Objective-C BambuPlayer |
Practical consequences for libBambuSource implementers:
- A portable C-ABI implementation of
libBambuSourcecovers all three platforms when the user runs current BambuStudio. The Windows DirectShow filter is no longer on the hot path. - For OrcaSlicer (and any pre-
94d91be60Studio install) the Windows DirectShow filter is still required: withoutCLSID {233E64FB-…}registered against thebambu:URL scheme, Orca cannot play the camera at all and falls into the "BambuSource has not correctly been registered" dialog. - macOS still additionally requires an Objective-C
BambuPlayerclass inside the dylib regardless of Studio version; that path was not touched by the FFmpeg migration.
The subsections below describe each back-end. §7.4.1 (Linux gstbambusrc) and §7.4.3 (macOS BambuPlayer) are unchanged. §7.4.2 (Windows DirectShow) is now an Orca-only / legacy-Studio path. §7.4.4 documents the new wxMediaCtrl3 flow that current Studio actually uses on Windows.
wxMediaCtrl2::wxMediaCtrl2() (src/slic3r/GUI/wxMediaCtrl2.cpp:44-68, in the __LINUX__ branch) registers a custom GStreamer element after the underlying wxMediaCtrl has spun up its own playbin:
#ifdef __LINUX__
auto playbin = reinterpret_cast<wxGStreamerMediaBackend *>(m_imp)->m_playbin;
GstElement* video_sink = nullptr;
for (const char* sink_name : {"ximagesink", "xvimagesink"}) {
...
}
g_object_set (G_OBJECT (playbin),
"audio-sink", NULL,
"video-sink", video_sink,
NULL);
...
gstbambusrc_register();
...
#endifgstbambusrc_register lives in src/slic3r/GUI/Printer/gstbambusrc.c — it is statically linked into Studio's binary (no plugin search path involved). The element handles the bambu:// URI scheme; internally it calls the generic accessor bambulib_get(), which in turn returns the same StaticBambuLib pointer table used by the file browser:
BambuLib *bambulib_get();extern "C" BambuLib *bambulib_get() {
return &StaticBambuLib::get(); }So on Linux the camera flow is:
MediaPlayCtrl::Play→m_media_ctrl->Load(wxURI("bambu:///...")).- wxGStreamerMediaBackend builds the standard playbin with
bambusrcas the source element. bambusrccallsBAMBULIB(Bambu_Create)(..., url)etc., i.e. the C ABI fromlibBambuSource.so.- For MJPG streams the source emits JPEG access units; the playbin attaches
jpegdec ! videoconvert ! ximagesink. For RTSPS streams the source emits raw H.264 byte stream and the playbin attachesh264parse ! avdec_h264 / openh264dec ! videoconvert ! ximagesink. Either way the slicer-side pipeline does the decode.
i.e. on Linux the C ABI is sufficient. No Linux-specific code needs to live inside libBambuSource.so.
Note: this is the path OrcaSlicer and pre-94d91be60 BambuStudio take on Windows. Current upstream BambuStudio (wxMediaCtrl3, see §7.4.4) bypasses DirectShow entirely and goes straight through the Bambu_* C ABI like Linux does.
wxMediaCtrl2::Load (the Windows branch) drops wxURI("bambu:...") into wxMediaCtrl::Load, which uses wxWidgets's wxMediaBackendDirectShow. wxWidgets resolves the URL by looking up HKCR\bambu\Source Filter in the registry to find the CLSID that handles bambu: URLs, then CoCreateInstances that CLSID and asks the resulting filter to load the URL via IFileSourceFilter::Load. Studio expects a custom DirectShow source filter to be COM-registered against the URL scheme bambu::
#define CLSID_BAMBU_SOURCE L"{233E64FB-2041-4A6C-AFAB-FF9BCF83E7AA}"
...
wxRegKey key11(wxRegKey::HKCU, L"SOFTWARE\\Classes\\CLSID\\" CLSID_BAMBU_SOURCE L"\\InProcServer32");
wxRegKey key12(wxRegKey::HKCR, L"CLSID\\" CLSID_BAMBU_SOURCE L"\\InProcServer32");
wxString path = key11.Exists() ? key11.QueryDefaultValue()
: key12.Exists() ? key12.QueryDefaultValue() : wxString{};
wxRegKey key2(wxRegKey::HKCR, "bambu");
wxString clsid;
if (key2.Exists())
key2.QueryRawValue("Source Filter", clsid);
...
auto dll_path = data_dir_path / "plugins" / "BambuSource.dll";
if (path.empty() || !wxFile::Exists(path) || clsid != CLSID_BAMBU_SOURCE) {
if (boost::filesystem::exists(dll_path)) {
... regsvr32 /q /s "<dll_path>" ...
}
}Concretely:
BambuSource.dllmust exportDllRegisterServer/DllUnregisterServer, registerCLSID_BAMBU_SOURCE = {233E64FB-2041-4A6C-AFAB-FF9BCF83E7AA}withInprocServer32 = <path-to-BambuSource.dll>, and register itself as theSource Filterfor thebambu:protocol underHKCR\bambu.- The actual filter must implement
IBaseFilter+IFileSourceFilterand produce video samples on its output pin. - For Orca / legacy Studio, the C ABI from §7.2 is not used for camera output — it is exclusively the file browser path. The DirectShow filter is a separate code path inside the same DLL.
Practical consequence (Orca / legacy Studio only): a portable C-ABI implementation of libBambuSource covers the file-browser feature on Windows for free, but the camera live view requires a separate DirectShow source filter — a substantial extra engineering effort.
This project ships such a filter (stubs/dshow_filter.cpp, exports gated through stubs/BambuSource.def). It implements IBaseFilter + IFileSourceFilter + a single output IPin, accepts the downstream-provided IMemAllocator, and pushes either Annex-B H.264 (RTSPS, port 322) or framed MJPEG (port 6000) into the renderer. Self-registration writes both HKCR\CLSID\{233E64FB-…} and HKCR\bambu\Source Filter exactly as wxMediaCtrl2 expects; regsvr32 /s BambuSource.dll is the only setup step. Three Windows-specific pitfalls were encountered during the port and are worth recording, since each one cost meaningful debugging time:
setvbuf(fp, NULL, _IOLBF, 0)is undefined behaviour on the MSVC CRT. The MSVC CRT's_setvbuf_internalenforces "if buffer isNULL, the only accepted mode is_IONBF(size 0)" via_invalid_parameter→__fastfail(FAST_FAIL_INVALID_ARG)→STATUS_STACK_BUFFER_OVERRUN (0xC0000409). The same call is silently accepted on glibc/musl. Cross-platform code that wants line-buffering-equivalent behaviour on log files must either supply a real buffer or fall back to_IONBFand let eachfprintfflush.wxURInormalisesbambu:///rtsps___…tobambu://rtsps___…before callingIFileSourceFilter::Load. The triple-slash form is whatMediaPlayCtrl::load()produces (no host, path =/rtsps___user:pwd@ip/...), but wxURI's "authority is empty" canonicaliser interpretsrtsps___user:pwdas userinfo andipas host, then re-emits the URI with a single//. A parser keyed strictly offbambu:///rtsps___will reject every Orca camera URL withE_INVALIDARG. Accept any number of/characters afterbambu:(1, 2, or 3).- The source must push samples while the graph is in
Paused, not justRunning. wmp/wxMediaCtrl keeps the filter graph inState_Pauseduntil the renderer receives its first sample (which is what triggers the transition toState_Running). A worker thread that gatesIMemInputPin::Receiveonparent->state() == State_Runningdeadlocks the graph: the renderer waits for the first sample, the source waits forRunning, the user sees an infinite "playing" indicator with a black frame, and the RTSP server eventually disconnects on TCP back-pressure. Standard DirectShow pattern is to commit the allocator in thePause()transition and start streaming immediately; downstream filters apply any reference-clock pacing themselves.
On macOS Studio does not use wxMediaCtrl's GStreamer/AVFoundation back-end. Instead, wxMediaCtrl2.mm reaches directly into libBambuSource.dylib and looks up an Objective-C class by the synthetic name OBJC_CLASS_$_BambuPlayer:
void wxMediaCtrl2::create_player()
{
auto module = Slic3r::NetworkAgent::get_bambu_source_entry();
if (!module) {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "Network plugin not ready currently!";
return;
}
Class cls = (__bridge Class) dlsym(module, "OBJC_CLASS_$_BambuPlayer");
if (cls == nullptr) {
m_error = -2;
return;
}
NSView * imageView = (NSView *) GetHandle();
BambuPlayer * player = [cls alloc];
[player initWithImageView: imageView];
[player setLogger: bambu_log withContext: this];
m_player = player;
}The expected interface is documented in src/slic3r/GUI/BambuPlayer/BambuPlayer.h:14-28:
@interface BambuPlayer : NSObject
+ (void) initialize;
- (instancetype) initWithDisplayLayer: (AVSampleBufferDisplayLayer*) layer;
- (instancetype) initWithImageView: (NSView*) view;
- (int) open: (char const *) url;
- (NSSize) videoSize;
- (int) play;
- (void) stop;
- (void) close;
- (void) setLogger: (void (*)(void const * context, int level, char const * msg)) logger withContext: (void const *) context;
@endStudio drives it from wxMediaCtrl2::Load / Play / Stop (wxMediaCtrl2.mm:87-141):
Load(url)→[player close]thenm_error = [player open: url.BuildURI().ToUTF8()].Play()→[player play], marks state aswxMEDIASTATE_PLAYING, postswxEVT_MEDIA_STATECHANGED.Stop()→[player close], postswxMEDIASTATE_STOPPED.GetVideoSize()→[player videoSize].
Failure mode if the symbol is missing: m_error = -2, m_player = nullptr. Subsequent Load / Play calls log create_player failed currently! and return without ever transitioning out of MEDIASTATE_LOADING. The user sees an infinite "Loading…" spinner in the camera tab, not the "Player is malfunctioning" dialog — the latter is reserved for m_failed_code == 2, which only fires after a state transition that never happens here. (MediaPlayCtrl.cpp:29-36, 415-428.)
This is the reason a C-ABI-only libBambuSource.dylib build (no Objective-C BambuPlayer class) is enough for the Mac file browser but produces an indefinite loading state in the Mac camera tab. To make the camera tab work on macOS the dylib must additionally export an Objective-C class symbol OBJC_CLASS_$_BambuPlayer whose interface matches BambuPlayer.h above.
In commit 94d91be60 ("NEW: reimpl wxMediaCtrl from ffmpeg"), upstream BambuStudio replaced the platform-specific wxMediaCtrl2 widget with wxMediaCtrl3, a self-contained widget that:
- Inherits from
BambuLib(theStaticBambuLibshim fromPrinterFileSystem.cpp) so it can call everyBambu_*symbol directly without going through any wxWidgets media backend. - Owns a single play-thread (
PlayThread) that walks the entireBambu_*lifecycle —Bambu_Create→Bambu_SetLogger→Bambu_Open→ spin onBambu_StartStream(true)until it stops returningBambu_would_block→Bambu_GetStreamInfo→ loop onBambu_ReadSample→Bambu_Close/Bambu_Destroy— exactly the way Linux'sgstbambusrcdoes. - Decodes each access unit with FFmpeg via the new
AVVideoDecoderhelper, packages it into awxBitmap(Windows) orwxImage(Linux), pushes it through a small ring buffer (m_frame_buffer), and renders it from a wxTimer onto a plainwxWindow.
Cross-references in the BambuStudio tree:
src/slic3r/GUI/wxMediaCtrl3.{cpp,h}— the widget itself and the play-thread (wxMediaCtrl3::PlayThreadatwxMediaCtrl3.cpp:260-405).src/slic3r/GUI/AVVideoDecoder.{cpp,h}— wraps FFmpeg'savcodec_send_packet/avcodec_receive_frameand hands the raw planes back as a wxBitmap or wxImage.src/slic3r/GUI/MediaPlayCtrl.cpp:49— the constructor takeswxMediaCtrl3 *media_ctrl, replacing the oldwxMediaCtrl2 *from earlier revisions.
What this means for the libBambuSource ABI: on current Windows BambuStudio the camera now reaches the plugin through exactly the same C-ABI surface as Linux. The Windows-only logger expects wchar_t const* strings (the tchar typedef in BambuTunnel.h), so a portable plugin has to convert UTF-8 → UTF-16 for the logger callback — but otherwise the Linux build is bit-for-bit appropriate as long as the plugin is built as a .dll with a matching MSVC ABI.
Studio's wxMediaCtrl3 does not consult the registry, does not call CoCreateInstance, and does not require the DirectShow filter to be registered. A Windows install where BambuSource.dll exports the Bambu_* C ABI but is not registered as a DirectShow filter will produce a perfectly functional camera in current BambuStudio, while OrcaSlicer on the same machine will fail to play any camera until the DLL is registered (regsvr32 /s BambuSource.dll). This is why a port of libBambuSource should ship both: the C ABI for current Studio + Linux + macOS file browser, and the DirectShow filter for OrcaSlicer + legacy Studio installs.
| Slicer | Platform | Camera back-end | What libBambuSource must provide |
|---|---|---|---|
BambuStudio post-94d91be60 (current) |
Linux | wxMediaCtrl3 → Bambu_* C ABI → FFmpeg → wxWindow |
The Bambu_* C ABI (§7.2) only |
BambuStudio post-94d91be60 (current) |
Windows | wxMediaCtrl3 → Bambu_* C ABI → FFmpeg → wxWindow |
The Bambu_* C ABI (§7.2) only |
BambuStudio post-94d91be60 (current) |
macOS | wxMediaCtrl2.mm → dlsym(libBambuSource.dylib, "OBJC_CLASS_$_BambuPlayer") (wxMediaCtrl3 was not adopted on macOS — the platform still goes through the Objective-C class) |
Both the Bambu_* C ABI and the Objective-C BambuPlayer class |
OrcaSlicer (current) and BambuStudio pre-94d91be60 |
Linux | wxMediaCtrl2 → gstbambusrc (statically linked into Studio) → bambulib_get() → Bambu_* C ABI |
The Bambu_* C ABI (§7.2) only |
OrcaSlicer (current) and BambuStudio pre-94d91be60 |
Windows | wxMediaCtrl2 → wxMediaCtrl::Load → wxWidgets DirectShow backend → COM source filter (CLSID {233E64FB-…}) inside BambuSource.dll |
A DirectShow IBaseFilter/IFileSourceFilter implementation registered against bambu:. The Bambu_* C ABI is not used for video; only for the file browser |
OrcaSlicer (current) and BambuStudio pre-94d91be60 |
macOS | same as current Studio (Objective-C BambuPlayer) |
same |
In every case the file-browser path uses only the Bambu_* C ABI plus the CTRL JSON wire protocol described next.
When Studio opens a file browser, it goes through exactly the same Bambu_Create / Bambu_Open path as the camera, then "rotates" the tunnel by calling Bambu_StartStreamEx(tunnel, CTRL_TYPE = 0x3001):
static const int CTRL_TYPE = 0x3001; do{
ret = Bambu_StartStreamEx ? Bambu_StartStreamEx(tunnel, CTRL_TYPE) : Bambu_StartStream(tunnel, false);
if (ret == Bambu_would_block)
boost::this_thread::sleep(boost::posix_time::milliseconds(100));
auto now = boost::posix_time::microsec_clock::universal_time();
if (now - start_time > timeout) {
BOOST_LOG_TRIVIAL(warning) << "StartStream timeout after 5 seconds.";
break;
}
} while (ret == Bambu_would_block && !m_stopped);After this, the tunnel is no longer a media bytestream — it is a bidirectional JSON-RPC pipe. Studio:
- enqueues outgoing requests with
Bambu_SendMessage(tunnel, CTRL_TYPE, json_text, len); - polls for responses with
Bambu_ReadSample(tunnel, &sample)exactly as for video — exceptsample.buffernow holds a JSON document optionally followed by a binary payload (e.g. a thumbnail blob).
Both run on a dedicated worker thread inside PrinterFileSystem::Reconnect / RunRequests (src/slic3r/GUI/Printer/PrinterFileSystem.cpp:1567-1595).
On the wire there are two TCP sockets in play, even though Studio only ever opens one. The CTRL tunnel is just an RPC bus; the actual file data lives on a separate FTPS connection that the plugin is responsible for opening:
| Channel | Endpoint | Direction | Carries |
|---|---|---|---|
| CTRL tunnel | TLS over TCP/6000 (the same socket Studio opened with Bambu_Create / Bambu_Open) |
Studio ↔ plugin | JSON requests/responses + optional inline blobs (thumbnails) |
| FTPS data plane | implicit FTPS over TCP/990 (opened by the plugin, not by Studio) | plugin ↔ printer | LIST / RETR / STOR / DELE, i.e. the actual file metadata and bulk bytes |
In other words: Studio does not know there is an FTPS connection. From its point of view the entire file-browser feature is "send a JSON request on the camera tunnel, get a JSON response back". The plugin maps each CTRL command onto one or more FTPS operations and then synthesises a response.
This split has consequences:
- The plugin must keep the FTPS connection alive across CTRL requests (Bambu firmwares idle-close it after ~5 minutes — a reconnect-on-stale retry on the next request is the simplest fix).
- The plugin must probe the FTPS layout once per session (§7.6.1) because different printer firmwares expose storage either as
/sdcard,/usb, or as the FTPS root itself.
The serialiser is in PrinterFileSystem::SendRequest (src/slic3r/GUI/Printer/PrinterFileSystem.cpp:1431-1458):
boost::uint32_t PrinterFileSystem::SendRequest(int type, json const &req, callback_t2 const &callback,const std::string& param)
{
...
boost::uint32_t seq = m_sequence + m_callbacks.size();
json root;
root["cmdtype"] = type;
root["sequence"] = seq;
root["req"] = req;
std::ostringstream oss;
oss << root;
if (!param.empty()) {
oss << "\n\n";
oss << param;
}
auto msg = oss.str();
boost::unique_lock l(m_mutex);
m_messages.push_back(msg);
m_callbacks.push_back(callback);
...
}Concrete shape:
{"cmdtype":<int>,"sequence":<u32>,"req":{...command-specific...}}\n\n<optional binary param>
Notes:
cmdtypeis one of theLIST_INFO/SUB_FILE/FILE_DEL/FILE_DOWNLOAD/FILE_UPLOAD/REQUEST_MEDIA_ABILITY/TASK_CANCELconstants (§7.6).sequenceis a monotonically increasing per-tunnel counter; the plugin echoes it in every response so Studio can match callbacks to requests.- The optional
\n\n<param>tail carries an inline binary blob. In practice Studio uses this only for the file-upload command; on the response side the plugin uses the same\n\n<binary>convention to deliver thumbnail bytes to Studio.
The plugin returns each response as a Bambu_Sample whose buffer is the same json\n\n[blob] envelope. Studio's parser is PrinterFileSystem::HandleResponse (PrinterFileSystem.cpp:1598+):
{"sequence":<u32>,"result":<int>,...command-specific result fields...}\n\n<optional binary>
result is an integer in the error-code enum from PrinterFileSystem.h:48-72:
| Value | Meaning |
|---|---|
| 0 | SUCCESS |
| 1 | CONTINUE (used by streaming responses, e.g. progressive download) |
| 2 | ERROR_JSON (malformed request) |
| 3 | ERROR_PIPE |
| 4 | ERROR_CANCEL |
| 5 | ERROR_RES_BUSY |
| 6 | ERROR_TIME_OUT |
| 10 | FILE_NO_EXIST |
| 11 | FILE_NAME_INVALID |
| 12 | FILE_SIZE_ERR |
| 13 | FILE_OPEN_ERR |
| 14 | FILE_READ_WRITE_ERR |
| 15 | FILE_CHECK_ERR |
| 16 | FILE_TYPE_ERR |
| 17 | STORAGE_UNAVAILABLE |
| 18 | API_VERSION_UNSUPPORT |
| 19 | FILE_EXIST |
| 20 | STORAGE_SPACE_NOT_ENOUGH |
| 21 | FILE_CREATE_ERR |
| 22 | FILE_WRITE_ERR |
| 23 | MD5_COMPARE_ERR |
| 24 | FILE_RENAME_ERR |
| 25 | SEND_ERR |
Asynchronous notifications (printer-initiated, no preceding Bambu_SendMessage) carry a cmdtype in the NOTIFY_FIRST..NOTIFY_FIRST+N range and are dispatched through PrinterFileSystem::InstallNotify.
The full set of cmdtype values is in PrinterFileSystem.h:34-45:
enum {
LIST_INFO = 0x0001,
SUB_FILE = 0x0002,
FILE_DEL = 0x0003,
FILE_DOWNLOAD = 0x0004,
FILE_UPLOAD = 0x0005,
REQUEST_MEDIA_ABILITY = 0x0007,
NOTIFY_FIRST = 0x0100,
LIST_CHANGE_NOTIFY = 0x0100,
LIST_RESYNC_NOTIFY = 0x0101,
TASK_CANCEL = 0x1000
};Per-command request shape (the req object — line numbers for the assemblers in PrinterFileSystem.cpp):
| Cmd | Hex | Origin | req fields |
Plugin maps to |
|---|---|---|---|---|
LIST_INFO |
0x0001 |
BuildFileList (...:160-175) |
{ notify, type, storage } (type ∈ {timelapse,video,model}) |
FTPS LIST <prefix>/<storage> |
SUB_FILE |
0x0002 |
thumbnail / partial fetch (...:500-540) |
{ path, name, offset, size, ... } |
FTPS RETR <path> (range emulated by reading first N bytes) |
FILE_DEL |
0x0003 |
DeleteFiles (...:776-799) |
{ paths: [...] } or { path, file } |
FTPS DELE for each <prefix>/<path> |
FILE_DOWNLOAD |
0x0004 |
DownloadFiles (...:811-829) |
{ path, file } (or mem:/<idx> for in-memory thumbnails) |
FTPS RETR <prefix>/<path> |
FILE_UPLOAD |
0x0005 |
UploadFile (...:1258-1280) |
{ path, file, size, md5, ... } + binary param |
FTPS STOR <prefix>/<path> |
REQUEST_MEDIA_ABILITY |
0x0007 |
media abilities probe (...:1228-1240) |
{} |
static answer (printer capabilities) |
TASK_CANCEL |
0x1000 |
CancelRequests (...:1469-1483) |
{ tasks: [seq, seq, ...] } |
per-job cancellation inside the worker |
LIST_CHANGE_NOTIFY |
0x0100 |
printer-initiated | "the file list changed, please refresh" | re-emits LIST_INFO to Studio |
LIST_RESYNC_NOTIFY |
0x0101 |
printer-initiated | "the printer reset its file index" | full re-fetch |
Studio's CTRL requests address files by logical storage label (storage field on LIST_INFO, path field on the others). The labels Studio knows about are sdcard and usb. The actual filesystem layout the plugin sees over FTPS depends on the printer/firmware:
LIST /sdcard.LIST /usb.LIST /that directly targets external disk drive
The plugin does the mapping. The standard recipe is to probe once per session (CWD /sdcard, CWD /usb, then CWD / as a fallback) and remember the resolved prefix on the tunnel; every subsequent request from Studio is rewritten as <ftp_prefix>/<path-from-request>. The observed prefixes per firmware family are:
| Printer family | FTPS layout | storage label Studio sends |
Effective FTPS path |
|---|---|---|---|
| X1 / P1 / A-series with microSD | /sdcard/, sometimes /usb/ if a stick is plugged in |
sdcard |
/sdcard/<path> |
| P2S (USB-only, no SD slot) | FTPS root is the USB stick — neither /sdcard nor /usb exists |
sdcard (Studio still sends this label so the radio reads "External Storage") |
/<path> (no prefix) |
| H-series / X2D | USB only, layout per upstream model enum (probe to confirm) | sdcard |
/<path> |
There are no concurrent CTRL requests on the same tunnel: PrinterFileSystem::RunRequests serialises everything on the worker thread, holding m_mutex between Bambu_SendMessage and the matching Bambu_ReadSample. This means the plugin can serve requests strictly sequentially without worrying about interleaved FTPS commands on its side.
Bambu firmware ships a stripped-down vsftpd / busybox-ftpd hybrid (the exact image varies across O1S / X1 / P1 / P2S / A-series) that deviates from RFC 959 / 4217 in several ways. None of these quirks are documented anywhere in the stock plugin or Studio source, so a fresh implementation needs to know all of them up front:
-
Implicit TLS, TCP/990. The TLS handshake starts immediately after the TCP
connect(); there is noAUTH TLSupgrade dance. A plaintext FTP client that opens 990 and waits for a220banner gets nothing — the server is already in TLS mode. -
Self-signed cert with no usable SAN. The certificate Bambu's FTPS daemon presents has no SAN that matches the printer's LAN IP, so OpenSSL hostname verification always fails. Run with
SSL_VERIFY_NONE(the same compromise the stock plugin makes for MQTT against the same printer). Pinning against the bundledprinter.cerchain works on some firmwares but not all. -
Login is
USER bblp+PASS <printer-access-code>. The 8-character code shown on the printer screen is the FTPS password. There is no anonymous mode, and no other usernames are accepted. -
Mandatory post-login sequence. After
230the client must issueTYPE I→PBSZ 0→PROT Pin that order before any data-channel command. SkippingPROT P(or sending it beforePBSZ) makes the nextPASVreply425/431depending on firmware. -
PASV only —
PORTis not implemented. The daemon either ignoresPORToutright or replies500 Unknown command. Active mode is not negotiable. -
PASV replies with a bogus IP. The first four digits of the
(h1,h2,h3,h4,p1,p2)tuple cannot be trusted: most firmwares advertise0.0.0.0, some leak a private printer-side address (192.168.x.xfrom the firmware's internal namespace) that is not reachable from the LAN. Always discard those four octets and reconnect the data socket to the same host the control connection is on. -
Delayed TLS handshake on the data channel. This is the single biggest gotcha. The wire order for a STOR/RETR/LIST is:
- send
PASV, parse the reply, TCP-connect to the printer's data port (still plaintext); - send the data command (
STOR foo/LIST/ …) on the control; - wait for the
150reply; - only now start the TLS handshake on the data socket;
- transfer payload bytes;
- close (or
SSL_shutdown) the data socket; - read the
226/250final reply on control.
If the client tries to TLS-handshake right after the TCP connect (the order most generic FTPS libraries follow), the daemon never starts its half of the handshake and the connection hangs until the data timeout.
- send
-
Data-channel TLS session reuse. Bambu's current vsftpd build accepts data sockets without session reuse, but several adjacent FTPS forks (pureftpd hardened, newer vsftpd with
require_ssl_reuse=YES) refuse otherwise. Safe to always opt in: pull the control session viaSSL_get1_session()and bind it to the data SSL withSSL_set_session()beforeSSL_connect(). -
MLSDis not implemented.FEATdoes not listMLSD, and an explicitMLSDcall returns500 Unknown commandon every observed firmware (O1S / X1 / P1 / P2S / A1). UseLISTexclusively. The output is plainls -lwith two date variants and timestamps in the printer's local time without a timezone hint:-rwxr-xr-x 1 0 0 12345 Oct 21 12:34 name # recent (HH:MM, year implicit) -rwxr-xr-x 1 0 0 98765432 Oct 21 2020 name # old / future (year explicit, no HH:MM) -
NLSTis unreliable. Some firmwares return clean filenames; others return the fullls -lblock, and a few reply502. TreatNLSTas a hint only and always be prepared to fall back toLIST+ parse-and-extract-name. -
No
MKD/RMD/APPE/REST/RNFR/RNTO/MDTM. Either the command is not wired up (response:502 Command not implemented) or it is gated off (response:550). In particular: you cannot create a directory over FTPS, and you cannot resume an interruptedSTOR— Studio's "upload retry" flow re-uploads from byte 0.SIZEis implemented (213 <bytes>),DELEis implemented,CWDworks,PWDis hit-and-miss across firmwares. -
Idle timeout ≈ 5 minutes. The control connection is torn down silently (no
421 Timeoutfirst) once it has been idle for roughly 5 minutes. The simplest fix is a reconnect-on-stale retry on the next request. -
Strictly serial commands. Pipelining or concurrent commands on the same control connection is not safe — the daemon can desynchronise its reply queue. Always wait for the previous reply (or, for data commands, the closing
226) before sending the next command. -
Only the bare command set is implemented. From RFC 959 Bambu firmware reliably implements:
USER,PASS,TYPE(Ionly —Ais accepted butSTORof an ASCII file still ships the bytes verbatim),PBSZ,PROT,PASV,LIST,RETR,STOR,DELE,SIZE,CWD,CDUP,PWD(sometimes),NOOP,QUIT. Everything else is best-effort or missing.
A few practical contracts that the Studio code path enforces but does not document:
- Tunnel ownership. Studio creates one tunnel per UI tab. The camera tab and the file-browser tab live on different
Bambu_Tunnelhandles even though they target the same printer IP. The plugin must not share state across them. Bambu_would_blockis not an error. BothBambu_OpenandBambu_StartStream*are expected to be polled (PrinterFileSystem.cpp:1738-1749,gstbambusrc.cdoes the same). Studio retries with a 100 ms backoff for up to 3-5 seconds, then gives up.Bambu_ReadSamplecontrols the wakeup cadence. On the file-browser tunnel the worker callsBambu_ReadSamplewith no separate condvar — it relies on the plugin returningBambu_would_blockinstead of blocking forever. A plugin that blocks indefinitely freezes the tab.- Negative return values are fatal. Anything outside
{0, Bambu_stream_end, Bambu_would_block, Bambu_buffer_limit}makes Studio callBambu_Close+Bambu_Destroyand try to re-open the tunnel from scratch. (PrinterFileSystem.cpp:1577-1593.) - Logger callback is signal-safe.
Bambu_SetLoggeris invoked from arbitrary threads; the receiving callback inside Studio (bambu_loginwxMediaCtrl3.cppfor current Windows/Linux Studio,bambu_loginwxMediaCtrl2.mmfor macOS,DumpLoginPrinterFileSystem.cppfor the file browser everywhere) is wrapped to be reentrant. The plugin must not assume the callback runs on a particular thread. On Windows the logger receiveswchar_t const*strings (a UTF-16 buffer), on POSIX it receiveschar const*(UTF-8); the plugin must allocate accordingly because Studio frees each message withBambu_FreeLogMsg. - Race between
Bambu_Closeand a streaming reader. Studio assumes that onceBambu_Closereturns it is safe to also callBambu_Destroy, even if another thread was blocked insideBambu_ReadSamplea microsecond earlier. A correct plugin must therefore either gracefully unblock the reader (viashutdown(SHUT_RDWR)on the underlying socket, etc.) or serialise the two; failing to do so manifests as a use-after-free during reconnect.
| Topic | File:lines |
|---|---|
| C ABI declarations / function-pointer table | 3rd_party/OrcaSlicer/src/slic3r/GUI/Printer/BambuTunnel.h |
Loader (StaticBambuLib) |
3rd_party/OrcaSlicer/src/slic3r/GUI/Printer/PrinterFileSystem.cpp:1817-1872 |
dlopen/LoadLibrary of libBambuSource |
3rd_party/OrcaSlicer/src/slic3r/Utils/BBLNetworkPlugin.cpp:272-311 |
Public accessor get_bambu_source_entry |
3rd_party/OrcaSlicer/src/slic3r/Utils/NetworkAgent.cpp:67-75 |
| Linux/Windows camera widget — current Studio (FFmpeg-based) | 3rd_party/BambuStudio/src/slic3r/GUI/wxMediaCtrl3.{cpp,h}, AVVideoDecoder.{cpp,h} |
| Linux camera back-end (gstbambusrc, used by Orca / legacy Studio) | 3rd_party/OrcaSlicer/src/slic3r/GUI/Printer/gstbambusrc.c, gstbambusrc.h |
| Windows camera back-end (DirectShow filter, COM CLSID — Orca / legacy Studio only) | 3rd_party/OrcaSlicer/src/slic3r/GUI/wxMediaCtrl2.cpp:71-138 |
macOS camera (BambuPlayer Objective-C) |
3rd_party/OrcaSlicer/src/slic3r/GUI/wxMediaCtrl2.mm:67-141, BambuPlayer/BambuPlayer.h |
Camera URL formats (bambu:///local/, rtsps___, rtsp___) |
3rd_party/OrcaSlicer/src/slic3r/GUI/MediaPlayCtrl.cpp:307-318, 551-559 |
File-browser CTRL_TYPE constant |
3rd_party/OrcaSlicer/src/slic3r/GUI/Printer/PrinterFileSystem.h:32 |
File-browser command codes (LIST_INFO etc.) |
3rd_party/OrcaSlicer/src/slic3r/GUI/Printer/PrinterFileSystem.h:34-45 |
| File-browser error codes | 3rd_party/OrcaSlicer/src/slic3r/GUI/Printer/PrinterFileSystem.h:48-72 |
CTRL JSON envelope (cmdtype/sequence/req) |
3rd_party/OrcaSlicer/src/slic3r/GUI/Printer/PrinterFileSystem.cpp:1431-1458 |
| CTRL response dispatch | 3rd_party/OrcaSlicer/src/slic3r/GUI/Printer/PrinterFileSystem.cpp:1567-1596 |
| Camera UI panel and state machine | 3rd_party/OrcaSlicer/src/slic3r/GUI/MediaPlayCtrl.cpp |
- Sanity entry point for debugging: immediately after
create_agentStudio makes the exact sequence of calls documented in § 6.1 ("Initialization sequence"). Observing those in order is the shortest way to confirm that the ABI is wired correctly. QueueOnMainFnis critical: nearly every UI-touching callback must be dispatched through this lambda — wxWidgets is not thread-safe, and direct calls from the plugin's worker threads will race.- Client certificates: the file
<resources>/cert/slicer_base64.ceris the root CA bundle Bambu uses for TLS/MQTT. It is handed to the plugin viabambu_network_set_cert_file. - ABI/STL compatibility is the single biggest foot-gun of this contract: the plugin has to be built with the exact same toolchain that built Bambu Studio (matching MSVC runtime on Windows, matching libstdc++ ABI on Linux, matching Xcode/libc++ on macOS). Any mismatch is undefined behaviour the moment a
std::string/std::mapcrosses the library boundary.
| Topic | File:lines |
|---|---|
| Resolution of all 100+ symbols | src/slic3r/Utils/NetworkAgent.cpp:279-382 |
| API typedefs | src/slic3r/Utils/NetworkAgent.hpp:10-115 |
| Name constants | src/slic3r/Utils/bambu_networking.hpp:97-100 |
| Error codes | src/slic3r/Utils/bambu_networking.hpp:13-94 |
| Data structures | src/slic3r/Utils/bambu_networking.hpp:180-275 |
InitFTModule / UnloadFTModule |
src/slic3r/Utils/FileTransferUtils.hpp:239-253 |
ft_* symbol resolution |
src/slic3r/Utils/FileTransferUtils.cpp:12-37 |
| Signature verification | src/slic3r/Utils/CertificateVerify.cpp:289-300 |
| Signature bypass | app_config → ignore_module_cert; src/slic3r/GUI/GUI_App.cpp:3423 |
| Request URL | src/slic3r/GUI/GUI_App.cpp:1469-1556 |
| Plugin download | src/slic3r/GUI/GUI_App.cpp:1573-1761 |
| Extraction / installation | src/slic3r/GUI/GUI_App.cpp:1763-1912 |
| Version check | src/slic3r/GUI/GUI_App.cpp:1982-1998 |
| Restart networking | src/slic3r/GUI/GUI_App.cpp:1914-1957 |
| Removal | src/slic3r/GUI/GUI_App.cpp:1959-1973 |
| OTA copy-in | src/slic3r/GUI/GUI_App.cpp:3359-3419 |
| Agent initialization | src/slic3r/GUI/GUI_App.cpp:3421-3519 |
OTA sync_plugins |
src/slic3r/Utils/PresetUpdater.cpp:1165-1253 |
sync_resources (shared engine) |
src/slic3r/Utils/PresetUpdater.cpp:561-737 |
| OTA cache validation | src/slic3r/Utils/PresetUpdater.cpp:1131-1163 |
| UI install job | src/slic3r/GUI/Jobs/UpgradeNetworkJob.cpp:16-146 |
| "Downloading Bambu Network Plug-in" dialog | src/slic3r/GUI/DownloadProgressDialog.cpp |
libBambuSource C ABI |
src/slic3r/GUI/Printer/BambuTunnel.h |
libBambuSource loader |
src/slic3r/GUI/Printer/PrinterFileSystem.cpp:1817-1872 |
libBambuSource dlopen/LoadLibrary |
src/slic3r/Utils/BBLNetworkPlugin.cpp:272-311 |
| Camera URL formats | src/slic3r/GUI/MediaPlayCtrl.cpp:307-318, 551-559 |
| File-browser CTRL command set | src/slic3r/GUI/Printer/PrinterFileSystem.h:32-72 |
| File-browser CTRL JSON envelope | src/slic3r/GUI/Printer/PrinterFileSystem.cpp:1431-1458 |
| Linux camera back-end (gstbambusrc — Orca / legacy Studio) | src/slic3r/GUI/Printer/gstbambusrc.{c,h} |
| Windows / Linux camera widget — current Studio (FFmpeg, C ABI) | src/slic3r/GUI/wxMediaCtrl3.{cpp,h}, AVVideoDecoder.{cpp,h} |
| Windows camera back-end (DirectShow CLSID — Orca / legacy Studio only) | src/slic3r/GUI/wxMediaCtrl2.cpp:71-138 |
macOS camera (BambuPlayer Objective-C class) |
src/slic3r/GUI/wxMediaCtrl2.mm:67-141, BambuPlayer/BambuPlayer.h |
Key facts about the stock Bambu Network Plugin, distilled from the sections above:
- Download source:
https://api.bambulab.com/v1/iot-service/api/slicer/resource?slicer/plugins/cloud=<MAJOR.MINOR.PATCH.00>(or the regional.cn/ dev / QA endpoints), which returns a JSON manifest pointing at a ZIP. - Install layout: the binary ends up at
<data_dir>/plugins/{bambu_networking,BambuSource,live555}.{dll|so|dylib}; OTA staging in<data_dir>/ota/plugins/must hold all three libraries plusnetwork_plugins.jsonor the cache is treated as incomplete. - Version gate: Studio compares only the first 8 characters of
bambu_network_get_version()againstSLIC3R_VERSION; everything beyond that is build metadata. - Signature gate: Authenticode publisher match on Windows, Developer Team ID match on macOS; on Linux the check is a no-op.
ignore_module_certinAppConfigdisables it on Windows/macOS. - ABI surface: roughly 100
bambu_network_*entry points using C linkage butstd::string/std::vector/std::map/std::functionat the boundary — tightly coupled to Studio's libstdc++/libc++ ABI — plus a separate, pure-Cft_*tunnel/job bus (ft_abi_version() == 1) that ships in the same.so/.dll. - Initialization contract: a deterministic call sequence
create_agent → set_config_dir → init_log → set_cert_file → set_extra_http_header → set_on_*_fn(…) → set_country_code → start → start_discovery(GUI_App::on_init_network), withQueueOnMainFnas the only safe way back to the GUI thread. - Notable Studio quirks observed during reverse engineering: the
bambu_network_get_user_nickanmesymbol name is misspelled in the real ABI, and Studio mistakenly resolvesget_my_tokenthrough the string"bambu_network_get_my_profile"— a compatible plugin must export both, with matching signatures. - Second library, second contract: camera live view and the on-printer file browser go through a separate library
libBambuSource(different symbol prefixBambu_*, different loader, no signature gate, no version gate). It exposes a small C ABI (Bambu_Create/Bambu_Open/Bambu_StartStreamEx/Bambu_SendMessage/Bambu_ReadSample/ …) plus, on macOS only, an Objective-C classBambuPlayerresolved throughdlsym(libBambuSource.dylib, "OBJC_CLASS_$_BambuPlayer")insidewxMediaCtrl2.mm. The camera widget that consumes this ABI varies by slicer: current BambuStudio (commit94d91be60+, June 2024) useswxMediaCtrl3which callsBambu_*directly and decodes via FFmpeg on both Linux and Windows; OrcaSlicer and pre-94d91be60Studio still use the olderwxMediaCtrl2which routes Linux through thegstbambusrcGStreamer element baked into the Studio binary and Windows through a DirectShow source filter registered against thebambu:URI scheme (CLSID{233E64FB-…}) — meaning a WindowsBambuSource.dllaimed at Orca compatibility must additionally export the COM filter, while a Studio-only port can stick to the C ABI. The file browser uses the same camera tunnel (TLS over TCP/6000) but switches it into JSON-RPC mode viaBambu_StartStreamEx(tunnel, CTRL_TYPE = 0x3001); the actual file bytes travel over a separate implicit-FTPS connection on TCP/990 that the plugin opens itself. See §7 for the full ABI, wire format and per-platform back-ends.