Bambu Lab β LAN Protocol¶
Status: π’ hardware-validated (A1 Mini fw
01.08.00.00, AMS Lite + a third-party AMS clone; read + single/multi-color launch) β X1/P1/H2 specifics are π‘ source-read Β· Firmware: proprietary Bambu Β· Models: X1 / P1 / A1 / P2 / X2 / H2 familiesBambu printers speak two LAN transports at once β MQTT over TLS
:8883for state and control, implicit FTPS:990for the file β both unlocked by turning LAN mode on with a per-device access code. There is no command ack: you confirm every write by watching the status stream.
At a glance¶
- Transport: MQTT/TLS
:8883(userbblp, password = the printer's access code) plus implicit-TLS FTPS:990(bblp/ same access code). Both LAN-local. - Discovery: SSDP on
:2021(urn:bambulab-com:device:<model>,USN= serial); manual IP always works. - Auth / credential: the access code shown on the printer once local access is enabled β a user secret, read from the machine's own screen. No client cert, no cloud token. See Credentials.
- Read / status: publish a
pushallfor a full snapshot, then consume live deltas.gcode_statedrives the lifecycle; temps/progress/AMS/faults ride along. - File transfer: FTPS upload the
.3mfto the SD-card root (a strict, non-RFC dialect β see the gotcha). - Print launch: two transports β FTPS-upload, then MQTT
project_filereferencing the uploaded file. - Feeders / multi-material: AMS (full / Lite / high-temp) + external spool. See Multi-material.
- β οΈ The load-bearing gotchas: (1) the FTPS data channel must be TLS-handshaked only after the
150β generic libraries that wrap it right afterconnect()hang; (2) the serial is case-sensitive everywhere (topic, SNI, cert CN) β a miscased serial connects fine but returns zero reports.
Transport & connection¶
A printer with local access enabled exposes two LAN transports; a full client needs both.
| Transport | Port | TLS | Auth |
|---|---|---|---|
| MQTT | 8883 | TLS (see below) | user bblp, password = access code |
| FTPS (implicit) | 990 | implicit TLS on connect | bblp / access code |
Identity per printer = (ip, serial, access_code). The serial is reused as the MQTT topic key, the TLS
SNI name, and the device cert CN β and it is case-sensitive. A miscased serial connects, subscribes, and
publishes without error but yields zero reports (the canonical silent failure β detect it as "connected but 0
inbound messages"). π’
MQTT session shape. MQTT v3.1.1; a unique client_id per (re)connect (reused ids leave zombie sessions on
the broker). Every publish uses qos=1 β the printer ignores qos-0 commands while it is busy broadcasting
status. Raise the inflight ceiling well past paho's default of 20 (e.g. to ~1000): the broker's PUBACK matching is
racy against a low ceiling and wedges the session after ~16β20 cumulative commands. π‘ Keepalive ~30 s, plus an
app-level staleness timer (~60 s): some firmware stops publishing while the TCP socket stays alive, so treat a
silent-but-connected session as dead and reconnect with a fresh client_id (which also discards any stale qos-1
backlog so an old launch can't replay). π‘ Bench idle streams ran ~16β28 frames / 90 s with a max silent gap ~7 s, so
the 60 s window is generous. π’
TLS β two strategies. The printer presents a self-signed-from-a-Bambu-CA cert (CN = serial, no SAN):
- Verify-off β
CERT_NONE, hostname check off, no client cert. Works everywhere; the simplest path. - Proper verification (preferred) β trust Bambu's published device-CA cert and connect with SNI = serial
(you dial the IP but verify against the serial). On Python 3.13+ clear the strict-X.509 flag (the cert omits a
key-usage extension the strict verifier rejects). Newer device CAs may not chain against the bundled root β
offer pin-the-leaf-on-first-use (snapshot the leaf, CN-check thereafter), with verify-off as the final
fallback. Apply the same CN-check on the FTPS and camera ports. Verified end-to-end on both
:8883and:990against a real unit with no fallback firing. π’
Topics & envelope β flat, no wrapper. Subscribe device/<serial>/report; publish device/<serial>/request.
Payloads are category-keyed JSON, the category being one of print Β· pushing Β· info Β· system Β· xcam Β·
camera Β· upgrade; the verb lives in an inner command field, e.g. {"pushing":{"command":"pushall"}}. A string
sequence_id (often the literal "0") is loose correlation, not a strict request/reply map β there is no
command ack; confirm effects by watching the status stream (see Reading state). π’
Discovery & identity¶
- SSDP on UDP
:2021β service typeurn:bambulab-com:device:<model>,USN= serial,Location= the bare IP, and aDevModel.bambu.comheader carrying the model_id code (not a friendly name), plusDevVersion(firmware) andDevName(user's name). Confirmed live for the A-series (N1= A1 mini,N2S= A1). π’ - Manual IP always works and is the guaranteed onboarding path β see
../patterns/discovery-and-credentials.md. - The model is not required to connect β it only refines feature behavior (chamber-sensor presence, FTPS quirks,
AMS type). Read the friendly firmware/module identity from the MQTT
get_versionreply once connected. π’
The model_id code space is first-party (from Bambu's own machine profiles) and worth pinning, because the widely
copied community map is wrong (it maps C11βX1C when C11 is P1P):
| code | model | Β· | code | model |
|---|---|---|---|---|
BL-P001 |
X1 Carbon | Β· | N1 |
A1 mini |
BL-P002 |
X1 | Β· | N2S |
A1 |
C13 |
X1E | Β· | N7 |
P2S |
C11 |
P1P | Β· | N6 |
X2D |
C12 |
P1S | Β· | O1D |
H2D |
π‘ (A-series rows π’, confirmed by live SSDP capture). Treat auto-detection as a pre-fill, let the user confirm the model, never hard-fail on an unknown code (fall back to the raw string), and derive capabilities from live report structure, not the model string β e.g. detect dual-nozzle from the report's extruder-info length β₯ 2, so an H2D is handled even if its code never made a table.
Credentials / auth¶
"Developer Mode" is just LAN mode β don't over-read the name. Bambu's LAN-Only Mode disables the cloud; the
Developer Mode toggle inside it opens the local MQTT/FTP broker and surfaces an access code. That is the exact
same "cloud off β local access via a per-device code" posture as every other brand (Anycubic/Creality/Elegoo LAN
access, Snapmaker's auth-code, FlashForge's check-code). It is a normal onboarding precondition to surface, not a
special gate β see ../patterns/discovery-and-credentials.md. The
access code is the user's own secret, read from the printer's screen; this reference documents how a user obtains
theirs, never a value.
Why the LAN path is the clean one. On stock (cloud) firmware, privileged commands such as project_file
must carry an RSA-signed header envelope and a url_enc (the source URL AES-encrypted, the AES key RSA-wrapped
against the device cert) β and the signing key lives inside Bambu's closed networking plugin. Unsigned commands
are rejected with MQTT Command verification failed (error 84033543). The LAN/local path sends plain commands
and needs zero vendor secrets β plain url, no signed header. That is why an implementer should scope strictly
to LAN mode: it both unlocks the printer and stays clear of any signing-key material. π‘
Per-firmware nuance (be honest): the signing-enforcement framing above describes a newer firmware wave. On the bench A1 Mini (
01.08.00.00), the local broker behaved identically with the Developer-Mode toggle off β connect, subscribe, status stream, and even a would-be-privileged command all succeeded, noverify failed. So on that build the toggle is effectively cosmetic for LAN MQTT. A client should still detect and surface a monitor-only state for firmware that does enforce (see the dev-mode probe below), but must not assume every build enforces. The enforcing (verify failed) path has not been captured live. π’/π‘
Dev-mode / write-availability probe. X1/H2 firmware advertises a fun bitfield in the status object
(dev_mode_on = (fun & 0x20000000) == 0); A1/P1 never send fun, so probe instead: wait β₯5 s after connect
(probing too early destabilizes some brokers), send a guaranteed no-op write (echo the printer's own external-spool
values back at it, scraped from the snapshot β skip the probe entirely if no snapshot yet, so you never risk a real
write), and read the result: a failed result whose reason says verify failed β writes are gated (monitor-only);
any other response β writes available. This doubles as zombie-session detection. π’
Reading state¶
Publish {"pushing":{"command":"pushall"}} on connect for a full snapshot, then consume live deltas off
device/<serial>/report. Dispatch each report by its top-level category key. The same field can arrive at top level
or nested under print, and P-series firmware sends only the changed keys while X-series sends the full object β so
deep-merge partials, never replace. π’ This is a poll-hybrid-ish push model: subscribe, snapshot, diff.
- Lifecycle β
gcode_state(native β normalized "the neutral state any client needs"): π’
| native | normalized |
|---|---|
IDLE |
standby |
PREPARE, SLICING, RUNNING |
printing |
PAUSE |
paused |
FINISH |
complete |
FAILED |
error |
β οΈ An aborted print returns to IDLE, so a failed job reads as plain standby unless you also surface the
fault channels (below) β the two are otherwise indistinguishable. π’
- Progress:
mc_percent(0β100) is a time-based estimate β note it includes the calibration phase, so percent can advance whilelayer_numis still 0.mc_remaining_timeis in minutes (a classic cross-brand unit trap β see../patterns/timing-normalization.md);layer_num/total_layer_numtrack layers. π’ - Temperatures:
nozzle_temper/bed_temper/chamber_temperand their*_target_tempercompanions, plain Β°C. βͺ Some encoded forms are reported to packtarget*65536 + currentwhen the value exceeds 500 β unconfirmed on hardware; validate per field/model.chamber_temperis absent or meaningless on P1/A1-class (the bench A1 Mini reported a bogus fixed ~5 Β°C with no chamber sensor). π’ - Fans: part (
cooling_fan_speed), aux (big_fan1_speed), chamber (big_fan2_speed), heatbreak. - Firmware: the
get_versionreply returns amodule[]array; firmware = theotamodule'ssw_ver. Module names (ams/N,n3f/N,ams_f1/N,th,mc, β¦) also reveal hardware and AMS type. π’ - Faults:
hms[](an array of{attr, code}) plus a separate 32-bitprint_error. See Faults & HMS.
Writing / control¶
All commands publish to device/<serial>/request at qos 1, category-keyed. There is no command ack β
confirm by watching the status stream. Core verbs: π’/π‘
| Command | Category / shape | Purpose |
|---|---|---|
pause / resume / stop |
print |
Job control |
print_speed |
print, param "1".."4" |
silent / standard / sport / ludicrous |
ledctrl |
system, led_node chamber_light|work_light |
Lights β the flashing params are required even for plain on/off |
gcode_line |
print, param = raw G-code |
Arbitrary G-code |
calibration |
print, option bitmask |
lidar=1, bed=1<<1, vib=1<<2, motor=1<<3 |
print_option |
print |
auto_recovery, filament_tangle_detect, β¦ |
skip_objects |
print, obj_list:[ids] |
Cancel objects mid-print (echoes s_obj) |
xcam_control_set |
xcam |
AI-detector toggles |
project_file |
print |
Start a print (below) |
| AMS load/unload/setting | print |
See Multi-material |
β οΈ Control writes drive a hot, moving machine. Validate against your own device and gate them behind an explicit "enable writes" in any client β a
π‘control fact is far riskier than aπ‘read fact.
Print launch β two transports, then poll. π’
- FTPS-upload the
.3mfto the SD-card root (dialect below), wait for the closing226, and SIZE-verify (server size == local size) so a partial file never gets a print command. - MQTT publish
{"print":{"command":"project_file", β¦}}referencing the uploaded file. Load-bearing fields: param=Metadata/plate_<N>.gcodeβ this is how the 1-indexed plate is conveyed (the gcode for that plate must exist inside the 3MF).url=ftp://<bare-name>(plain URL on the local path β the stock firmware'surl_encis not needed),file= the same bare name.md5= empty string (skip validation; a wrong synthetic digest risks activating validation).bed_type="auto"(or an explicit plate type).- Calibration flags (
timelapse,bed_leveling,flow_cali,vibration_cali,layer_inspect) must be JSON booleans for all models (int-encoding broke H2 flow-cali).use_amsmust stay boolean (an H2D Pro reads an int here as a nozzle index). - IDs (
project_id/task_id/subtask_id/β¦) are strings. A fresh, int32-capped id per launch is the safe choice β some firmware clamps oversized epoch-ms ids and treats a reprint as a continuation, wedging in IDLE. π‘ ams_mapping+ams_mapping2for multi-color routing;nozzle_mappingon multi-extruder units β see Multi-material.- Confirm by polling the status stream β accept a transition into
{PREPARE, SLICING, RUNNING, PAUSE}orsubtask_idadvancing to your submission id. Don't accept arbitrary state changes. β οΈ A launch can be accepted then aborted by a printer-side precondition (e.g. the screen sitting on a menu that blocks a remote start) β thesubtask_idecho proves the command landed, not that the print succeeded, so also watchprint_errorafter dispatch. π’ H2D can sit at FINISH ~50 s before flipping to PREPARE, so thesubtask_idecho is the early signal there. π‘
The FTPS dialect (implement to spec)¶
Bambu firmware runs a stripped FTPS server that deviates from RFC 959/4217. The rules that matter: π’/π‘
- Implicit TLS on
:990β TLS starts on connect; there is noAUTH TLS(waiting for a plaintext220banner hangs). - Mandatory post-login order:
TYPE IβPBSZ 0βPROT P, in that order, before any data command (reordering makes the nextPASVfail). - PASV only (
PORT/active is unimplemented) β and PASV returns a bogus host IP; discard the four host octets and reconnect the data socket to the control connection's host. - β Delayed data-channel TLS (the #1 gotcha):
PASVβ TCP-connect the data socket plaintext β send STOR/RETR/LIST on control β wait for150β only now TLS-handshake the data socket β transfer β read the226/250. Libraries that handshake right afterconnect()hang until timeout. - β Read the closing
226with the transfer timeout, not a short command timeout β the firmware sends the final ack only after flushing the whole file to storage, so ack latency scales with file size. A ~16 MB multicolor 3MF's226arrives well past a typical ~15 s command budget; a post-transfer ack is part of the transfer, so budget it accordingly. (Small single-color files hide this bug.) π’ - Data-channel TLS session reuse β opt in by default (bind the control session onto the data socket); the A1
family may instead need a clear data channel (
prot_c), and P2S/X2D may need TLS capped to 1.2 (auto-detect / fall back). On the bench A1 Mini the standardPROT P+ session-reuse posture worked β the clear-channel lore did not apply to that build. π’ (other-model quirks π‘) - No
MLSD/MKD/APPE/RESTβ can't create dirs, can't resume (a failed upload re-STORs from byte 0); parse a plainLIST.SIZE,DELE,CWDwork. Sessions idle-timeout silently at ~5 min. - Storage prefix varies by model β probe once (
CWD /sdcardβ/usbβ/) and cache it (X1/P1/A-series typically/sdcard; the bench A1 Mini fell through to/; USB-only P2S roots at the stick). Theurlmust match where the file actually landed. - Filenames: strip to the stem + a single
.3mf, replace spaces with underscores (the firmware parsesftp://{name}as a URL β spaces break it), and reject shell/URL-illegal characters or you get an opaque553.DELEthe remote name before re-uploading. π‘
Multi-material / feeders¶
Bambu's AMS is a rich feeder (per-slot RFID UUID, recommended temps, color name, live flow-K) β a superset of
what most brands expose over LAN. It folds cleanly into the neutral feeder model in
../patterns/multi-material-feeders.md (a bank of slots β one hotend, plus
an external spool); everything below the opaque per-slot atom (RFID, remaining %, dry-box) is optional and degrades.
Where it arrives. Under print.ams in the snapshot, or as partial top-level ams/tray objects in
incremental updates β deep-merge at the tray level so RFID/sub-brand/drying fields survive. External spools ride
in vt_tray (single dict β X1C/P1S/A1) or vir_slot (list β H2 series). π’
Global slot addressing (keep this opaque per the pattern's slot_key invariant): standard global = ams_id*4 +
slot (inverse ams_id = g//4, slot = g%4); AMS-HT units are single-tray with id β₯ 128 and global = id;
external spools are 254 (single / left) and 255 (right, on dual-nozzle). tray_now is the active global tray
(255 = none, 254 = external); a raw 0..3 is ambiguous on multi-AMS/dual-nozzle and must be disambiguated via
the load-time target you set. π’
Per-slot fields (each degrades to null): π’
| Field | Meaning |
|---|---|
tray_type |
base material |
tray_sub_brands |
variant |
tray_color |
RRGGBBAA hex β normalize to RRGGBB at the boundary; β¦00 alpha = clear |
tray_info_idx |
the filament preset id β the load-bearing calibration key |
remain |
percent, RFID-estimated (Γ tray_weight for grams) |
tray_weight |
grams (g) |
k / cali_idx |
live flow-K + profile index, -1 = default |
tag_uid / tray_uuid |
RFID identity β the UUID is the preferred stable id |
| recommended nozzle-temp window | |
state |
9 empty / 10 present-not-fed / 11 loaded |
Variants β detect by presence of data, not by model/AMS name (a third-party AMS clone on the bench reported as a full "AMS Pro 2" yet had genuinely no RFID/humidity/dryer hardware, all-zero identity β so gate sub-capabilities on real observed values, never on the unit's claimed identity): π’
- Full AMS (X1/P1/P2): 4 trays, RFID + humidity + drying.
- AMS Lite (A1 / A1 mini): 4 slots, no RFID/UUID (module name
n3f/n3s/ams_f1; identity falls back to user assignment). - AMS-HT (high-temp): single tray per unit,
id β₯ 128. - External / virtual spool: same tray shape; its loaded flag doubles as the runout signal.
AMS commands (all {"print":{β¦}}, qos 1, ack-less β watch ams_status main-code 1 = filament-change and
tray_now settling): π’
- Load / unload β
ams_change_filament. Load passes-1temps (firmware picks the temp from the tray preset); unload and preset-less external-right loads pass the real current nozzle temp (the head must be hot to retract). Load auto-unloads whatever is present first. ams_controlβ resume/reset/pause a stuck change.ams_filament_settingβ set a slot's material/color/temps/preset (uses the local tray id).extrusion_cali_selβ select a flow-K profile. β οΈ Trap: this one uses the global tray id (unlikeams_filament_setting) and must not carry asetting_id.ams_get_rfidβ re-read a tag; valid only when nothing is loaded (tray_now == 255).
Print-time routing β emit two fields together: π’
ams_mappingβ flat array indexed by 3MF filament slot, carrying global ids, but external (254/255) and unmapped rewritten to-1(sending raw 254/255 here triggers a "failed to get AMS mapping table" fault).ams_mapping2β the parallel[{ams_id, slot_id}]form carrying real external routing; this is the firmware-preferred form (firmware that supports both prefers it).nozzle_mapping(flat int array, 3MF-slot β physical nozzle) on multi-extruder / H2D only.
This colorβslot map is the key to per-material accounting β see the pattern doc. Validated live: single-color
(ams_mapping=[3]) and 3-color (ams_mapping=[0,2,3] + three ams_mapping2 entries) launches both fed the correct
slots, confirmed by subtask_id echo and a live tray_now tool-change. π’
Faults & HMS¶
Two raw fault channels in the status object: hms[] ({attr, code} objects) and a separate 32-bit
print_error (MMMM_EEEE). Decode each: severity nibble = (attr >> 8) & 0xF (1 fatal / 2 serious / 3 common
/ 4 info), module = (attr >> 24) & 0xFF, and a short_code = f"{(attr>>16)&0xFFFF:04X}_{code&0xFFFF:04X}" for
lookup. Two filters are essential or the UI lights up during a normal print: π‘
- Status-not-fault: skip if the low half
< 0x4000(firmware emits low values as normal-phase status). - User-action echoes: skip a small hand-maintained set of short codes the firmware emits during normal user-cancel sequences.
Fold print_error through the same decode/filters and dedupe it against hms[]. The human-readable message
catalog (~800 codes) is not a protocol fact and should be regenerated from Bambu's own published error-code list
β do not copy the copyleft community dictionaries. Retaining raw attr+code lets a client build Bambu's HMS
deep-link so unknown codes stay actionable without a complete catalog. π‘ The live filter behavior was proven correct
on the bench (an AMS clone's steady-state zeros-HMS decoded as info/status-not-fault and was correctly dropped; an
accepted-then-aborted launch surfaced a real β₯0x4000 print_error). π’
Quirks & gotchas¶
- Serial is case-sensitive across topic / SNI / cert CN β wrong case = connect OK, 0 reports. π’
- No command ack anywhere β confirm every write (launch, pause, AMS change) by watching the status stream. π’
- Delayed data-channel TLS on FTPS and the transfer-timeout on the closing
226β the two FTPS traps that cost the most hours (above). π’ - P-series sends only changed keys, X-series sends the full object β always deep-merge partials, including at the AMS tray level. π’
use_amsand the cali flags must stay booleans;md5must be empty; the plate index rides inparam, not a numeric field. π‘funis absent on A1/P1 β probe for write-availability rather than reading a bit. π’- Chamber temp is bogus/absent on P1/A1-class β filter it out for those. π’
- Derive capabilities from live report structure, not the model string β and the community model-code map is
wrong/incomplete (e.g.
C11is P1P, not X1C); never hard-fail on an unknown code. π‘ - Raise the MQTT inflight ceiling and use a fresh
client_idon reconnect or the session wedges after a handful of commands / replays stale launches. π‘
Confidence & validation¶
- π’ Hardware-validated on an A1 Mini (fw
01.08.00.00) with AMS Lite and a third-party AMS clone: verified TLS (BBL-CA + SNI=serial) on both:8883and:990; MQTT connect +pushall+ live deltas;get_versionfirmware; lifecycle/temps/fans/lights parse; AMS-Lite slot/color/exist-bit hygiene; the dev-mode probe; the full FTPS dialect (implicit TLS,TYPE/PBSZ/PROT P, PASV-IP discard, delayed data-TLS + session reuse, STOR + SIZE-verify + DELE); and single- and multi-colorproject_filelaunches confirmed bysubtask_idecho and a live tool-change. SSDP identity confirmed live for the A-series (N1,N2S). The FTPS large-file transfer-timeout and the accepted-then-aborted launch semantics were both found and proven on this unit. - π‘ Source-read (first-party docs / machine profiles, not yet hardware-confirmed): the stock-firmware signing /
url_encdetails; per-model FTPS quirks beyond the A1 (prot_cclear-channel, TLS-1.2 cap on P2S/X2D); the X1/P1fun-bit dev-mode read; the HMS decode thresholds and severity map; the fullmodel_idtable beyond the A-series; H2D dual-nozzlenozzle_mapping/ dual-external routing. - βͺ Inferred / unconfirmed: the
target*65536 + currenttemperature encoding (which fields/models). - Open gaps β the captures that would close them: the Developer-Mode-OFF path under an enforcing firmware
(the
verify failed/84033543rejection has never been seen live β needs an X1/P1-class unit or a post-update A1); an X1 or P1 bench (full AMS with RFID, thefundev-mode path, full-object-not-partial reports); an H2D (dual-nozzletray_nowdisambiguation,nozzle_mapping, dual-external); the temp-encoding confirmation; and a live HMS/print_error catalog with the exact wiki deep-link format.
Sources¶
Clean-room, facts-only. Built primarily from two clean, citable public sources β OpenBambuAPI (GFDL-1.3
documentation: MQTT command/report shapes, ports, the TLS recipe, the device-CA cert, the LAN file behavior) and
scarlton/open-bambu-networking (MIT, clean-room: the full FTPS dialect, the signing/url_enc requirement, the
cross-model project_file field map, nozzle_mapping) β plus Bambu's own first-party machine profiles for the
model_id table, corroborated by owner hardware captures (A1 Mini + AMS Lite + a third-party AMS clone, sanitized;
sanitized wire-shape fixtures for pushall (X1C), get_version, and P1 partial / AMS-partial updates back the schema
tests). Copyleft management apps were used as behavioral reference only β no code copied. No certificate, key,
access code, serial, or real IP is reproduced; credentials are described as a mechanism (the access code the user
reads from their own printer), never a value. Bambu's closed networking plugin and the projects redistributing it are
neither used nor referenced for key material. Passed
../CLEANROOM-CHECKLIST.md.