11 KiB
Multi-extruder and multi-AMS device control
DeviceManager/MachineObject model the toolheads, AMS units, and filament slots of a connected
printer and drive the device-control UI from that model. The model represents the counts as
device-reported runtime values rather than compile-time caps, so a printer with an arbitrary number
of toolheads, several AMS units, and a non-uniform number of slots per unit is handled by the same
code paths. The printer agent is responsible for translating its printer's state into the shared
JSON dialect this model consumes; the agent boundary is described in
printer-agent.md, and this document describes the dialect and the model.
Model layout
MachineObject owns the device subsystems that this document covers:
DevExtderSystem— the toolheads (extruders) and their per-toolhead filament/temperature state.DevFilaSystem— the AMS units (DevAms) and their trays (DevAmsTray).DevNozzleSystem— the nozzles and, on rack printers,DevNozzleRack.DevFilaSwitch— a filament switch that can feed several AMS units to more than one extruder.
Each subsystem exposes its own count — DevExtderSystem::GetTotalExtderCount(),
DevFilaSystem::GetAmsCount(), DevAms::GetSlotCount() — and the rest of the application derives
its layout from those values rather than from fixed assumptions.
Wire dialect
The dialect is the Bambu-shaped JSON that DeviceManager and MachineObject already consume. An
agent whose printer speaks something else is expected to translate into it; the agent is not free
to invent a new shape. Two representations coexist for extruders, and one for AMS/slots.
Extruders
The generic representation is print.device.extruder, parsed by
ExtderSystemParser::ParseV2_0 (DevExtruderSystem.cpp). It has a packed state integer and an
info array with one object per toolhead:
device.extruder.state packed integer
bits 0..3 total extruder count
bits 4..7 current extruder id
bits 8..11 target extruder id
bits 12..14 switch state
bits 15..18 currently loading extruder id
bit 19 busy for loading
device.extruder.info[] one entry per toolhead
id extruder id
filam_bak array of backup-group bits
info bit 1 has filament, bit 2 buffer has filament, bit 3 nozzle present
temp bits 0..15 current temperature, bits 16..31 target temperature
spre slot_id bits 0..7, ams_id bits 8..15 (previous)
snow same packing (current)
star same packing (target)
stat bit 0..15 AMS status, bits 16..31 RFID status
hnow current nozzle id
The legacy single/dual representation is the pair of scalar fields nozzle_temper /
nozzle_target_temper, plus the print.ams.tray_tar / tray_now tray pointer, parsed by
ExtderSystemParser::ParseV1_0. That parser is a fallback and only fills the main extruder when the
reported toolhead count is one.
Physical extruder ids are fixed (MAIN_EXTRUDER_ID = 0, DEPUTY_EXTRUDER_ID = 1). Bambu printers
present the two toolheads as a left/right pair and invert the physical-to-logical mapping; generic
printers use the toolhead id itself as the logical id. DevNozzle::GetLogicExtruderId() /
GetExtruderId() centralise that distinction.
AMS units and slots
AMS units arrive as print.ams.ams[] and are parsed by DevFilaSystemParser::ParseV1_0. Each unit
carries an id and an info bitfield:
info bits 0..3 type: 0 dummy, 1 AMS, 2 AMS-Lite, 3 N3F, 4 N3S, 5 AMS-Lite-mixed (N9)
info bits 8..11 bound extruder id (0xE marks a switch-bound unit)
info bits 24..27 switch inlet when the unit is switch-bound
DevAmsType (DevDefs.h) enumerates the resulting unit kinds: EXT_SPOOL, AMS, AMS_LITE,
N3F, N3S, and the AMS_LITE_MIXED N9 variant. AMS_LITE_MIXED reports itself as AMS_LITE to
generic callers; code that needs the distinction uses IsAmsLiteMixed().
A unit whose bound-extruder nibble is 0xE is switch-bound: it is kept only when a DevFilaSwitch
is installed, in which case its extruder id is pinned to the main extruder and its binding set
becomes {0, 1}; otherwise the unit is dropped from the AMS list.
Each unit owns a map of DevAmsTray keyed by the tray id from the payload. The slot count is a
property of the unit kind and the reported trays:
AMS,AMS_LITE, andN3Freport four slots on Bambu printers, matching the firmware contract;N3Sreports one slot;EXT_SPOOLand any non-Bambu unit report the number of trays actually present.
The tray ids are the payload's slot indices; they are expected to be unique, 0-based, and contiguous within a unit, because the generic tray-index mapping below is cumulative.
Bulk/extended spools are carried by print.vir_slot, which DeviceManager parses into
MachineObject::vt_slot. Two ids are fixed: VIRTUAL_TRAY_MAIN_ID = 255 (extruder 0) and
VIRTUAL_TRAY_DEPUTY_ID = 254 (extruder 1); the id convention is vt_id = 255 - extruder_id, so
further extruders use 253, 252, … . The vir_slot parser recognises the 255/254 pair; the agent
filament descriptor path DevFilaSystemParser::ParseAgentFilament is the entry that stores a fuller
descending set in vt_slot.
Mapping
Several subsystems relate AMS units, trays, and extruders, and more than one place derives a tray
index. The Bambu mapping path (DevMappingUtil) computes ams_id * 4 + slot_id and knows only the
Bambu unit kinds; DevFilaSystem provides the device model's own mapping below. The two agree for
Bambu units and diverge for a non-Bambu unit whose slot count is not four.
AMS id to extruder id
DevFilaSystem::GetExtruderIdByAmsId() resolves an AMS id to the extruder it feeds:
- an AMS unit returns its bound extruder (
DevAms::GetExtruderId()); - a virtual tray returns
VIRTUAL_TRAY_MAIN_ID - vt_id; - Bambu's virtual AMS ids map to the main/deputy extruder.
A unit may be bound to more than one extruder when a DevFilaSwitch feeds it; the binding set
(DevAms::m_binded_extruder_set) and switch inlet are read by switch-aware code while the
single-extruder id stays pinned for legacy callers.
Tray index
DevFilaSystem::GetTrayIndexMap() maps a global tray index to an (ams_id, slot_id) pair and is
used for backup-slot translation, calibration addressing, and the mapping popups.
- Bambu AMS:
ams_id * 4 + slot_id; N3S: the AMS id; AMS-Lite-mixed (N9):24 + slot_id. - Generic (non-Bambu): a running lane index — the count of trays in preceding units plus the slot id. This depends on each unit's tray ids being 0-based and contiguous.
Virtual-slot recognition
devPrinterUtil::IsVirtualSlot() decides whether an id denotes a virtual tray. On Bambu printers it
is the fixed pair {255, 254}. On generic printers it is the descending range
[255 - count + 1, 255], where count is the selected printer preset's extruder count. The preset
is expected to match the connected device; when the two disagree the virtual range is wrong, which
is why a mismatch is reported rather than silently accepted.
Device-control UI
The device-control widgets follow the same device-reported counts.
AMSModel(AMSItem.hpp) mirrorsDevAmsTypenumerically:EXT_AMS,GENERIC_AMS,AMS_LITE,N3F_AMS,N3S_AMS. A plainAMSmaps toGENERIC_AMS.- On non-Bambu printers
use_generic_ams_layout()routesGENERIC_AMSunits through a variable-lane rendering that draws one lane per reported tray; Bambu keeps its fixed four-slot rendering. AMSControlpicks its layout by vendor: Bambu keeps the left/right presentation (CreateAmsSingleNozzlefor one toolhead,CreateAmsDoubleNozzlefor two), while non-Bambu printers useCreateAmsMultiNozzle, which builds one preview and one AMS simplebook per toolhead for any count.- The status panel's
ExtruderImagerenders one per-toolhead state per nozzle and is resized toGetTotalExtderCount(). Non-Bambu printers choose the active toolhead throughm_generic_nozzle_selector(shown when more than one toolhead exists); Bambu keeps its left/rightm_nozzle_btn_panel. Nozzle temperature controls are created on demand per toolhead.
Constraints
- Single dialect. The agent translates its protocol into this JSON; the model does not learn vendor protocols. Extending the model with a new unit kind or field means extending the dialect.
- Agent-produced counts. Toolhead, AMS, and slot counts come from the device payload. Nothing caps them at two toolheads or four slots except the Bambu contract described above.
- Extruder dialect invariants.
device.extruder.state's count field must equal the length ofinfo[], andinfo[]must be ordered by extruder id withidequal to the array index: the parser places toolheads by array position whileGetExtderById()indexes by id. - Preset/device agreement. The generic layout and virtual-slot range derive their count from the
selected printer preset. A printer preset must therefore declare the same number of
nozzle_diameterentries as the device has toolheads; a mismatch is surfaced because the sidebar and extruder logic would otherwise disagree. - Bambu compatibility. Bambu-specific presentation (left/right pair, fixed 4-slot units, fixed virtual ids, physical/logical inversion) is preserved behind the vendor check. Generic paths do not alter it.
- Tray-index invariant. The generic tray-index map is cumulative, so payload tray ids must be unique, 0-based, and contiguous per unit. A malformed id is a producer error.
- Protocol markers. The dialect has no neutral equivalent for some Bambu state, so agents that
lack it send empty
print.cfg,print.fun,print.aux, andprint.stat. Their presence selects the new-protocol parse path; they are compatibility markers, not feature switches.
Main implementation locations
DevExtruderSystem— toolhead model and thedevice.extruderparserDevFilaSystem— AMS/tray model, the AMS parser, and the mapping helpersDevDefs.h—DevAmsType, extruder ids, and virtual tray idsDeviceManager.cpp— message dispatch,vir_slotparsing, and device state assemblyAMSControl.cpp— per-toolhead AMS panes (CreateAmsMultiNozzle) and the generic layoutAMSItem.hpp—AMSModeland the variable-lane AMS renderingStatusPanel.cpp—ExtruderImage, the toolhead selector, and per-toolhead temperature controlsMoonrakerPrinterAgent.cpp— producer example (AMS payload and thedevice.extruderencoding)