From 95279f7084e107951c368d18360d8bd343ea4e33 Mon Sep 17 00:00:00 2001 From: Andrew <159703254+andrewsoonqn@users.noreply.github.com> Date: Fri, 31 Jul 2026 14:07:31 +0800 Subject: [PATCH] docs: document the printer-agent subsystem --- docs/printer-agent/README.md | 72 ++ docs/printer-agent/agents.md | 205 ++++ docs/printer-agent/architecture.md | 158 +++ docs/printer-agent/camera.md | 96 ++ docs/printer-agent/connection-and-status.md | 248 +++++ docs/printer-agent/filament.md | 110 ++ docs/printer-agent/plugin-agents.md | 161 +++ docs/printer-agent/printing.md | 138 +++ .../reference/capability-matrix.md | 137 +++ .../reference/device-tab-annotations.html | 971 ++++++++++++++++++ .../reference/manual-checklist.html | 280 +++++ docs/printer-agent/testing.md | 249 +++++ 12 files changed, 2825 insertions(+) create mode 100644 docs/printer-agent/README.md create mode 100644 docs/printer-agent/agents.md create mode 100644 docs/printer-agent/architecture.md create mode 100644 docs/printer-agent/camera.md create mode 100644 docs/printer-agent/connection-and-status.md create mode 100644 docs/printer-agent/filament.md create mode 100644 docs/printer-agent/plugin-agents.md create mode 100644 docs/printer-agent/printing.md create mode 100644 docs/printer-agent/reference/capability-matrix.md create mode 100644 docs/printer-agent/reference/device-tab-annotations.html create mode 100644 docs/printer-agent/reference/manual-checklist.html create mode 100644 docs/printer-agent/testing.md diff --git a/docs/printer-agent/README.md b/docs/printer-agent/README.md new file mode 100644 index 0000000000..b36cfb2b4e --- /dev/null +++ b/docs/printer-agent/README.md @@ -0,0 +1,72 @@ +# Printer agents + +Printer agents let OrcaSlicer communicate with printers through a +standardized protocol. They translate between a printer's +native API and the application interfaces that the app already +uses. + +This documentation explains the compatibility boundary, runtime ownership, +connection and status flow, command and feature behavior, built-in and plugin +agent implementations, and the testing evidence required for compatibility +claims. + +## What printer agents do + +A printer agent has two jobs: + +1. Accept the app's existing commands and translate the ones its + printer supports. +2. Convert native printer status into correctly-shaped state that + `MachineObject` understands. + +Currently, agents work at a compatibility boundary, i.e., making other vendors compatible with Bambu-shaped code, not a vendor-neutral one. +Some Bambu concepts remain part of the payload and command vocabulary. +End goal is to make the whole command and payload interfaces vendor-neutral. + +## Vocabulary + +Every chapter reuses these terms. The "Is not" column is the part that +causes confusion when it is left implicit. + +| Term | Is | Selected by | Is not | +| --- | --- | --- | --- | +| Agent ID | Which printer agent implementation to use | `printer_agent` on the printer preset; empty is the legacy `bbl`-or-`orca` sentinel | Which printer | +| Printer agent | The live `IPrinterAgent` instance for that ID, created and cached once per ID by `NetworkAgentFactory` | Factory lookup on the agent ID | A connection, and not one object per printer | +| Device ID | One printer inside that implementation | Bind with Access Code for the Moonraker family, where the entered address becomes the ID; Bambu uses its own discovery identity | Which protocol | +| `MachineObject` | The Device tab's view of one selected printer | `DeviceManager::selected_machine`, which stores only an ID | Proof that a printer is reachable | +| Freshness | `is_connected()`, a test over the last-update time | Any reset of the update time, including one no status has followed | Proof that status arrived | +| Status-confirmed readiness | A push-status message has actually been parsed | The first real status message | The same thing as a successful `connect_printer()` | + +Earlier drafts used "transport" for the printer agent instance. That term +is retired: the code selects an implementation, not a wire protocol. + +## How to use this guide + +- [Architecture](architecture.md) describes objects, ownership, lifetimes, + error handling, the feature gate, and compatibility contracts for printer + agents. +- [Connection and status](connection-and-status.md) describes how presets, + machines, access codes, status messages, and commands fit together at + runtime. Unlike Architecture, it follows the sequence of selecting an + agent, connecting, receiving status, and sending commands. +- [Printing](printing.md), [filament synchronization](filament.md), and + [camera support](camera.md) are separate chapters because they contain + per-feature detail rather than because they are universally special: + Printing has its send, preflight, recovery, and start contracts; Filament + covers acquisition, mapping selection, and print-time delivery; Camera + covers the distinct Bambu, Moonraker, and Snapmaker ownership models. +- [Built-in agents](agents.md) describes the Moonraker family and the Qidi, + Snapmaker variants. +- [Python plugin agents](plugin-agents.md) describes the plugin bridge and + lifecycle. +- [Testing and troubleshooting](testing.md) explains automated checks, manual + hardware work, known defects, and the evidence required for compatibility + claims. +- The [capability matrix](reference/capability-matrix.md) is the compact + feature reference. The [manual checklist](reference/manual-checklist.html) + is for a live-printer verification pass. + +Treat source code as authoritative when it differs from this guide. In +particular, preserve the compatibility rules called out in each chapter: +they protect stored presets, existing profiles, and the Device tab's +assumptions. diff --git a/docs/printer-agent/agents.md b/docs/printer-agent/agents.md new file mode 100644 index 0000000000..f1197dcde5 --- /dev/null +++ b/docs/printer-agent/agents.md @@ -0,0 +1,205 @@ +# 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](architecture.md) and +[Python plugin agents](plugin-agents.md).* + +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_count` and `m_full_msg_count` are at least one and refreshes + `last_push_time`. Together with the normal-storage state and a placeholder + module version, this satisfies the native `is_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.info` contains + numeric values. Moonraker may send `null`, and many profiles do not emit the + `SET_PRINT_STATS_INFO` data 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 `MoonrakerPrinterAgent` rather than comparing its ID to +> `moonraker`. 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` 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.cpp` +- `src/slic3r/Utils/QidiPrinterAgent.cpp` +- `src/slic3r/Utils/SnapmakerPrinterAgent.cpp` diff --git a/docs/printer-agent/architecture.md b/docs/printer-agent/architecture.md new file mode 100644 index 0000000000..776806ddbc --- /dev/null +++ b/docs/printer-agent/architecture.md @@ -0,0 +1,158 @@ +# Architecture + +*Owns the structural rules: what the objects are, who owns them, what an +agent must implement, and which behaviors are compatibility contracts. +Defers the runtime sequence - selecting, connecting, receiving status, +sending commands - to [Connection and status](connection-and-status.md).* + +## The compatibility boundary + +The Device tab was built around Bambu-style commands and status. A printer +agent is the translation boundary between that existing contract and a +vendor's native protocol: + +```text +Device tab <-> MachineObject <-> NetworkAgent <-> IPrinterAgent + <-> vendor protocol +``` + +Note: end goal is to move beyond this and achieve a truly vendor-neutral translation layer. + +The GUI builds commands and reads `MachineObject` state. An agent owns the +vendor request, response, connection, and status translation. Keep vendor +details on the agent side of this boundary. + +Status translation is deliberately Bambu-shaped. Agents deliver payloads +through the callbacks used by the existing Bambu path, and +`MachineObject::parse_json()` interprets them. This preserves the Device +tab's established behavior, but it is not a vendor-neutral protocol. + +Important (again): end goal is to move beyond this and achieve a truly vendor-neutral translation layer. + +## Runtime objects and ownership + +`NetworkAgent` is the facade used by the application. It holds one live +`IPrinterAgent` pointer, which is initially null and may return to null +when a selected ID is unavailable. Callers must handle the null case. An +absent agent is an inert state, not permission to fall back to another +printer agent. A fallback would connect to a different implementation than +the one selected by the preset, and could therefore send commands or status +work to the wrong printer. + +`NetworkAgentFactory` registers built-in and plugin implementations by +agent ID. It creates and caches one implementation for each ID. The ID +selects a printer agent implementation, while a `MachineObject` selects one +printer by device ID. The resulting cardinality is one active agent to many +machines. + +For example, suppose two Moonraker printers are on the LAN at +`192.168.1.20` and `192.168.1.21`. In the Device tab machine-select popup, the +user chooses **Bind with Access Code**; `PinCodePanel::on_mouse_left_up` opens +`InputIpAddressDialog`, and each entered address is bound as a separate +printer. Both presets store the same agent ID, `moonraker`, so +`NetworkAgentFactory::create_printer_agent_by_id` returns the same cached +`IPrinterAgent` pointer for both presets. Each printer nevertheless has its +own `MachineObject` and device ID. For the Moonraker family, +`MoonrakerPrinterAgent::bind_detect` calls `init_device_info` with the entered +address as both the device ID and address, so the two device IDs are the two +addresses. + +That is what one active agent to many machines means. Per-printer state must +be keyed by device ID rather than held only on the agent instance, because one +agent object is shared by both printers. State stored only on that object +would be shared between two different machines and could route status or +commands to the wrong one. The same sharing explains why +`GUI_App::switch_printer_agent` compares device IDs even when the agent pointer +is unchanged: otherwise its unchanged-agent early return would skip +reselection when the user switches between these presets, leaving status and +filament work aimed at the previous printer. + +> **Do not make an agent instance per printer just to hold device state.** +> Keep per-printer state keyed by device ID, because one agent object is +> shared by every printer of that type - state held on the instance would +> route status or commands to the wrong `MachineObject`. + +> **Do not fall back to another printer agent when the live one is null.** +> An absent agent is an inert state. A fallback would connect to a +> different implementation than the preset selected. + +## Commands and unsupported work + +An agent must either translate a Device-tab command or return an explicit +error. `ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED` means no translation exists. +`ORCA_NETWORK_ERR_CAP_NOT_AVAILABLE` means a translation exists but this +printer cannot use it. `MachineObject::publish_json()` turns either result +into the user-visible unsupported-command response. + +Every Device-tab command must leave by one of these four exits. The fifth +path is the one to watch for in review: + +```mermaid +flowchart TD + CMD["Device-tab command JSON"] --> PUSH{"pushing namespace?"} + PUSH -- yes --> OK1["Accept - the status stream already satisfies it"] + PUSH -- no --> TRANS{"Translation exists for this agent?"} + TRANS -- no --> E1["Return ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED"] + TRANS -- yes --> CAP{"This printer can use it?"} + CAP -- no --> E2["Return ORCA_NETWORK_ERR_CAP_NOT_AVAILABLE"] + CAP -- yes --> OK2["Translate and send to the printer"] + TRANS -. FORBIDDEN .-> BAD["Return success without translating"] + E1 --> PUB["MachineObject::publish_json turns both errors
into the unsupported-command response"] + E2 --> PUB + BAD --> LIE["UI reports an action that never happened"] +``` + +> **Do not return success for an unhandled command.** That makes an +> unsupported button look as though it worked and hides missing coverage +> from both users and maintainers. + +The `pushing` command namespace is the exception. Its request means +"send status"; an active status stream already satisfies it. The Device +Manager sends these requests repeatedly as a keepalive, so rejecting them +would surface a warning repeatedly even though no action is missing. + +## Feature gate + +`use_printer_agents` enables printer-agent routing. With the gate off, +agent code must have no observable effect. Released profiles can already +contain `printer_agent` values, so activating an agent while the gate is +off would change existing user behavior merely by loading a profile. + +Keep the gate at the routing call sites. Do not fold it into general Bambu +vendor checks: slicing and hardware decisions such as AMS, lidar, bed +types, and G-code flavor still describe printer capabilities, not the +selected printer agent. + +## Backward compatibility + +`printer_agent` remains a `coString`, even when the ID is currently +unregistered. A preset may refer to an optional plugin that is not +installed. The unknown string must load, remain unchanged, and round-trip +without making the preset dirty. The UI may show it as missing, but must +not rewrite it to a fallback ID. + +Keep the feature gate's off-path behavior unchanged, preserve stored agent +IDs, and treat Bambu-shaped payloads as a compatibility contract. + +The reason these three are grouped is that each looks like a local code +change and is not. Switching which printer agent handles a preset edits no +profile and no project file, so it reads in review as contained to the +agent layer. But a user's stored presets and `.3mf` projects already carry +`printer_agent` values and were saved against the Bambu-shaped payload. So +a change that is local in the code is not local in effect: it reaches +every previously saved file. That is why the gate must be inert when off, +an unknown ID must survive untouched, and the payload shape is treated as +a contract rather than an implementation detail. + +## Threading rule + +Agents may perform network work on their own threads, but all mutations of +Device Manager maps and `MachineObject` UI state must run on the UI thread. +Queue incoming status before it reaches `parse_json()` or any operation +that adds, removes, selects, or changes a device. This prevents races +between background network callbacks and UI reads. For example, when a status +callback arrives on an agent's network thread, queue it to the UI thread +before it reaches `MachineObject::parse_json()` or changes a device map or +selection. The Device tab reads those same structures on the UI thread, so +parsing or adding, removing, or selecting a device from the network thread +could race with that read. diff --git a/docs/printer-agent/camera.md b/docs/printer-agent/camera.md new file mode 100644 index 0000000000..7a4e57cd0a --- /dev/null +++ b/docs/printer-agent/camera.md @@ -0,0 +1,96 @@ +# Camera support + +*Owns the three camera ownership models and what each one renders through. +Defers the Snapmaker filament path to [Built-in agents](agents.md), even +though the same subclass owns both.* + +Camera support has three ownership models. They share the Device panel, +but not a common frame or stream interface. + +Two render surfaces, never one: + +```mermaid +flowchart LR + BU["Bambu URL provider
LAN or cloud, not from IPrinterAgent"] + MU["Moonraker webcam discovery
/server/webcams/list stream URL"] + SU["Snapmaker camera page
local HTML that polls monitor.jpg"] + + BV["wxMediaCtrl2
native Bambu media pipeline"] + WV["Device-panel wxWebView
stream URL or local polling page"] + + BU --> BV + MU --> WV + SU -- overrides normal discovery --> WV +``` + +## Bambu + +Bambu playback uses `wxMediaCtrl2` and the native Bambu media pipeline. +The URL comes from the Bambu LAN or cloud path, not from +`IPrinterAgent`. A printer agent should not attempt to force a Bambu URL +through the Moonraker or WebView path. + +## Moonraker live view + +On connection, the Moonraker agent obtains the first enabled webcam URL +from `/server/webcams/list`. An absolute HTTP URL is used directly. A +relative URL is resolved against the printer's host web root, with the +Moonraker API port removed. This is necessary because a relative webcam +path may exist on the printer's web server but not on the API port. + +The connection generation guards the result. A late request must not +replace the URL after the user has selected a different printer. Failed +discovery clears the URL, which prevents a prior camera from remaining +visible on a printer with no camera. + +The agent places the discovered URL in its status payload. The Device +panel renders it in `wxWebView`. It reloads only when the URL changes and +resets the camera-start timestamp at that point. Reloading every update +would loop indefinitely for endpoints that redirect, so an unchanged URL +is shown again without calling `LoadURL()`. + +The trade-off is intentional: a WebView that loses an unchanged stream +does not automatically reload. Camera controls beyond live viewing remain +out of scope for Moonraker. Recording, timelapse, settings, and virtual +camera are Bambu-oriented features and must not be presented as supported +merely because live view works. + +## Snapmaker polling view + +Snapmaker overrides normal webcam discovery. It writes a per-printer local +HTML page that polls the printer's `monitor.jpg` with a cache-busting URL. +Each next request starts after the prior image loads or fails, preventing +requests from piling up on a slow printer. A raw snapshot URL is not used, +because it would display one frozen frame instead of a live-looking view. + +The printer must be asked to start its camera capture task. While the +camera view is visible, the Device panel requests this at first display +and then attempts another request every 300 seconds. Other agents reject +the command quietly, so the common timer does not create an error for +Bambu or ordinary Moonraker. + +The Snapmaker command is sent from a detached thread because the request +can block on socket I/O and the printer responds over a different channel. +This avoids blocking the UI but leaves a raw-`this` lifetime risk: the +agent can be destroyed while the detached operation still refers to it. +Do not extend this path without addressing that ownership boundary. + +The wrapper is written below the application cache with a name derived from +the printer IP. The source contains no cleanup path for those files, so they +can accumulate as different printer IPs are used. This is source-derived and +was not reproduced during this rewrite. + +Source code proves 300-second renewal attempts only. The long-running +behavior of the shipped polling and renewal cycle has not yet been tested. +Do not claim that the attempt renews an active capture task or that it +prevents camera expiry until hardware verification establishes both. + +## Maintenance checklist + +- Keep the three ownership models separate. +- Preserve host-root resolution for relative Moonraker URLs. +- Keep generation guards and stale-URL clearing on every discovery path. +- Reload WebView content only after a URL change. +- Reset the camera-start timestamp when the camera URL changes. +- Treat Moonraker as live-view-only and Snapmaker lifecycle behavior as + not yet verified beyond the observed renewal attempts. diff --git a/docs/printer-agent/connection-and-status.md b/docs/printer-agent/connection-and-status.md new file mode 100644 index 0000000000..125cc1e02f --- /dev/null +++ b/docs/printer-agent/connection-and-status.md @@ -0,0 +1,248 @@ +# Connection and status + +*Owns the runtime sequence in order: selecting an agent and machine, +starting a connection, receiving status, sending commands. Defers the +structural rules those steps must obey - ownership, no-fallback, unknown +IDs, threading - to [Architecture](architecture.md), and cites them at the +point where they bite.* + +## The four runtime concepts + +Keep these concepts separate when tracing a connection problem: + +- A preset stores an agent ID and printer address. +- `NetworkAgent` holds the active printer agent for that agent ID. +- `MachineObject` represents the selected printer at that address. +- Freshness and status-confirmed readiness are separate states. + +An agent ID selects a printer agent implementation. A device ID selects one +printer within that implementation. + +A non-Bambu printer reaches the machine list through **Bind with Access +Code**, the tile in the Device tab's machine-select popup. The user enters +an address and an access code, `bind_detect()` probes the address before +any connect, and `DeviceManager::insert_local_device()` creates the +`MachineObject`. For the Moonraker family the address itself becomes the +device ID: `MoonrakerPrinterAgent::bind_detect()` seeds `dev_name` and +`dev_id` from the entered address, so an unreachable or unnamed printer +still shows up as its IP rather than blank. + +Binding is the only route for that family. `MoonrakerPrinterAgent::start_discovery()` +deliberately announces nothing, because a partial discovery implementation +would populate the machine list with stale hosts. Bambu is the exception: +it has its own discovery identity and does not use the address as an ID. + +`DeviceManager::selected_machine` is only a selected ID. It can name no +resolvable object. `get_selected_machine()` answers whether an object is +actually available. `set_selected_machine()` accepting an ID therefore +does not prove the printer is connected. The selected ID can remain when its +object is unavailable, so connection state must come from the object itself. + +## Selecting the agent and machine + +`GUI_App::switch_printer_agent()` reads the edited printer preset and +resolves its stored agent ID through `NetworkAgentFactory`. + +1. An empty stored ID is a legacy sentinel. It resolves to `bbl` for a + Bambu vendor preset and to `orca` otherwise. +2. If that effective ID is registered, the factory provides the matching + printer agent implementation. +3. Clear the live printer agent only when a nonempty stored ID is unregistered + or the factory cannot construct the matching registered agent. +4. When the active printer agent changes, clear the current selection, user + selection, stale device discoveries, sidebar state, and AMS state before + installing the replacement. +5. Select the preset's address-derived machine for non-Bambu agents. + +The lifetimes are easier to see than to read. Note that the agent pointer +can be unchanged while the machine still must be re-selected - that is the +trap in the same-agent path below: + +```mermaid +sequenceDiagram + participant U as User + participant P as Printer preset + participant G as GUI_App::switch_printer_agent + participant F as NetworkAgentFactory + participant N as NetworkAgent + participant M as MachineObject + + U->>P: Edit or switch the printer preset + P->>G: printer_agent value, possibly empty + G->>G: resolve_printer_agent_id - empty becomes bbl or orca + G->>F: get_printer_agent_info(effective ID) + + alt ID not registered, and stored value was nonempty + F-->>G: no info + G->>N: clear the live printer agent + Note over N: null is inert - never fall back to another agent + else ID registered + F->>F: create once per ID, then serve from cache + F-->>G: the cached IPrinterAgent for this ID + alt Agent pointer changed + G->>N: clear selection, user selection, discoveries, sidebar, AMS + G->>N: install the replacement agent + else Same cached pointer returned + Note over G,F: Two presets can share one agent ID + end + G->>M: compare device ID, re-select if the address differs + end +``` + +> **Do not use the first available machine as a fallback** (rule owned by +> [Architecture](architecture.md), Runtime objects and ownership). It +> connects to a printer the user did not choose, including one owned by a +> different printer agent. + +The same-agent path is important too. Two presets can use one agent type +but point at different addresses, and the factory caches one agent per ID, +so switching between them returns the same pointer and would otherwise +skip reselection entirely. Re-select the machine whenever the preset's +address changes, even when the factory returned the same active agent. +Otherwise status and filament work can continue against the previous +printer. + +Note: this is a legacy coupling, not the primary workflow. It reads an +address stored on the printer preset itself (`print_host` and +`printhost_port`, named here only so the keys can be found in the code) +and derives a device ID from it with `dev_id_from_address()`. Those keys +predate printer agents and are edited through `PhysicalPrinterDialog`, +which despite its name writes the printer preset rather than a +`PhysicalPrinter` object - that object is no longer constructed. Printers +normally arrive through Bind with Access Code instead, which does not +touch the preset. Both routes end at `insert_local_device()`, so they must +agree on the device ID: `dev_id_from_address()` strips the URL scheme and +drops an empty port, while the bind path stores the address as the user +typed it. + +The unknown-`coString` compatibility rule belongs to `architecture.md` under +Backward compatibility. Keep a nonempty unknown `printer_agent` ID unchanged +and display a missing state if needed; do not rewrite it during plugin unload +or choose an arbitrary replacement, so the preset can round-trip while its +plugin is temporarily unavailable. + +## Starting a connection + +Machine selection causes `MachineObject::connect()` to invoke the active +agent's `connect_printer()` with the selected address and effective access +code. A success return means that the connection attempt started. It does +not mean that the printer is ready or that a status stream is alive. + +Moonraker-family agents must force HTTP. Moonraker and print-host +installations commonly serve plain HTTP, while the generic machine path +can request TLS by default. Passing that default through turns a valid +connection into an HTTPS request the printer will refuse. The agent therefore +must keep the connection on HTTP unless its protocol support changes +deliberately and is verified. + +## Access codes: four coordinated slots + +One effective access code can live in four places: + +| Slot | Location | Purpose | +| --- | --- | --- | +| Device runtime | `MachineObject::access_code` | Code learned from the device. | +| User runtime | `MachineObject::user_access_code` | Code entered by the user. | +| Device config | `access_code[dev_id]` | Persisted device value. | +| User config | `user_access_code[dev_id]` | Persisted user value. | + +The effective code prefers the user value when present, then the device +value. Keep user input in the user path and device replies in the device +path. Crossing those paths obscures which value should win. + +`set_access_code()` deliberately does not save configuration immediately. +Device replies and polls can update it often; forcing a full config write +for each message adds unnecessary work. The normal deferred config save +persists dirty state later. Do not add an eager save just to make this one +path symmetric: device replies and polls update it often, so a config +write per message is wasted work. + +> **Do not erase the user access code when a printer connects.** On the +> LAN reselection path that code can be the only credential that lets the +> machine pass the access check and receive the status or access-code +> reply that would refresh it, so erasing it at connection time can leave +> the machine permanently unable to receive updates. A failed connection +> is the place to handle a proven bad credential. + +## Receiving status + +An agent receives native status, translates it to the existing payload +shape, and dispatches it to the matching `MachineObject`. The object +parses the payload and records when it last received an update. + +Readiness is four states, and three of them look connected: + +```mermaid +stateDiagram-v2 + [*] --> SelectedIdOnly + SelectedIdOnly: Selected ID only + SelectedIdOnly: selected_machine names no resolvable object + SelectedIdOnly --> FreshWindow: connect_printer returns success + + FreshWindow: Fresh window + FreshWindow: reset seeded the update time - no status yet + FreshWindow --> Connecting: still fresh, still no push-status + + Connecting: Connecting + Connecting: is_connecting true - the honest state + Connecting --> Ready: first push-status message parsed + + Ready: Status-confirmed ready + Ready: the only state that proves a usable printer + Ready --> Stale: update time ages out + + Stale: Stale + Stale: is_connected false + Stale --> Ready: a later status message arrives + + note right of FreshWindow + is_connected() is true from here on. + It is a freshness test over the update + time - not proof that any status arrived. + end note +``` + +`is_connected()` is a freshness test over the update time. It does not +describe whether `connect_printer()` returned success or whether any status +message arrived: reset initializes the update time, creating an initial +freshness window. `is_connecting()` distinguishes that window from +status-confirmed readiness: while the object is fresh and no push-status +message has arrived, it remains connecting. + +> **Do not treat freshness or a successful connect as proof of readiness.** +> Code that needs a usable printer must wait for status-confirmed +> readiness, because the fresh window exists before any status has been +> parsed. + +The UI-thread mutation rule belongs to `architecture.md` under Threading rule. +Dispatch the status callback to the UI thread before changing device maps, +selection, or `MachineObject` state, because network callbacks may run in a +worker thread and mutating these structures there races with the Device tab. + +## Sending commands + +`MachineObject` builds the established command JSON and sends it through +the active `NetworkAgent`. The agent translates it or returns an explicit +unsupported result. It must not report success when no translation exists. + +The `pushing` command exception belongs to `architecture.md` under Commands +and unsupported work. It asks for status, and a working status stream already +supplies it, so accepting it avoids false unsupported warnings from the Device +Manager's repeated keepalive. + +## Maintainer constraints + +- Preserve same-agent reselection by address, because an agent type can + serve more than one printer. +- Preserve the null-agent, no-fallback, and unknown-`coString` rules in + `architecture.md`; selection must remain an explicit user or preset choice, + and stale state must not belong to a replacement printer agent. +- An empty value is the legacy Bambu-or-Orca sentinel, not a missing printer + agent. +- Preserve deferred access-code saves and the no-on-connect-erase rule; + they prevent excessive config writes and credential-driven status loss. +- Keep Moonraker connections HTTP-only unless the agent's protocol support + changes deliberately and is verified. +- Do not treat freshness as proof that status arrived; wait for + status-confirmed readiness. The UI-thread mutation rule is in + `architecture.md` under Threading rule. diff --git a/docs/printer-agent/filament.md b/docs/printer-agent/filament.md new file mode 100644 index 0000000000..5fbfe78905 --- /dev/null +++ b/docs/printer-agent/filament.md @@ -0,0 +1,110 @@ +# Filament synchronization and mapping + +*Owns the three filament stages end to end: acquiring printer state, +selecting a mapping, and delivering it at print time. Defers the upload +and start mechanics to [Printing](printing.md), and per-vendor discovery +detail to [Built-in agents](agents.md).* + +Filament support has three separate stages. A successful first stage does +not mean that a selected mapping will be delivered to the printer. + +Stage 3 is where the two paths diverge, and only one of them reaches the +printer: + +```mermaid +flowchart TD + SRC["Filament source
Moonraker lane_data, or the classic MMU object"] + ACQ["1 - Acquire state
Moonraker pull, not subscription"] + ST["DevFilaSystem
Bambu-shaped view, synthetic 4-slot groups"] + SEL["2 - Select mapping
Send dialog matches project filament to slots"] + PAR["PrintParams
ams_mapping fields"] + QIDI["Qidi agent writes enable_box
and value_t per tool, then starts the print"] + BASE["Base Moonraker, Happy Hare, AFC
upload and start ignore the mapping fields"] + OK["Mapping reaches the printer"] + DEAD["Mapping never reaches the printer
the job runs on the printer's own loaded-lane behavior"] + + SRC --> ACQ --> ST --> SEL --> PAR + PAR -- Qidi --> QIDI --> OK + PAR -- every other target --> BASE --> DEAD +``` + +## 1. Acquire printer state + +`FilamentSyncMode` declares how the UI obtains filament state: + +| Mode | Meaning | +| --- | --- | +| `subscription` | A status stream keeps the state current. | +| `pull` | The UI must request state before it can use it. | +| `none` | The agent has no usable filament state. | + +Moonraker uses `pull`. Its ordinary status stream does not supply the +filament data used by this UI. In particular, `lane_data` is a Moonraker +database namespace, not a printer object that the existing subscription +can follow. Changing Moonraker to `subscription` would suppress the pull +that actually populates the UI. + +The agent first reads `lane_data`, which can describe AFC and newer Happy +Hare installations. If that is unavailable, it reads the classic Happy +Hare `mmu` object. Those response shapes are source-supported but not yet +verified against current Happy Hare and AFC installations. + +The current parser expects lane values as strings and silently skips +numeric values. Whether current AFC or Happy Hare installations emit +numeric lane values is unverified. + +The received lanes are converted into a Bambu-shaped model so existing +AMS UI can render them. The model groups numeric lane indexes into +synthetic groups of four slots and passes the result through +`ParseV1_0`. This is a UI compatibility adapter, not evidence that the +printer has a Bambu AMS. + +Pull state can be stale. The Send dialog can build a mapping from the +current `DevFilaSystem` without refreshing it first, and a failed pull can +leave older state visible. Do not represent a displayed lane list as a +fresh printer read unless the call site just performed the pull. + +## 2. Select a mapping + +The Send dialog matches each project filament to a compatible reported +slot. It rejects a mismatched material type and then prefers compatible +slots according to the existing mapping rules. The result is carried in +the legacy linear mapping, the explicit AMS-and-slot mapping, and mapping +metadata for the job. + +Treat lane numbers as printer contracts. A numeric lane index is used as a +slot index in the synthetic four-slot view, so an incorrect numbering +assumption can select the wrong physical lane. + +The material identity code also retains a defect: ABS and ASA can be +shown as PLA when profile identifiers collide. This is not fixed here, +and multi-color mapping has not received hardware verification. + +## 3. Deliver the mapping at print time + +`PrintJob` copies the selected mappings into `PrintParams`, but base +Moonraker does not read those fields when it uploads and starts a print. +For plain Moonraker, Happy Hare, and AFC targets using that base path, a +correct-looking mapping in the UI is therefore not delivered to the +printer. The print runs using the printer's own loaded-lane behavior. + +Qidi is the implemented exception. Its agent writes its own box mapping +before starting the print. That is a Qidi-specific delivery contract, not +a generic Moonraker solution. + +There are deliberately no guessed Happy Hare or AFC write macros. Their +macro and variable names are defined by printer-side configuration, so a +guessed command could silently do nothing or control the wrong setup. Add +a delivery path only after verifying the exact contract against upstream +documentation or a real printer. + +## Maintenance checklist + +- Keep Moonraker in `pull` mode while `lane_data` remains pull-only. +- Refresh or clearly surface stale state before relying on Send-dialog + mappings. +- Do not claim base Moonraker honors mappings until it consumes them at + print time. +- Preserve Qidi as a distinct delivery implementation. +- Verify lane numbering, material identity, and multi-color behavior on + hardware before expanding the mapping contract. diff --git a/docs/printer-agent/plugin-agents.md b/docs/printer-agent/plugin-agents.md new file mode 100644 index 0000000000..68f03a00dd --- /dev/null +++ b/docs/printer-agent/plugin-agents.md @@ -0,0 +1,161 @@ +# Python printer-agent plugins + +*Owns the plugin bridge: the implementation contract, registration and +lifetime, and the audit scope. Defers what the agent must do once live to +[Architecture](architecture.md) and +[Connection and status](connection-and-status.md).* + +Python printer agents use the current capability bridge. They do not use a +separate adapter or a Moonraker-specific plugin path. + +## What an agent must implement + +Not overriding a member of `IPrinterAgent` has four different +consequences depending on which tier it is in. This is the whole plugin +contract: + +| Tier | Members | Consequence of not overriding | +| --- | --- | --- | +| Pure virtual | `connect_printer`, `disconnect_printer`, `send_message_to_printer`, the `start_*` print operations, `start_discovery`, `bind`, `bind_detect`, `unbind`, the callback setters, `set_cloud_agent`, `get_agent_info`, and the rest of the pure surface | Compile error | +| Concrete, succeeds | `start_subscribe`, `stop_subscribe`, `add_subscribe`, `del_subscribe` | Silently returns `BAMBU_NETWORK_SUCCESS` | +| Concrete, declines | `command_ams_refresh_rfid`, `command_ams_calibrate`, `command_ams_select_tray`, `command_start_camera` | Silently returns `ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED` | +| Concrete, inert | `get_filament_sync_mode`, `fetch_filament_info` | Reports `FilamentSyncMode::none` and `false` - no filament capability at all | + +The refusal tier is deliberate: those commands carry Bambu-dialect G-code +in their bodies, so the honest default is a refusal that +`MachineObject::publish_json()` turns into a dialog. The success tier is +equally deliberate - a printer whose status already streams needs no +subscription call. + +> **Do not assume a missing override quietly inherits useful behavior, and +> do not assume it fails loudly either.** Only the first tier fails at +> compile time. The second silently reports success, the third silently +> declines, and the fourth silently reports no filament capability. + +## The plugin contract + +A plugin subclasses `printer_agent.PrinterAgentBase`, the Python binding for +`PrinterAgentPluginCapability`. The capability itself is the live native +`IPrinterAgent`; there is no intermediate protocol adapter, because +`PrinterAgentPluginCapability` inherits both `PluginCapabilityInterface` +and `IPrinterAgent` directly. + +`get_type()` stays a `PluginCapabilityInterface` method and +`set_cloud_agent()` remains the native host injection point. A plugin must +implement the pure connection, communication, discovery, binding, print, +callback-registration, and filament-refresh operations. The certificate, +bind-ticket, HMS-snapshot, and user-selected-machine members are pure too; +the table above abridges the list. + +The only tracked Python printer-agent implementation is the BBL plugin. There +is no Python Moonraker printer agent in the current source tree. Moonraker is +implemented by the built-in C++ class. + +## Registration and lifetime + +When an enabled plugin advertises a printer-connection capability, the factory +gets its `AgentInfo` and registers a factory under `AgentInfo.id`. This is the +same registry used for built-in agents. + +Two similarly named structs are involved, and they are not the same thing. +`AgentInfo` is what the agent says about itself; `PrinterAgentInfo` is the +registry's entry about it: + +```mermaid +classDiagram + class AgentInfo { + <> + string id + string name + string version + string description + } + class PrinterAgentInfo { + <> + string id + string display_name + string plugin_identifier + PrinterAgentFactory factory + } + class PrinterAgentFactory { + <> + takes cloud_agent and log_dir + returns shared_ptr~IPrinterAgent~ + } + class NetworkAgentFactory { + <> + register_printer_agent(id, display_name, factory) + create_printer_agent_by_id(id, cloud_agent, log_dir) + clear_printer_agent_cache() + register_python_printer_agent(plugin_key, capability_name) + deregister_python_printer_agent(plugin_key, capability_name) + } + + AgentInfo ..> PrinterAgentInfo : id becomes the registry key + PrinterAgentInfo *-- PrinterAgentFactory + NetworkAgentFactory o-- PrinterAgentInfo : one entry per ID + PrinterAgentFactory ..> PrinterAgentPluginCapability : weak reference +``` + +`plugin_identifier` is empty for built-ins and +`;;` for plugins - that is how the +registry tells the two apart at deregistration time. Built-in IDs are the +constants `ORCA_PRINTER_AGENT_ID` and `BBL_PRINTER_AGENT_ID`. + +Agent IDs are global. A plugin cannot replace a built-in agent or another +plugin with the same ID. Registry rejection is unconditional. The conflicting +capability is disabled and the user is shown the conflict only when `wxTheApp` +exists and the app is not closing. Re-registering the same plugin capability +is allowed so a reload can replace its factory with the current capability +instance. + +The registered factory holds a weak reference to the capability. If the plugin +has already gone away, creation returns null instead of reviving a destroyed +Python object. Callers must treat that as no active printer agent. + +On deregistration, the factory removes the registry entry and cached agent, +disconnects a cached agent, and clears the live agent if it has the same ID. +This order prevents `NetworkAgent` from retaining a Python implementation +whose module is about to unload. The current path is UI-thread oriented. Raw +pointer hazards become relevant only if deregistration moves to another thread +without adding synchronization around the GUI-held active-agent handle. + +## Device-tab integration + +Plugins share the native Device tab with built-in agents. There is no +printer-agent API for adding custom Device-tab panels and no plugin-owned +`MachineObject` to populate directly. + +Instead, the plugin supplies the same callbacks as any `IPrinterAgent`. Its +status messages must use the Bambu-shaped payload that `MachineObject` already +parses. If a required field is absent, the shared native UI shows its default +or incomplete state. A custom protocol is acceptable inside the plugin, but +its boundary with the app must perform this translation. + +## Python calls, errors, and audit scope + +The C++ trampoline acquires the Python GIL, invokes each pure virtual override, +logs a Python exception, and rethrows it. A missing override is a separate +C++ pure-virtual failure, not a logged Python traceback. Python construction +also bypasses the virtual trampoline, so the bridge logs a constructor failure +at the construction boundary. + +Plugin-created threads need their own exception handling. An exception raised +there does not cross the C++ trampoline; it reaches Python's thread exception +handling and is recorded through redirected Python standard error. + +The audit hook is defense in depth, not a sandbox. Current printer-agent +trampoline calls use loading audit mode. In that mode, normal reads are +allowed, only some file writes are checked against allowed roots, and many +operations are outside the policy, including network access and process +creation. Work that runs outside an active trampoline scope, including a +plugin-created thread, has no attributed plugin context and is allowed by +default. Do not treat this mechanism as permission to run untrusted code. + +## Source locations + +- `src/slic3r/plugin/pluginTypes/printerAgent/PrinterAgentPluginCapability.hpp` +- `src/slic3r/plugin/pluginTypes/printerAgent/` + `PrinterAgentPluginCapabilityTrampoline.hpp` +- `src/slic3r/Utils/NetworkAgentFactory.cpp` +- `resources/orca_plugins/BBLPrinterAgentPlugin.py` diff --git a/docs/printer-agent/printing.md b/docs/printer-agent/printing.md new file mode 100644 index 0000000000..f8efe9e5be --- /dev/null +++ b/docs/printer-agent/printing.md @@ -0,0 +1,138 @@ +# Printing through printer agents + +*Owns the send path: connection choices, preflight, upload, start, and the +two recovery flows. Defers filament mapping delivery to +[Filament synchronization](filament.md), which is a separate contract even +though it is applied at print time.* + +This chapter describes the printer-agent send path. It is separate from +the older print-host implementation, even when both target Moonraker. +Keep the paths separate unless their contracts and failure handling can +be deliberately reconciled. + +## Connection choices + +Three connection paths are in use: + +| Target | Connection path | Use | +| --- | --- | --- | +| Native Bambu | Custom TLS tunnel on port 6000 | Send and optional eMMC preflight | +| Bambu Python agent | Implicit FTPS on port 990 | Upload and Bambu preflight fallback | +| Moonraker family | HTTP | Upload and start print | + +The Bambu connection paths are independent. Selecting one does not prove that +the other is available. The Moonraker agent uploads with a multipart +request to its `gcodes` storage and then starts the uploaded filename; +it does not reuse the legacy `Moonraker` print-host class. + +## Bambu native tunnel + +The native tunnel depends on the versioned networking DLL and its +file-transfer module. `InitFTModule()` is a single-owner initialization: +it rejects a second call. Any future shared initialization must therefore +be idempotent, while `BBLNetworkPlugin` remains the single teardown owner. +It must call `UnloadFTModule()` before freeing the DLL, otherwise the +module's function pointers can point into unloaded code. + +There is currently an initialization gap: selecting a printer agent does +not initialize this module. It is initialized only when the +`installed_networking` option causes the native BBL network plugin to +initialize. Calls to the tunnel must continue to fail safely until that +path has initialized the module. The Send UI catches this failure and +reports an initialization error instead of letting an exception leave a +wx event handler. + +## Bambu FTPS upload + +The Python Bambu agent uses implicit FTPS on port 990. Its live upload +path closes the data connection, then waits at most two seconds for the +control response with `voidresp()`. A `TimeoutError` is accepted as a +completed transfer. An `error_reply` is also accepted when its reply +begins with `200`. This is the behavior to preserve. + +Do not describe the path as using TLS `unwrap()`: the live construction +does not enable it. Enabling it without a bounded wait could hang while +waiting for the peer's TLS close notification. The current timeout-based +handling has not been verified on hardware against every printer and FTP +server combination. + +## Print preflight and recovery + +For normal LAN prints, `PrintJob` performs a preflight before the real +send. When eMMC is eligible it tries the native tunnel, then it sends a +small `verify_job` upload through the selected agent. The latter is a real +upload, not a special protocol command. Non-Bambu agents therefore upload +the probe too. + +> **Do not re-enable eMMC by default** without hardware coverage for the +> affected devices. It is opt-in because the tunnel can hang during upload +> on some printers. + +The whole send, including the thread hop and the recovery fork: + +```mermaid +sequenceDiagram + participant UI as Send UI (UI thread) + participant J as PrintJob (worker) + participant A as Selected IPrinterAgent + participant P as Printer + + UI->>J: Start send + opt eMMC eligible - off by default + J->>P: Native Bambu tunnel attempt + Note over J,P: Can hang on some printers + end + J->>A: verify_job preflight + Note over J,A: A real upload, not a protocol probe -
non-Bambu agents upload it too + A->>P: Upload probe + + alt Preflight and upload succeed + J->>A: Upload the real job + A->>P: Upload, then request print start + Note over A,P: The start response may stay open while
the printer prepares - a timeout is not
proof of failure, so check reported print state + A-->>UI: Result from the reported print state + else Upload fails + J-->>UI: Failure callback, marshalled to the UI thread + Note over UI: Re-resolve the machine here.
Never reuse a machine pointer captured
on the worker - agent or machine may have changed + alt Printer still connected + UI->>UI: Explain that storage upload failed + else Printer disconnected + UI->>UI: Open the IP or access-code flow + end + end +``` + +An upload failure and a disconnected printer need different recovery: + +| Condition | UI response | +| --- | --- | +| Printer is still connected | Explain that storage upload failed. | +| Printer is disconnected | Open the IP or access-code flow. | + +> **Do not retain a machine pointer from a worker callback.** The callback +> that chooses between these two outcomes runs on the UI thread and +> re-resolves the machine there, because the selected agent or machine can +> change first. The connection check is adequate for choosing the message, +> but is not a strong enough signal to authorize a reconnect. + +## Moonraker upload and start + +`MoonrakerPrinterAgent` uploads through Moonraker HTTP, then requests the +print start separately. The start endpoint may keep its response open +while the printer prepares the job. A timeout after that request is not +automatically proof that the start failed: the agent checks the reported +print state before deciding the result. + +The legacy print-host Moonraker path implements its own upload and start +logic. It is not the agent path and should not be changed as an implicit +side effect of agent work. + +## Maintenance checklist + +- Test the selected connection path, not just another path on the same + printer. +- Preserve cancellation and progress callbacks across upload and start. +- Treat `verify_job` as an actual upload when estimating storage effects. +- Keep eMMC opt-in until its hanging behavior is resolved and verified. +- Keep the connected-upload-failure dialog distinct from the disconnected + recovery flow. diff --git a/docs/printer-agent/reference/capability-matrix.md b/docs/printer-agent/reference/capability-matrix.md new file mode 100644 index 0000000000..296730dd98 --- /dev/null +++ b/docs/printer-agent/reference/capability-matrix.md @@ -0,0 +1,137 @@ +# Printer-agent capability matrix + +This is a compact lookup for the built-in Moonraker family. It combines +implementation state with recorded evidence. It is not a promise that every +firmware configuration behaves the same way. Python plug-in behavior depends +on the plug-in, not on this matrix. + +Use [testing and troubleshooting](../testing.md) before calling a live-printer +result complete. + +For a quicker tour of the controls users actually see, open the +[annotated Device-tab view](device-tab-annotations.html). The annotations +explain the important routing constraints; this matrix remains the compact +cross-agent reference. + +## Status definitions + +- Supported - implemented, with a relevant live-printer result recorded. +- Partial - an important condition, limitation, or defect applies. +- Unsupported - no applicable implementation, or deliberate refusal. +- Not verified - implemented or source-inspected, but without a relevant live + result. + +`Base` means `MoonrakerPrinterAgent`. Qidi, Creality, and Snapmaker inherit +from it unless a row identifies an override. + +## Connection and status + +| Capability | Base | Qidi | Creality | Snapmaker | +| --- | --- | --- | --- | --- | +| Direct LAN connection with API key | Supported | Supported | Not verified | Not verified | +| WebSocket status updates | Supported | Supported | Not verified | Not verified | +| Reconnect and fresh status | Partial | Partial | Partial | Partial | +| Discovery and cloud binding | Unsupported | Unsupported | Unsupported | Unsupported | +| Device identity with a configured port | Partial | Partial | Partial | Partial | + +- Reconnect completion can leave the Device tab with stale status. +- A bare IP and `host:port` can become separate device identities. +- The Base and Qidi Supported grades come from prior hardware sessions. They + were carried into this rewrite and not rerun. + +## Controls + +| Capability | Base | Qidi | Creality | Snapmaker | +| --- | --- | --- | --- | --- | +| Home and arbitrary G-code | Not verified | Not verified | Not verified | Not verified | +| Bed and nozzle temperature | Not verified | Not verified | Not verified | Not verified | +| Pause, resume, and cancel | Not verified | Not verified | Not verified | Not verified | +| Configured chamber light | Partial | Partial | Partial | Partial | +| Jog and manual extrusion | Partial | Partial | Not verified | Not verified | +| Legacy part-fan speed control | Partial | Partial | Not verified | Not verified | +| Structured fan, chamber, and AI controls | Unsupported | Unsupported | Unsupported | Unsupported | +| AMS RFID, calibration, and tray control | Unsupported | Unsupported | Unsupported | Unsupported | + +- Chamber light needs a recognised light object. +- Base and Qidi jog works, but can leave relative positioning active. Do not + use it as a general safe-control test until its G-code state is restored. +- Base and Qidi part-fan control works through legacy `gcode_line` while + `is_enable_np` is false. Adding `cfg`, `fun`, `aux`, and `stat` flips that + flag and routes fan and extruder controls to unsupported structured commands. +- Creality and Snapmaker inherit the source path but have no separate live + evidence for jog or fan control. + +## Printing + +| Capability | Base | Qidi | Creality | Snapmaker | +| --- | --- | --- | --- | --- | +| Upload G-code without starting | Not verified | Not verified | Not verified | Not verified | +| Upload and start a local print | Not verified | Not verified | Not verified | Not verified | +| Mapped multi-material print | Unsupported | Not verified | Unsupported | Unsupported | +| Cloud or SD-card start variants | Unsupported | Partial | Unsupported | Unsupported | +| Cancel during upload | Not verified | Not verified | Not verified | Not verified | +| Send with no nozzle identity | Not applicable | Not verified | Not applicable | Not applicable | + +- Qidi applies mapping before it routes the real local print path. +- Some Qidi print variants can reach base success stubs after mapping. +- Qidi tolerates missing nozzle data in source, but that Send preflight is not + hardware-verified. + +## Filament + +| Capability | Base | Qidi | Creality | Snapmaker | +| --- | --- | --- | --- | --- | +| Sync mode | Not verified | Not verified | Not verified | Not verified | +| Read installed material and slots | Partial | Not verified | Not verified | Not verified | +| Slot, material, and colour refresh | Unsupported | Not verified | Not verified | Not verified | +| Cleanup after removed material | Not verified | Not verified | Not verified | Not verified | +| Load, unload, or write a slot | Unsupported | Partial | Unsupported | Unsupported | +| Auto Refill | Unsupported | Unsupported | Unsupported | Unsupported | + +- All built-in agents use pull-mode sync. +- Base reads Happy Hare or AFC data when present. Qidi has print-time mapping; + Creality has CFS logic; Snapmaker reads printer arrays and NFC data. +- The Base Device-tab slot refresh uses a proprietary AMS command and has no + generic Moonraker translation. +- Generic write-side macros stay unsupported until their printer contract is + known and verified. + +## Camera + +| Capability | Base | Qidi | Creality | Snapmaker | +| --- | --- | --- | --- | --- | +| Discover a Moonraker webcam | Not verified | Not verified | Not verified | Unsupported | +| Provide a camera source | Not verified | Not verified | Not verified | Not verified | +| Live camera view | Not verified | Not verified | Not verified | Not verified | +| Snapshot-only camera start | Unsupported | Unsupported | Unsupported | Not verified | +| Camera start renewal and teardown | Unsupported | Unsupported | Unsupported | Partial | +| Print thumbnail | Not verified | Not verified | Not verified | Not verified | + +- Snapmaker bypasses webcam discovery with a local snapshot-polling page. +- Its renewal and teardown path has lifetime risks without live evidence. +- Moonraker thumbnail endpoint responses and filename-cache behavior need live + coverage. + +## Python plug-ins + +| Capability | Python plug-in agent | +| --- | --- | +| Registration and re-registration | Not verified - lifecycle tests cover replacement | +| Duplicate agent ID | Not verified - conflict is rejected and reported | +| Disable or unload | Not verified - deregistration is tested; session teardown needs coverage | +| Capability surface | Defined by the plug-in and exposed Python API | + +## Reading the matrix safely + +- `Partial` is not a softer form of `Supported`. It names a condition that must + be checked before use. `Not verified` means the code was found, but no + relevant live result is recorded. +- Pair each claim with the evidence grades in the testing guide. This matters + especially for CFS, Snapmaker camera, MMU macros, Qidi Send preflight, and + thumbnail endpoints. + +## Background - deliberate exclusions + +The matrix excludes Bambu-specific cloud binding, RFID, calibration, and +camera-control features from the Moonraker family. They use different protocol +contracts and are deliberately refused when no safe Klipper equivalent exists. diff --git a/docs/printer-agent/reference/device-tab-annotations.html b/docs/printer-agent/reference/device-tab-annotations.html new file mode 100644 index 0000000000..e023d378b6 --- /dev/null +++ b/docs/printer-agent/reference/device-tab-annotations.html @@ -0,0 +1,971 @@ + + + + + +Moonraker Device-tab annotations + + + + + +
+ +
+ Moonraker printer agents · annotated reference +

Selected Device-tab and Plater controls on Moonraker

+

The markers connect visible controls to their implementation and limitations. This page is a + visual companion to the capability matrix, not a substitute for + live-printer verification.

+
+ +
+

Read the verdicts as operating conditions

+

Supported has relevant live-printer evidence. Partial has an important + limitation. Unsupported has no safe built-in implementation. Not verified + describes a source-inspected path without equivalent live evidence. A visible control can still look available + when its underlying command is unsupported.

+
+ +
+

Why a control can appear to succeed

+

Much of the Device tab was designed around Bambu protocol commands. Moonraker accepts generic G-code and a + smaller set of print actions, but it has no general equivalent for proprietary AMS, camera, or structured + control commands. Treat a closed dialog or a changed button state as UI feedback, not proof that the printer + received a safe command.

+
+ +
+

Keep protocol boundaries explicit

+

Do not make proprietary Bambu commands appear available by widening capability checks. In particular, the + legacy Moonraker paths for the part fan and extrusion rely on is_enable_np staying + false. Adding cfg, fun, aux, or stat changes + routing to unsupported structured commands. The visible result can be a silent loss of a previously working + control.

+
+ +
+ Supported or Local only + ! Partial + Unsupported + ? Not verified + Hover or focus a marker to highlight its note, and vice versa. The eye at each + screenshot's top right hides the markers so you can see the bare UI. +
+ + +
+
+ Panel 01 · Device tab +

AMS / filament

+

Moonraker can pull installed material and slots from a compatible MMU source. It does not provide + a generic, verified contract for changing slots, loading material, or delivering a selected mapping to a + print.

+
+
+
+
+ OrcaSlicer Device tab AMS panel with four spools, an external spool slot, and Auto Refill, Unload and Load buttons + + + + + + + + +
+
+
    +
  1. + 1 + Spool data (A1-A4, Ext) Partial +

    The built-in agent uses pull-mode sync. It can populate the view from Happy Hare or AFC data, + but refresh depends on the Plater sidebar sync action. Base Moonraker does not deliver a selected material + mapping when a multi-material print starts.

    +

    See get_filament_sync_mode() and fetch_filament_info() in + MoonrakerPrinterAgent.

    +
  2. +
  3. + 2 + Slot refresh arrow (A1-A4) Unsupported +

    The control relies on a Bambu-specific AMS opcode. Generic Moonraker has no verified + replacement. Do not widen the structured AMS path merely to expose it: that also changes filament-state + handling and can turn an unavailable action into a blocking error.

    +
  4. +
  5. + 3 + Slot pencil, filament setting Unsupported +

    The dialog is available, but generic Moonraker has no built-in write-side material or colour + contract. Printer-specific macros must be known and verified before adding one.

    +
  6. +
  7. + 4 + External spool pencil Unsupported +

    This is the external-spool variant of the same unsupported write-side material path.

    +
  8. +
  9. + 5 + Auto Refill Unsupported +

    The dialog can accurately report that the feature is unavailable, but generic Moonraker has + no built-in auto-refill contract.

    +
  10. +
  11. + 6 + AMS settings icon Unsupported +

    The settings use Bambu-specific print and AMS options. The built-in Moonraker family does not + map them to generic printer controls.

    +
  12. +
  13. + 7 + Unload Unsupported +

    A generic unload macro is unsafe to assume. The required printer-side contract is not part of + base Moonraker.

    +
  14. +
  15. + 8 + Load Unsupported +

    This uses the same unsupported generic write-side material path. Qidi has agent-specific + behavior, but that does not make it a Base Moonraker capability.

    +
  16. +
+
+
+ + +
+
+ Panel 02 · Device tab +

Extruder feed and retract

+

Manual extrusion uses a legacy raw-G-code path while the structured capability flag remains + disabled. It is a useful compatibility path, not a general proof that manual motion is safe.

+
+
+
+
+ Extruder control widget with a retract button above and a feed button below the extruder graphic + + +
+
+
    +
  1. + 1 + Retract (up) Partial +

    The legacy path sends raw G-code through Moonraker and can move the extruder. Klipper still + applies its own extrusion-temperature and safety limits.

    +
  2. +
  3. + 2 + Feed (down) Partial +

    The same legacy path handles feed. It remains conditional on is_enable_np being + false. Adding cfg, fun, aux, or stat changes + routing to unsupported structured extrusion commands.

    +
  4. +
+
+
+ + +
+
+ Panel 03 · Device tab +

Fan and chamber light

+

The legacy part-fan route is available only while structured capabilities stay disabled. A chamber + light also depends on a recognised light object in the printer configuration.

+
+
+
+
+ Fan button above a 100 percent fan speed readout and a Lamp toggle + + + +
+
+
    +
  1. + 1 + Fan button, speed popup Partial +

    The Base and Qidi legacy path sends M106 through gcode_line, so part-fan + speed control works while is_enable_np is false. It is not a structured fan API: + adding capability fields routes the control to unsupported structured commands.

    +
  2. +
  3. + 2 + Fan speed readout (100%) Not verified +

    The value is sourced from Moonraker fan status, with fan.power as a fallback. + The displayed value still needs equivalent live-printer evidence across supported configurations.

    +
  4. +
  5. + 3 + Lamp Partial +

    A configured chamber light can be controlled when the agent recognises a suitable light object. + It is not a generic guarantee for every output_pin or LED configuration.

    +
  6. +
+
+
+ + +
+
+ Panel 04 · Device tab +

Camera

+

Moonraker live view can work when webcam discovery yields a usable stream. Recording, timelapse, + resolution, and Bambu-specific camera controls do not have a generic Moonraker implementation.

+
+
+
+
+ Camera panel showing a black video area, a Local Mqtt signal readout, a Lan toggle, three icons and a green play button + + + + + + + +
+
+
    +
  1. + 1 + Live view Not verified +

    The path is source-inspected and configuration-dependent: webcam discovery must provide a usable + stream with a URL the local media pipeline accepts. Equivalent live-printer evidence is not recorded.

    +
  2. +
  3. + 2 + Play Unsupported +

    The Bambu local-camera transport is not a Moonraker webcam control.

    +
  4. +
  5. + 3 + "Local Mqtt -92/-91" Unsupported +

    This diagnostic readout is not sourced from Moonraker device status.

    +
  6. +
  7. + 4 + Lan toggle Unsupported +

    This tunnel subscription control has no Moonraker override.

    +
  8. +
  9. + 5 + Recording Unsupported +

    Moonraker webcam discovery does not define a recording-control contract.

    +
  10. +
  11. + 6 + Timelapse Unsupported +

    Timelapse control is printer-specific and is not provided by the Base Moonraker agent.

    +
  12. +
  13. + 7 + Camera settings, resolution Unsupported +

    The Base Moonraker agent does not expose a generic stream-resolution setting.

    +
  14. +
+
+
+ + +
+
+ Panel 05 · Device tab +

Printing progress

+

The print display is built from Moonraker status and file metadata. The paths shown here are + source-inspected unless the capability matrix records relevant live-printer evidence.

+
+
+
+
+ Printing progress panel with a placeholder thumbnail, N/A task name and state, a progress bar, Layer N/A, a 13:00 time and pause and stop buttons + + + + + + + + +
+
+
    +
  1. + 1 + Task thumbnail Not verified +

    The agent requests a thumbnail for the uploaded G-code and exposes its URL through + slice_info->thumbnail_url. The request must stay off the WebSocket update path and be cached: + a blocking lookup while that path holds shared state can delay status updates and the UI.

    +

    /server/files/thumbnails → + slice_info->thumbnail_urlStatusPanel thumbnail handling. + The response shape is unverified - if the image 404s, check the path prefix first.

    +
  2. +
  3. + 2 + Task name Not verified +

    Real value: subtask_name is filled from print_stats.filename. It reads + N/A here only because the printer is idle; it populates once a print starts.

    +

    StatusPanel reads subtask_name.

    +
  4. +
  5. + 3 + Print state Not verified +

    gcode_state is mapped from print_stats.state, with a parallel + mc_print_stage for printing / paused / complete / error.

    +

    map_moonraker_state() in build_print_payload_locked()

    +
  6. +
  7. + 4 + Progress bar Not verified +

    mc_percent comes from virtual_sdcard.progress, clamped to 0-100.

    +

    StatusPanel reads task_progress.

    +
  8. +
  9. + 5 + Layer Not verified +

    Layer data comes from print_stats.info.current_layer and + .total_layer. It is conditional on G-code that calls SET_PRINT_STATS_INFO; where + a printer profile omits those calls, the field stays blank.

    +

    StatusPanel reads curr_layer and + total_layers from the status payload.

    +
  10. +
  11. + 6 + Remaining time Not verified +

    Remaining time is estimated from virtual_sdcard.progress. It is not a + slicer-accurate ETA, and pause or heating behavior can make the estimate differ from the printer's actual + completion time.

    +

    mc_remaining_time is derived in + build_print_payload_locked().

    +
  12. +
  13. + 7 + Pause Not verified +

    POST /printer/print/pause hits Klipper's + pause_resume webhook as a direct interrupt, instead of queuing a raw PAUSE behind + the G-code queue where it could stall behind a long move or a macro.

    +

    StatusPanelDeviceManager → + post_print_action("pause") in MoonrakerPrinterAgent.

    +
  14. +
  15. + 8 + Stop Not verified +

    POST /printer/print/cancel is used. Note that stop is also the + discriminator emitted by HMS fault-stop, so the agent cannot distinguish a user cancel from a fault + abort.

    +

    StatusPanelDeviceManager → + post_print_action("cancel").

    +
  16. +
+
+
+ + +
+
+ Panel 06 · Plater sidebar +

Filament list

+

Most controls in this sidebar edit the local project. The AMS sync button is the relevant + Moonraker integration point and uses pull mode.

+
+
+
+
+ Plater sidebar filament section with a Flushing volumes button, four icon buttons and seven filament rows + + + + + + +
+
+
    +
  1. + 1 + Flushing volumes Local only +

    Opens the flushing-volume dialog. Never touches the printer agent, so the agent cannot break + it.

    +

    Plateropen_flushing_dialog().

    +
  2. +
  3. + 2 + Remove last filament Local only +

    Local project edit only.

    +

    Platerdelete_filament().

    +
  4. +
  5. + 3 + Add one filament Local only +

    Local project edit only.

    +

    Plateradd_filament().

    +
  6. +
  7. + 4 + Sync filament list from AMS Partial +

    The built-in Moonraker family uses pull-mode sync to read installed material and slots. It can + populate the local list when Happy Hare or AFC data is available, but generic Moonraker does not deliver a + selected mapping to a print. Qidi mapping is agent-specific.

    +

    Platersync_ams_list() → + load_ams_list()build_filament_ams_list() → + MoonrakerPrinterAgent::fetch_filament_info(). The button is visible when the sync mode is not + FilamentSyncMode::none.

    +
  8. +
  9. + 5 + Set filaments to use Local only +

    Opens ConfigWizard. Local only.

    +

    Plater opens ConfigWizard.

    +
  10. +
  11. + 6 + Filament rows and "..." menus Local only +

    Pure preset selection - colour swatch, preset combo, and the Edit / Merge / Delete menu. No agent + call on any of them.

    +

    Plater::init_filament_combo().

    +
  12. +
+
+
+ +
+

How to use this page. Each marker connects a visible control to a current capability verdict. + Check the capability matrix for the agent-family scope and evidence grade, + then use the testing guide before relying on a configuration-dependent result.

+

Safety boundary. Manual extrusion and legacy part-fan control depend on + is_enable_np remaining false. Adding cfg, fun, + aux, or stat redirects these controls to unsupported structured commands. Jog is + Partial because it can leave relative positioning active; do not use it as a general safe-control test until + the G-code state is restored.

+

Deliberate exclusions. Bambu-specific AMS write operations, RFID, calibration, and camera + controls remain Unsupported for the Base Moonraker agent because no generic, verified Klipper contract exists. + Printer-specific support belongs in the relevant agent, not in a guessed Base command.

+
+ +
+ + + + diff --git a/docs/printer-agent/reference/manual-checklist.html b/docs/printer-agent/reference/manual-checklist.html new file mode 100644 index 0000000000..f6fcd95731 --- /dev/null +++ b/docs/printer-agent/reference/manual-checklist.html @@ -0,0 +1,280 @@ + + + + + + Printer-agent manual checklist + + + +

Printer-agent manual checklist

+

Record what the printer did, not just what the UI displayed. Mark a case + blocked when the required printer, configuration, or safe test condition is unavailable.

+ +
+ + + + +
+ +
+ 0 of 0 cases marked +
+ + +
+ + + +

Use Pass only after observing the expected result. Fail needs enough evidence + to reproduce it. Include response text, log markers, or firmware behavior in the note.

+ +
+

Connect and observe status

+
+

Connect to the selected printer

+

Expected: the Device tab receives a fresh status update after connection. Do not use a + successful connection return alone as the result.

+
+

Observe live status changes

+

Expected: temperature and target changes, fan state, print state, filename, progress, + elapsed time, and homing state reach the UI while the printer changes state.

+
+

Disconnect and reconnect

+

Expected: a second connection produces new status messages and does not create a duplicate + device. Record the post-reconnect status evidence.

+
+

Handle network and response failures

+

Expected: for discovery, status, G-code, upload, and print start, exercise controlled + HTTP 401, 404, and 500 responses, invalid JSON, and refused sockets. Each operation must + fail clearly or offer a retry, without a crash or a false success. Record the operation, + injected failure, UI result, and any retry.

+
+ +
+

Controls and printing

+
+

Exercise safe controls

+

Expected: home, bed and nozzle temperature, and a harmless G-code command take effect on + the printer. Do not use Moonraker jog as a safe control test while it can leave relative + positioning active.

+
+

Verify queued command order under latency

+

Expected: queue three harmless, uniquely marked commands while a proxy, network shaper, + or request log introduces or records latency. The printer-side log must show the markers in + the same order they were queued. Record the latency method and observed order.

+
+

Send a file without starting it

+

Expected: the file appears on the printer and no print starts.

+
+

Start a small print

+

Expected: upload completes, the printer starts the selected file, and status transitions + to printing.

+
+

Pause, resume, and cancel an active print

+

Expected: after the small print is actively printing, pause it and observe a paused state + on both printer and UI; resume it and observe printing again; then cancel it and observe the + printer stop and the UI leave the active or paused state.

+
+

Check upload failure handling

+

Expected: cancellation during upload and a missing input fail clearly and do not begin a + partial or unintended print.

+
+ +
+

Filament, camera, and agent-specific checks

+
+

Refresh material-system data

+

Expected: populated slots, empty slots, material, colour, and a live change are represented + correctly. Moonraker-family agents pull this data; do not expect a subscription callback.

+
+

Clear material data when filament or the system is absent

+

Expected: remove filament or disable the material system, refresh, and confirm the UI no + longer shows obsolete slots, material names, or colours.

+
+

Verify print-time mapping where supported

+

Expected: only agents with a documented mapping implementation change printer-side mapping. + Do not attempt load, unload, or slot-setting macros unless their printer-specific contract is + known and safe.

+
+

Check Qidi Send preflight with missing nozzle identity

+

Expected: on a Qidi agent and compatible single-nozzle slice, Send proceeds when the + Device tab has no reported nozzle diameter or type. It must not stop with + PrintStatusNozzleDataInvalid. Record any reported identity and any mismatch + result separately; this does not approve a known mismatch.

+
+

Verify a camera feed

+

Expected: frames advance and switching printers does not display a stale feed. For Snapmaker, + observe immediately before and after 300 seconds in one open view. The renewal result is + unknown until hardware evidence exists. Then swap agents and shut down the app to exercise + teardown around the detached callback's raw-this lifetime risk.

+
+

Verify the print thumbnail

+

Expected: test a reused filename after its thumbnail changes, response payloads with both + thumbnail_path and relative_path, and a path below the G-code + root. The displayed image must match the current file in each case.

+
+

Reload a Python printer-agent plug-in

+

Expected: the capability registers once, duplicate agent IDs are rejected visibly, and + disable or unload removes the agent cleanly.

+
+ + + + diff --git a/docs/printer-agent/testing.md b/docs/printer-agent/testing.md new file mode 100644 index 0000000000..057f92811b --- /dev/null +++ b/docs/printer-agent/testing.md @@ -0,0 +1,249 @@ +# Testing and troubleshooting printer agents + +*Owns evidence grades, the automated and manual verification passes, and +the open-defect register. Every "not hardware-verified" note elsewhere in +this guide resolves to a grade defined here.* + +This page describes how to assess a printer-agent change without treating +source inspection as a hardware result. Use the +[manual checklist](reference/manual-checklist.html) for a repeatable live-printer +pass, and use the [capability matrix](reference/capability-matrix.md) to decide +which cases apply to the agent being changed. + +## Evidence grades + +Keep these grades separate in reviews and release notes. + +- Source-inspected - the current implementation was read. It establishes + intended behavior, not printer compatibility. +- Automated - a targeted test ran. It covers its inputs and assertions, not a + printer, firmware version, or network failure that it does not model. +- Hardware-verified - the stated behavior was observed on a named class of + live printer. Record the model, firmware, configuration, and result with the + test evidence. + +Do not call a capability supported by hardware solely because the code compiles or +a unit test passes. Conversely, a hardware observation should not be generalized +to every Moonraker-family printer without checking its configuration. + +## Carried hardware evidence + +Prior hardware sessions verified direct Moonraker-family connection, live +WebSocket status, and jog on both a Qidi/Moonraker printer and a generic +Moonraker box. The jog moved the printer but could leave relative positioning +active. This evidence establishes the Moonraker-base and Qidi grades in the +capability matrix. It was carried into this rewrite and was not rerun here. + +It does not establish those behaviors for Creality or Snapmaker, and it does +not cover Qidi-specific filament discovery, box mapping, or print wrappers. + +## Build and automated tests + +Run the smallest relevant test target first, then broaden the run if the change +crosses shared agent, plug-in, or Device-tab code. Set +`` to the CMake build tree that was already configured for +the compiler, generator, and build type you intend to use. Do not replace it +with the source directory or assume a `build` subdirectory exists. + +```powershell +cmake --build --config RelWithDebInfo --target slic3rutils_tests +cmake --build --config RelWithDebInfo --target printer_agent_plugin_tests +ctest --test-dir /tests/libslic3r --output-on-failure +``` + +`--config RelWithDebInfo` is needed for multi-config generators such as Visual +Studio. Omit it only when the configured generator is single-config and its +build type was selected at configure time. Parallel-build options belong to the +generator: for example, pass `--parallel 6` to CMake when the generator +supports it, or use the generator's own trailing arguments only when that +generator documents them. Do not combine a changed working directory, a +generator-specific flag, and an assumed build-tree layout in one command. + +On Windows, start from an MSVC developer environment. A shell without the MSVC +include paths can fail in dependencies before it compiles Orca code, with errors +such as `C1083: Cannot open include file: 'stddef.h'`, `'time.h'`, or `'cstdint'`. +Those signatures are environment failures, not evidence against the agent change. + +If a machine exhausts MSVC precompiled-header memory, use the documented lower +parallelism command: + +```powershell +cmake --build --config RelWithDebInfo --target slic3rutils_tests --parallel 6 +``` + +Errors such as `C3859: Failed to create virtual memory for PCH` and `C1076: +internal heap limit reached` are machine-specific resource failures. If a build +appears hung and file operations are blocked, inspect for idle `cl.exe` processes +holding locks before changing source. + +Relevant automated coverage includes: + +- `tests/slic3rutils/test_qidi_printer_agent.cpp` validates malformed and null + Qidi slot responses without throwing. +- `tests/slic3rutils/test_printer_agent.cpp` checks the public printer-agent + surface, including filament-sync mode exposure. +- `tests/slic3rutils/test_printer_agent_plugin.cpp` exercises plug-in + registration, replacement, conflict handling, and deregistration. + +Do not present a historic test count, failure count, or skipped-test count as the +current state. Run the command above and attach its own output when a current +result is needed. + +## Manual hardware verification + +Use a small, disposable model and a printer that can safely accept the actions. +The checklist groups the work in the order below. + +1. Confirm the printer accepts its configured URL and API key, then select it in + the Device tab. Verify a fresh status update, not merely a successful connect + return code. +2. Observe temperatures, targets, fan state, print state, filename, progress, + elapsed time, and axis homing while the printer changes state. +3. Exercise safe idle controls first: home, bed and nozzle temperature, and a + harmless G-code command. Verify the printer's action as well as the UI + response. +4. Send a small file without starting it, then start a small print. Once the + print is active, pause it, confirm the printer and UI both enter a paused + state, resume it, and confirm both return to printing. Cancel only after + observing an active or paused print, then confirm that the printer stops and + the UI leaves that state. Cancel an upload and retry a missing input so that + failure handling is observed too. +5. For Moonraker command workers, send three harmless, uniquely marked commands + while a proxy, network shaper, or request log introduces or records latency. + Pass only if the printer-side log records the markers in the same order they + were queued. Record the latency method and the observed order. +6. For a material system, verify populated slots, empty slots, material, colour, + refresh after a change, and cleanup when the system is absent or filament is + removed. The latter must remove obsolete slot or material data from the UI. + Do not infer write support from read support. +7. For Qidi, use a compatible single-nozzle slice and Send it while the Device + tab has no reported nozzle diameter or type. Pass only if Send proceeds past + preflight without `PrintStatusNozzleDataInvalid`; record any reported + diameter/type and any mismatch message separately. This checks the intended + tolerance for missing identity data, not that a mismatched known nozzle is + safe. +8. Verify the camera only on hardware that advertises or implements it. Check + that frames advance, switching printers starts the newly selected camera, and + closing or changing the view does not leave misleading stale output. For + Snapmaker, also test immediately before and after 300 seconds in an + uninterrupted view. The expected renewal result is unknown until hardware + evidence exists. Swap agents and shut down the app after the camera cases to + exercise teardown around the detached callback's raw-`this` lifetime risk. +9. For Moonraker thumbnails, test a reused filename after its thumbnail changes, + responses that use `thumbnail_path` and `relative_path`, and a thumbnail in a + subdirectory below the G-code root. Record the endpoint payload and displayed + image for each case. +10. Disconnect and reconnect the printer, then confirm that new status messages + still reach the UI. A reconnect completion alone is insufficient evidence. +11. Test network and response failures for discovery, status, G-code, upload, + and print start. For each operation, exercise HTTP 401, 404, and 500, + invalid JSON, and a refused socket with a controlled proxy or test server. + Each case must fail clearly or offer a retry, without a crash or a false + success. Record the operation, injected failure, UI result, and any retry. + +For Moonraker, record whether the thumbnail endpoint returns the response shape +the agent expects. That response has not yet been verified across a live +Moonraker deployment. + +## Troubleshooting by symptom + +### Connection appears successful but the Device tab stays stale + +Treat status freshness as the connection result. Enable `ORCA_NETWORK_DEBUG` and +look for a new `parse_json: dev_id=` entry after the connection or reconnection. +The unresolved reconnect-delivery problem can complete the second connection +without delivering any new parsed messages. Capture an instrumented second +connection before changing dispatch or message-delay logic, because both remain +plausible causes. + +Check identity too. One path can use a bare IP address while another uses +`host:port`; configuring a port can therefore create two machine objects. Do not +diagnose a duplicate as a printer-agent failure until the identities are +compared. + +### A control reports success but the printer did not change + +First establish that the command has a documented translation in the capability +matrix. Unsupported commands are deliberately rejected rather than silently +accepted. For Moonraker, queued controls are asynchronous, so wait for the +printer-side result and capture the request or log before concluding it was lost. + +Moonraker jog is a special case. The current path can leave the printer in +relative positioning mode after a jog. Do not use it as a general verification +control until it is changed to save state, issue `G91` and the move, then restore +state with `SAVE_GCODE_STATE` and `RESTORE_GCODE_STATE`. Extruder-relative moves +use a separate `M83` path. + +### A thumbnail is missing or belongs to an earlier print + +The thumbnail lookup accepts both `thumbnail_path` and `relative_path`, but the +live endpoint response is not yet verified. The cache is keyed by filename, so +reusing a common name can retain the previous image. Test a distinct filename +before changing the lookup. Paths below the G-code root also need live coverage +for the `relative_path` fallback. + +### Filament looks stale, blank, or does not follow an edit + +Moonraker-family agents use pull-mode filament sync. Verify the pull request and +the resulting Device-tab update rather than expecting a subscription callback. +Read-side discovery does not establish load, unload, slot-setting, or Auto Refill +support. Happy Hare and AFC macro names are printer-side configuration; do not +guess them. A guessed macro can silently do nothing or issue the wrong action. + +### A Python agent disappears or cannot be enabled + +Check its agent ID first. A duplicate ID is rejected and the conflicting +capability is disabled rather than auto-promoted later, because automatic +promotion could change the active printer implementation without user intent. +Reload and teardown also need a live check: registration tests cover lifecycle +logic, but a plug-in can still be exposed to API drift or a teardown race in a +real session. + +## Known defects and safeguards + +- Reconnect delivery remains unresolved. Instrument the second connection before + attempting a fix; the observed failure is stale data after a completed reconnect. +- Moonraker jog can leave relative mode active. Keep the future state-save and + restore sequence together so the jog cannot affect later G-code positioning. +- Qidi's nozzle-data Send-preflight tolerance for unreported diameter and type + has not received a hardware verification. +- A configured `host:port` can coexist with a bare-IP machine identity. This can + duplicate devices and confuse selection. +- Moonraker thumbnail caching can show an old image when a filename is reused. +- Moonraker filament data can be stale, and its pull/read path does not provide + safe generic write-side MMU operations. +- Duplicate plug-in agent IDs are rejected. There is no automatic fallback to a + losing capability after the winner unloads. +- Plug-in implementations can drift from the Python printer-agent API. Treat an + import or interface error as a plug-in compatibility issue until proved otherwise. +- Snapmaker camera callbacks can outlive their view during agent replacement or + shutdown because the detached path retains a raw `this` pointer. Treat a crash + or stale callback during those transitions as a source-derived use-after-free + risk until the lifetime is made explicit. +- Snapmaker writes an IP-specific local camera HTML file below the application + cache. The source contains no cleanup path, so residual files can accumulate + for each unique printer IP. This is source-derived and was not reproduced. + +## Not yet hardware-verified + +- Moonraker command-worker FIFO behavior under recorded or injected network + latency. +- Moonraker thumbnail responses: reused filenames, `thumbnail_path`, + `relative_path`, and subdirectory paths. +- Snapmaker camera behavior on a live U1: frames, renewal across a long-open + view including the 300-second boundary, switching between printers, agent + replacement, and shutdown teardown. +- Whether Snapmaker's renewal cadence prevents a stale-frame interval. Do not + shorten it as a workaround without resolving the printer-side behavior first. +- Creality CFS detection and preset scoring on a real printer. +- Qidi Send preflight when firmware omits nozzle diameter and type. +- Moonraker-family write-side MMU commands. They remain blocked on verified, + printer-specific macro contracts. + +## Background - source locations for maintainers + +The Moonraker command worker, status stream, print path, and thumbnail +lookup live in `MoonrakerPrinterAgent`. Qidi maps its material box before routing +to the Moonraker base. Snapmaker adds the camera start request and its snapshot +page. Python agent registration and conflict handling live in +`NetworkAgentFactory`.