11 KiB
Built-in printer agents
Owns the per-vendor behavior of the built-in agents: what each subclass changes and what it inherits unchanged. Defers the interface every agent implements to Architecture and Python plugin agents.
This chapter covers the built-in Moonraker family: the general
MoonrakerPrinterAgent and the Qidi and Snapmaker variants. Creality
(CrealityPrintAgent) is also a member of this family and inherits the base
behavior, but has no section here; see the capability matrix for its
per-feature coverage. They share the same connection and status machinery.
Change the base class only when the behavior is valid for all of them.
Each subclass is thin. MoonrakerPrinterAgent holds the HTTP connection,
the WebSocket status subscription, the REST command worker, thumbnail
lookup, the chamber-light heuristic, and the upload-and-start path.
QidiPrinterAgent overrides filament discovery and adds multi-color box
mapping; SnapmakerPrinterAgent overrides filament discovery and camera
setup; CrealityPrintAgent overrides filament refresh. Each derives from
MoonrakerPrinterAgent and is final, which is why the guard rule below
must be type-based.
Moonraker family
Connection and commands
Moonraker-family agents use plain HTTP for the LAN connection. The connection path deliberately ignores a TLS request because the supported printer stacks serve Moonraker or a reverse proxy over HTTP. Restoring the caller's TLS default can send a connection to an unavailable HTTPS endpoint.
Status is a Moonraker WebSocket subscription. Commands use REST. Command translation happens immediately, but the resulting HTTP work runs through one agent-owned FIFO worker. Each queued operation captures the current base URL and API key before it is queued, so a later printer switch does not redirect an earlier command. Keep this separation: network work on the UI path makes controls feel stalled, and allowing a queued command to reread connection state can send it to the wrong printer.
Pause, resume, and cancel use the dedicated Moonraker print endpoints. Do not
replace them with queued PAUSE, RESUME, or CANCEL_PRINT G-code. The
endpoints interrupt the print directly; a G-code command can wait behind the
active print or macro.
The request router accepts the Bambu-shaped JSON used by the native device
tab. Supply object-shaped namespaces such as print and system. A malformed
but parseable payload with a scalar where the router expects an object can
still fail before the unsupported-command fallback. The supported generic fan
status is the standard fan object, which represents the part fan only.
Ordinary part-fan control also works through the legacy gcode_line path,
which sends M106 while is_enable_np is false. Auxiliary and chamber fans
are neither reported nor controlled.
Do not add cfg, fun, aux, and stat to the Moonraker status payload just
to make it look more complete. Together those fields set is_enable_np and
make the UI choose its structured fan and extruder commands instead. The
Moonraker agent does not translate those commands, so working controls become
unsupported no-ops. This is a UI-routing constraint, not a reason to expose
structured fan support.
Status shown by the native device tab
The agent translates Moonraker status into the Bambu-shaped status payload the existing Device tab understands. Some fields are necessarily synthetic:
- The virtual SD-card readiness bit and a basic software-version row make the
native UI consider the printer ready. Each pull payload also ensures
m_push_countandm_full_msg_countare at least one and refresheslast_push_time. Together with the normal-storage state and a placeholder module version, this satisfies the nativeis_info_ready()and printing gates. These are compatibility scaffolding, not reports of physical storage or OTA support. - Current and total layers are emitted only when
print_stats.infocontains numeric values. Moonraker may sendnull, and many profiles do not emit theSET_PRINT_STATS_INFOdata needed to populate them. Do not turn that gap into a JSON conversion exception. - Remaining time is estimated from elapsed print time and virtual-SD progress. It is omitted below two percent progress because the early estimate is too unstable. Do not derive an ETA by subtracting Moonraker duration counters: both are elapsed counters, so their difference is overhead, not remaining time.
- Temperature readings are available, but nozzle diameter and nozzle type are not supplied in the status payload. The UI can therefore show an unknown nozzle. Do not make print submission depend on those missing fields.
Camera thumbnails and lights
For a running job, the agent asks Moonraker for thumbnails and chooses the widest usable entry, rather than assuming the first entry is useful. It accepts both thumbnail path spellings used by Moonraker versions, encodes each path segment, and caches the result by filename. A failed transient lookup is tried again only a bounded number of times; a clean response without a thumbnail is cached as a negative result. The response shape handling is source-derived, not hardware-verified.
Do not perform this HTTP lookup while holding
payload_mutex. The WebSocket thread builds the status payload under that mutex and the UI path also needs it, so a thumbnail timeout taken under the lock would stall status delivery or the UI. The lookup still blocks the WebSocket thread briefly, so move it to a worker if that becomes measurable.
Chamber-light control searches Moonraker objects for names that look like a
light or a standalone LED, then writes the first matching pin, LED, or macro.
The filter exists to avoid treating unrelated objects, such as a beeper, as a
lamp. It remains a heuristic. The incoming led_node is validated, but only
chamber_light is acted on; chamber_light2 is deliberately ignored. A
printer with more than one lamp therefore has no reliable node-to-object map.
Common maintenance limits
The same cache is reused for a selected agent ID, not per physical printer. Qidi and Snapmaker inherit this behavior. A stateful feature added to the base class must be reset carefully when a preset switches hosts.
Keep guards for this family type-based - check whether an agent derives from
MoonrakerPrinterAgentrather than comparing its ID tomoonraker. An ID-based guard silently excludes Qidi, Snapmaker, and Creality, even though they share the base behavior.
The family has no generic implementation for firmware-specific AMS write commands. Keep unsupported commands unsupported until the printer-side macro or API is known. Reporting success for an untranslated command makes the native UI claim that an action happened when it did not.
Qidi
Qidi inherits the Moonraker connection, status, camera, and local-print path. Its differences are Qidi filament discovery and the pre-print multi-color-box mapping.
Filament discovery
Discovery first reads the printer's device information to infer a Qidi series identifier, then falls back to the configured Orca model if needed. Series inference intentionally recognizes only a narrow set of known names. An unknown model still produces usable generic filament data, but not a series-specific preset identifier.
The agent reads a Qidi filament dictionary and the save_variables plus
slot-runout data. Failing to fetch the dictionary is non-fatal: slot discovery
continues with fallback material and colour values. Failing to fetch or parse
slot data is fatal to the refresh. A missing runout value means the agent
cannot prove filament is loaded, so it reports that slot as empty. This is an
ambiguity in the firmware data, not proof that the box is empty.
save_variables.variables must be an object. Qidi firmware can return null
there, and generic JSON value access can throw on a present null. The parser
rejects that shape without throwing. Preserve the null-slot tests whenever the
response parser changes.
Multi-color mapping before a print
Before every Qidi print-start wrapper, the agent writes enable_box and, for
mapped tools, persistent value_t<tool> variables. These writes survive the
job. Invalid mapping JSON is checked only after enable_box has been written.
When the mapping is enabled, that failure can therefore leave enable_box=1.
There is no rollback for this or for a later per-tool write failure, so a
partial mapping can remain on the printer. An empty mapping is accepted when
the box is enabled. Single-colour jobs disable the box but leave old per-tool
assignments in place.
enable_box currently follows task_use_ams. That meaning has not been
verified against all Qidi firmware: if firmware treats it as "a box exists"
rather than "use the box for this job", this gate is wrong and needs hardware
evidence before it changes.
Only start_local_print reaches Moonraker's real upload-and-start path. The
other Qidi mapping wrappers currently return success stubs after applying the
mapping. Do not describe those wrappers as confirmed print paths.
Because the agent cache is keyed by agent type, a Qidi mapping can also become
stale when switching between Qidi printers. This is a generic Moonraker-family
state risk, made more consequential by Qidi's persistent firmware variables.
The configured printer_type can also be stale, so treat it as a fallback
hint rather than device truth.
Snapmaker
Snapmaker uses the Moonraker base and overrides filament discovery and camera setup. Neither path is hardware-verified in the current documentation set.
Filament information comes from parallel arrays in print_task_config.
filament_exist defines the number of slots; shorter type, subtype, colour,
vendor, or NFC arrays use safe fallback values. The agent first tries a visible
vendor, type, and colour preset, then a visible type match, and finally a
generic identifier when no preset bundle is available. An empty reported type
is changed to PLA, so an unknown occupied spool can look like confirmed PLA.
An unrecognized type can also reach the visible-preset fallback and be paired
with an unrelated visible preset. Treat the resulting preset as a suggestion,
not printer-ground truth.
Snapmaker U1 camera support starts the printer's monitor RPC, then serves the
still JPEG through a small local HTML page that reloads it after each load or
error. The wrapper is required because a direct still-image URL looks frozen.
The RPC is sent from a detached thread so the UI timer does not block on socket
I/O. That thread captures this directly, so agent destruction can race with
the camera command. Do not widen this pattern. Route future asynchronous work
through owned lifetime-managed work where possible.
Source locations
src/slic3r/Utils/MoonrakerPrinterAgent.cppsrc/slic3r/Utils/QidiPrinterAgent.cppsrc/slic3r/Utils/SnapmakerPrinterAgent.cpp