Skip to main content

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 DevFilaColorType enum in DevFilaSystem.h — its names (CTYPE_MULTI = 0, CTYPE_GRADIANT = 1) contradict the wire values above. The source acknowledges this in wgtFilaManagerColorType.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

  1. Connect.
  2. Publish app_cert_install (unsigned) with the chain and CRL.
  3. Poll app_cert_list until your cert_id is 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.
  4. 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)

Source distribution for bambu-mqtt-generator 0.1.0
File Size Uploaded
bambu_mqtt_generator-0.1.0.tar.gz 79.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for bambu-mqtt-generator 0.1.0
File Interpreter ABI Platform
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 log

Release 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

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page