bambu-mqtt-generator
Python library for building, signing, and interpreting Bambu Lab printer MQTT messages — primarily for reading and changing AMS and external spool filament settings.
Payload shapes, field types, filament presets, and the AMS response format are extracted from the Bambu Studio source by bambu-mqtt-extractor, so generated payloads should match what Bambu Studio itself sends for a given printer and firmware.
This library is focused on generating MQTT command payloads, but does not provide a method to send them. Sending commands can be handled by the separate bambu-mqtt-comms companion library, which is just a Bambu-focused wrapper around the paho-mqtt library.
This library's code is primarily AI generated and maintained, but the aspects related to AMS and filament settings are validated against real printers to ensure correctness. Functionality unrelated to that use has NOT been tested or validated. You use this library at your own risk.
Installation
pip install bambu-mqtt-generator
From a source checkout:
pip install . # or: pip install -e . for development
Requires Python 3.8+ and cryptography.
Quick start
from bambu_mqtt_generator import get_payload_builder, ExternalSpool
# Printer model and firmware version
builder = get_payload_builder("A1 mini", "01.05.00.00")
payload = builder.build_filament_setting(
tray_info_idx="GFA00", # Bambu PLA Basic
tray_color="00FF00", # "RRGGBB" or "RRGGBBAA", optional "#" prefix
ams_id=0, # AMS unit 0-3, or ExternalSpool.MAIN / .DEPUTY
tray_id=2, # slot 0-3 (ignored for an external spool)
)
{
"print": {
"sequence_id": "21166",
"ams_id": 0,
"tray_id": 2,
"slot_id": 2,
"tray_info_idx": "GFA00",
"setting_id": "",
"tray_color": "00FF00FF",
"nozzle_temp_min": 190,
"nozzle_temp_max": 240,
"tray_type": "PLA",
"command": "ams_filament_setting"
}
}
Temperatures and type were filled in from the GFA00 preset because they
weren't supplied.
Supported printers
get_payload_builder() takes either the printer name or the model id.
| Printer | Model ID | Printer | Model ID | |
|---|---|---|---|---|
| X1 Carbon | BL-P001 | A1 | N2S | |
| X1 | BL-P002 | A2L | N9 | |
| X1E | C13 | H2C | O1C | |
| X2D | N6 | H2C | O1C2 | |
| P1P | C11 | H2D | O1D | |
| P1S | C12 | H2D Pro | O1E | |
| P2S | N7 | H2S | O1S | |
| A1 mini | N1 |
Both forms are case-insensitive, and surrounding or repeated whitespace is
ignored. An unrecognized printer raises ValueError.
H2C is the one name that isn't accepted: Bambu Studio ships it as two model ids
with different feature flags (O1C2 is the O1C2-V2 subseries), so the name
alone doesn't identify a printer. Passing "H2C" raises
AmbiguousPrinterNameError — a ValueError subclass — listing both candidates;
pass "O1C" or "O1C2". The printer reports which one it is in get_version.
Complete workflow
Read the AMS, change a slot, and confirm the change actually landed.
import time
from bambu_mqtt_comms import BambuMQTTClient, PrinterConfig
from bambu_mqtt_generator import (
MQTTSigner, check_command_result, get_payload_builder, load_config,
load_pem, parse_response,
)
MODEL, FIRMWARE = "N1", "01.05.00.00"
config = load_config()
builder = get_payload_builder(MODEL, FIRMWARE)
signer = MQTTSigner(
cert_pem=load_pem("certs/cert.pem"),
key_pem=load_pem("certs/key.pem"),
cert_chain_pem=load_pem("certs/cert_chain.pem"),
crl_pem=load_pem("certs/crl.pem"),
)
printer = PrinterConfig(ip="192.168.1.100", serial="01S00A123456789",
access_code="abcdef12")
with BambuMQTTClient(printer) as client:
# 1. Register the certificate; signed commands are rejected until this lands.
if not client.install_app_cert(signer.build_app_cert_install(),
cert_id=signer.get_cert_id())["trusted"]:
raise RuntimeError("printer never trusted our certificate")
# 2. Read the current slots.
status = client.request_status()
slots = parse_response({"print": status}, MODEL, FIRMWARE, config)["slots"]
slot = next(s for s in slots
if s["ids"]["amsID"] == 0 and s["ids"]["slotID"] == 2)
# 3. Build, sign, and send.
payload = builder.build_filament_setting(
tray_info_idx="GFA00", tray_color="0000FFFF",
ams_id=slot["ids"]["amsID"], tray_id=slot["ids"]["slotID"],
)
ack = client.send_and_wait(signer.sign(payload))
result = check_command_result(ack)
if not result["accepted"]:
raise RuntimeError(f"rejected: {result['err_code']} {result['description']}")
# 4. An accepted command is not proof of effect — re-read and compare.
# The printer applies the change asynchronously, so retry the read.
for _ in range(5):
time.sleep(2)
status = client.request_status()
slots = parse_response({"print": status}, MODEL, FIRMWARE, config)["slots"]
after = next(s for s in slots
if s["ids"]["amsID"] == 0 and s["ids"]["slotID"] == 2)
if after["tray_color"].upper() == "0000FFFF":
break
else:
raise RuntimeError(f"accepted but not applied: {after['tray_color']}")
Building payloads
build_payload()
build_payload() allows you to generate any payload that Bambu Studio supports, based on the provided config files.
It validates that every field Bambu Studio always sends is present, raising ValueError listing what is missing.
builder.build_payload(
"ams_filament_settings",
sequence_id=None, ams_id=0, tray_id=2, slot_id=2,
tray_info_idx="GFZ99", setting_id="", tray_color="FF0000FF",
nozzle_temp_min=190, nozzle_temp_max=240, tray_type="PLA",
)
The first argument is the command spec's name, which is the name Bambu Studio
gives the operation — not always the string that goes on the wire. The spec
above is ams_filament_settings, but it sends "command": "ams_filament_setting".
Either name works; they select the same spec and build the same payload.
A wire name used by more than one spec can't select one on its own — three
specs send print_option, and three send stop. Those raise
AmbiguousCommandNameError (a ValueError subclass) listing the spec names to
choose from. config/commands/ holds every spec name.
However, since the primary goal of this library is to support AMS filament control, there are several dedicated helper functions related to those tasks to make them easier.
build_filament_setting()
The method to use for filament changes. It routes to a physical AMS or an
external spool based on ams_id, and derives the ids Bambu Studio would send.
| Parameter | Required | Default | Notes |
|---|---|---|---|
tray_info_idx |
yes | — | Filament ID, e.g. "GFA00" |
tray_color |
yes | — | "RRGGBB" or "RRGGBBAA", optional #; alpha defaults to FF |
ams_id |
yes | — | 0-3 for a physical AMS, or ExternalSpool.MAIN (255) / .DEPUTY (254) |
tray_id |
yes* | — | Slot 0-3; ignored for an external spool |
slot_id |
from tray_id |
Physical AMS only; supply either one and the other is derived | |
nozzle_temp_min |
from preset | °C | |
nozzle_temp_max |
from preset | °C | |
tray_type |
from preset | "PLA", "PETG", … — the preset default already accounts for support filaments, so leave it unset unless you mean to override it |
|
setting_id |
"" |
Filament preset id; empty means no user preset | |
sequence_id |
random | 5-digit string starting with 2 |
|
cols, ctype |
omitted | Multi-color spools; see below |
* For a physical AMS you must supply tray_id or slot_id; they must be equal.
Raises ValueError for an unknown filament id, an out-of-range AMS or slot id,
or a physical AMS with neither id supplied.
AMS and external spool ids
Ids are used exactly as the printer reports them in push_status, and are
0-based.
| Target | ams_id |
tray_id |
slot_id |
|---|---|---|---|
| Physical AMS | 0-3 |
0-3 |
same as tray_id |
| External spool (main) | 255 |
254 |
0 |
| External spool (deputy) | 254 |
254 |
0 |
Multi-color spools: cols and ctype
tray_color holds one color. A spool that isn't one flat color is described by
the pair cols + ctype:
cols— the colors on the spool, as"RRGGBBAA"strings, in order. For a plain spool it is just[tray_color].ctype— color type: how those colors are laid out. It has nothing to do with the filament material.
ctype |
Meaning | Rendered as |
|---|---|---|
0 |
Gradient — colors blend continuously | a linear gradient across cols |
1 |
Multi-color — discrete bands | one solid segment per entry in cols |
2 |
Single color | one solid color |
ctype only carries meaning when cols has two or more entries. With a single
color, Bambu Studio ignores the reported value and treats the spool as single
(2); printers commonly report ctype: 0 on a single-color tray, which is not
a gradient but an unset field.
Only printers that advertise manual multi-color editing — bit 23 of the fun2
field in push_status — accept these; Bambu Studio omits both fields
otherwise and sends the plain single-color command. Many current printers do
not set that bit, so check it before sending cols/ctype.
When cross-checking against Bambu Studio, disregard the
DevFilaColorTypeenum inDevFilaSystem.h— its names (CTYPE_MULTI = 0,CTYPE_GRADIANT = 1) contradict the wire values above. The source acknowledges this inwgtFilaManagerColorType.h: "DevFilaColorType's legacy enumerator names are misleading, so never switch on those names here."
Other convenience commands
| Method | Command |
|---|---|
build_pushall() |
pushall — request a full status push |
build_get_version() |
get_version |
build_get_access_code() |
get_access_code |
build_ams_change_filament() |
ams_change_filament — load/unload |
build_ams_control(action) |
ams_control — resume/pause/abort/reset/done |
build_gcode(gcode) |
gcode_line — raw G-code |
build_set_nozzle_temp() |
set_nozzle_temp |
build_payload(name, **fields) |
any command in config/commands/ |
Several AMS operations are G-code rather than JSON commands — refresh RFID,
calibrate, and select tray all send M620 via build_gcode().
Reading responses
parse_response(response, model_id, firmware_version, config=None) -> dict
Normalizes a push_status into {"success": True, "slots": [...], "raw": ...}.
Pass the reply wrapped as {"print": status}.
Each slot carries the raw printer fields (tray_color, tray_info_idx,
tray_type, nozzle_temp_min/_max, …) plus normalized additions:
{
"ids": {"amsID": 255, "slotID": 0},
"Type": "PLA", "Color": "00FF00", "Brand": "Bambu",
"Min Temp": 190, "Max Temp": 240, "Bed Temp": 55,
"displayID": "External Spool (no AMS)",
"present": True, "info_known": True,
}
Every slot an AMS has is listed, loaded or not, so slot numbering is stable. Two flags say what is in one, and they are easy to conflate:
| Flag | Meaning | Type |
|---|---|---|
present: True, info_known: True |
a spool the printer has identified | its filament type, e.g. "PLA" |
present: True, info_known: False |
a spool is loaded but the printer does not know what it is. Typically happens after a RFID-detected spool is replaced with a non-RFID one. | "UNKNOWN" |
present: False |
nothing in the slot | "EMPTY" |
Only the third case cannot take filament settings. present comes from
ams.tray_exist_bits, the signal Bambu Studio itself uses (it ignores the
per-tray state value that printers also report, so that is only consulted
when a printer sends no exist bits). External spools are always present,
Bambu Studio never treats one as empty.
ids is the smallest set of data that identifies a slot, and feeds straight into the
builder, which derives the rest of the command ids from amsID:
builder.build_filament_setting(
tray_info_idx="GFA00", tray_color="FF0000",
ams_id=slot["ids"]["amsID"], tray_id=slot["ids"]["slotID"],
)
An external spool is identified by its amsID alone — 255 for the main slot,
254 for the deputy (ExternalSpool.MAIN / ExternalSpool.DEPUTY) — and always
reports slotID 0, since that is the slot_id the command carries.
When verifying a change, compare against the raw tray_color (8 characters,
including alpha) rather than the truncated Color field.
parse_slots(push_status, model_id, firmware_version, config=None) -> list —
the slot list alone, taking an unwrapped push_status.
check_command_result(response) -> dict
Interprets a printer acknowledgement:
| Key | Meaning |
|---|---|
accepted |
No non-zero err_code and no failure result |
explicit |
The printer actually stated a result; False means acceptance was inferred |
err_code, result, reason |
As reported |
description |
Human-readable meaning of err_code, if known |
scope |
Which payload section the ack was found in |
This reports only that the command was accepted. It is not evidence that the change took effect — re-read the state and compare.
Signing
Printers on firmware newer than (approximately) January 2025 reject unsigned commands by default, unless the printer is placed into LAN-Only and Developer Mode (LAN+DEV mode).
This library does not include or provide ready-to-use certificates or keys for singing. However, if you obtain a key and its associated certificate chain and CRL, this library enables you to generate certificate bootstrap commands as well as valid signatures for MQTT commands you send.
The companion bambu-mqtt-comms library makes it easy to send the appropriate messages to a printer, so these examples will focus on using that library for communication.
However, they could also be "manually" sent using any generic MQTT library like paho-mqtt if you know the internal process.
| Credential | Needed for |
|---|---|
cert_pem |
signing |
key_pem |
signing |
cert_chain_pem |
bootstrap — full chain: leaf + intermediate + root |
crl_pem |
bootstrap — certificate revocation list |
Every MQTTSigner() argument also has a _pem_path variant that reads the contents from a file stored on disk; don't mix the two argument forms.
signer = MQTTSigner(cert_pem=..., key_pem=...,
cert_chain_pem=..., crl_pem=...)
signer.get_cert_id() # "<serial hex>CN=<issuer CN>"
signer.build_app_cert_install() # bootstrap message
signer.sign(payload) # dict -> signed wire string
signer.sign_json('{"print": {...}}') # string -> signed wire string
Signing returns a JSON string, not a dict. The signature covers the exact bytes of the command object, so publish it straight to the appropriate topic — re-serializing it risks invalidating the signature.
Without cert_chain_pem and crl_pem provided to the constructor the signer still signs, but
build_app_cert_install() raises ValueError. That is fine when Bambu Studio
(or whatever Bambu app your key came from) has already bootstrapped the required cert to the printer.
Bootstrap flow
- Connect.
- Publish
app_cert_install(unsigned) with the chain and CRL. - Poll
app_cert_listuntil yourcert_idis listed. The printer never acknowledges step 2, and registration is not instant, so a fixed sleep is unreliable. Registration is lost on power-cycle, and (for some printers) when a different Bambu application connects to it. - Send signed commands.
bambu-mqtt-comms does steps 2 and 3 for you:
client.install_app_cert(signer.build_app_cert_install(), signer.get_cert_id())
Error codes
Rejections related to signing arrive as err_code in the reply. describe_error(code) explains
one; ERROR_CODES is the full table of known codes.
| Code | Meaning |
|---|---|
| 84033543 | No signature block on a command that requires one |
| 84033545 | No certificate registered for this cert_id — bootstrap needed |
| 84033546 | Malformed payload; checked before signature validation, so it can mask other errors |
| 84033547 | Certificate chain/CRL registration incomplete or invalid |
| 84033548 | Signature doesn't verify |
84033543 is the only one of these that reports the printer's configuration
rather than a fault in our message: a printer in LAN + Developer mode never
sends it, and neither does firmware predating command signing (probably). That makes it
the authoritative way to tell whether a printer needs signing at all, so it
gets a dedicated predicate:
from bambu_mqtt_generator import is_signature_required, SIGNATURE_REQUIRED_ERR
result = check_command_result(reply)
if is_signature_required(result["err_code"]):
... # this printer rejects unsigned commands
The check runs before the printer looks at the command name, which is what lets
bambu-mqtt-comms detect the requirement with a throwaway command that
cannot do anything. See its probe_signing_required().
Lower-level helpers
| Function | Purpose |
|---|---|
sign_payload(payload, private_key_pem, cert_id) |
Sign a dict |
sign(payload_json, private_key_pem, cert_id) |
Sign a JSON string |
build_app_cert_install(seq_id, cert_chain_pem, crl_pem) |
Build the bootstrap message |
compute_cert_id(cert_pem) |
Derive a cert_id from a certificate |
load_pem(path) |
Read any PEM file |
extract_leaf_cert(chain_pem) |
Pull the leaf cert out of a chain |
Printers and firmware
get_payload_builder() accepts a model id or a printer name, both
case-insensitive with extra whitespace ignored — see
Supported printers for the full list. The same applies to
parse_response(), parse_slots(), and the ConfigLoader lookups below.
Unknown printers raise ValueError.
Bambu Studio only records a firmware entry where behavior changes, so most versions inherit an earlier one. Passing a version with no entry of its own selects the nearest earlier entry and logs that it did; this is expected, not an error.
from bambu_mqtt_generator import load_config
config = load_config()
config.get_firmware_versions("BL-P001")
# ['00.00.00.00', '01.01.01.00', '01.05.06.01', '01.05.06.05', '01.05.06.06', '01.06.06.00']
config.supports_feature("BL-P001", "01.05.06.06", "support_tunnel_mqtt") # True
config.get_printer("N1") # full model config
config.resolve_model_id("a1 mini") # 'N1'
config.resolve_model_id("not a printer") # None
config.get_filament_preset("GFA00")
# {'filament_id': 'GFA00', 'filament_name': 'Bambu PLA Basic @base',
# 'filament_type': 'PLA', 'tray_type': 'PLA', 'is_support': False,
# 'nozzle_temp_min': 190, 'nozzle_temp_max': 240, ...}
config.get_tray_type("GFS00") # 'PLA-S'
config.get_tray_types() # every known filament id -> tray_type
builder.get_filament_defaults(filament_id) is the same preset lookup, and
builder.get_tray_type(filament_id) the same type lookup.
tray_type is not filament_type
filament_type is the material as the slicer profile declares it;
tray_type is what the printer is told, and the two differ for support
filaments. Bambu Support W is a PLA profile that goes out as PLA-S, Bambu
Support G a PA profile that goes out as PA-S. Bambu Studio applies that
rewrite in DynamicPrintConfig::get_filament_type() before sending, and the
extractor bakes the result into each preset, so a caller holding a filament id
should read tray_type (or call get_tray_type()) rather than splitting the
filament name — the mapping is not mechanical, and Bambu PLA-CF is PLA
while Bambu PA-CF is PA-CF.
Configuration
Generated by bambu-mqtt-extractor. Do not edit configs by hand — fix the extractor and regenerate.
| Path | Contents |
|---|---|
config/printers/ |
15 models, per-firmware feature flags; names.json maps printer names to model ids and records names claimed by more than one model |
config/commands/ |
48 command specs with field types and requiredness |
config/enums/ |
17 enum definitions |
config/filament_presets/ |
103 filament presets |
config/feature_flags.json |
Feature matrix per model/firmware |
config/virtual_ids.json |
Virtual tray ids (255, 254) |
config/ams_response_format.json |
Where AMS slots and the external spool live in a push_status |
AMS response format
ams_response_format.json is deliberately model-independent: Bambu Studio runs
one parser over every printer and detects the shape at runtime, so consumers
should not branch on model id either.
The external spool arrives in one of two shapes, vir_slot taking precedence:
| Shape | Container | Slot id |
|---|---|---|
vir_slot |
array | each entry's own id — 255 main, 254 deputy |
vt_tray |
object | reported id is discarded; always the main slot (255) |
The second row is easy to get wrong: some printers report "id": "254" inside
vt_tray and are still the main slot, 255. parse_response() applies this
rule, which is why its ids can be fed back to the builder unadjusted.
The file's slot_presence section carries the rest of what a slot's contents
are read from: ams.tray_exist_bits (a hex mask), the AMS type in bits 0-3 of
each AMS's info field, and the per-type mapping from (ams_id, slot_id) to a
bit position in that mask — Bambu Studio's DevAms::GetTrayId, which is not
simply the slot id once a printer has more than one AMS or an AMS HT. It also
records how many slots each AMS type has, and the observed meanings of the
per-tray state value, marked in_bambu_studio: false because Bambu Studio
never reads that field.
License and scope
Licensed under the GNU Affero General Public License v3.0 or later (see LICENSE).
This is an independent project, not affiliated with or endorsed by Bambu Lab.
It speaks an undocumented protocol that vendor firmware updates can change
without notice.
Metadata
Release files for bambu-mqtt-generator 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| bambu_mqtt_generator-0.1.0.tar.gz | 79.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| bambu_mqtt_generator-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 230.1 kB
Release files / bambu_mqtt_generator-0.1.0.tar.gz
| Download URL | bambu_mqtt_generator-0.1.0.tar.gz |
|---|---|
| Size | 79.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
72231923bc292ec8637c59da06256b8cdb05a6938aaf0d321eb57d37ce24d3d7
|
|
BLAKE2b-256 checksum How to use checksums |
3a28169415b464385b96baf906f8b1b4e105854daba55f7105a049fed0cd0396
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 11, 2026.
Transparency logRelease files / bambu_mqtt_generator-0.1.0-py3-none-any.whl
| Download URL | bambu_mqtt_generator-0.1.0-py3-none-any.whl |
|---|---|
| Size | 150.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
a3f4d775a9ab8c4d13c413da2019d035120dd8c0d4822290788e2bec066a3360
|
|
BLAKE2b-256 checksum How to use checksums |
5576d6c04bbee88356bf83890bade338eab14c4fef0b17109703d3c7eee3a45e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 11, 2026.
Transparency log