mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-08-04 14:58:03 +03:00
Compare commits
4 Commits
feat/print
...
feat/print
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8dfc7a14b9 | ||
|
|
1534268183 | ||
|
|
501af81ba9 | ||
|
|
16c44940d2 |
@@ -1,72 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,205 +0,0 @@
|
||||
# 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<tool>` variables. These writes survive the
|
||||
job. Invalid mapping JSON is checked only after `enable_box` has been written.
|
||||
When the mapping is enabled, that failure can therefore leave `enable_box=1`.
|
||||
There is no rollback for this or for a later per-tool write failure, so a
|
||||
partial mapping can remain on the printer. An empty mapping is accepted when
|
||||
the box is enabled. Single-colour jobs disable the box but leave old per-tool
|
||||
assignments in place.
|
||||
|
||||
`enable_box` currently follows `task_use_ams`. That meaning has not been
|
||||
verified against all Qidi firmware: if firmware treats it as "a box exists"
|
||||
rather than "use the box for this job", this gate is wrong and needs hardware
|
||||
evidence before it changes.
|
||||
|
||||
Only `start_local_print` reaches Moonraker's real upload-and-start path. The
|
||||
other Qidi mapping wrappers currently return success stubs after applying the
|
||||
mapping. Do not describe those wrappers as confirmed print paths.
|
||||
|
||||
Because the agent cache is keyed by agent type, a Qidi mapping can also become
|
||||
stale when switching between Qidi printers. This is a generic Moonraker-family
|
||||
state risk, made more consequential by Qidi's persistent firmware variables.
|
||||
The configured `printer_type` can also be stale, so treat it as a fallback
|
||||
hint rather than device truth.
|
||||
|
||||
## Snapmaker
|
||||
|
||||
Snapmaker uses the Moonraker base and overrides filament discovery and camera
|
||||
setup. Neither path is hardware-verified in the current documentation set.
|
||||
|
||||
Filament information comes from parallel arrays in `print_task_config`.
|
||||
`filament_exist` defines the number of slots; shorter type, subtype, colour,
|
||||
vendor, or NFC arrays use safe fallback values. The agent first tries a visible
|
||||
vendor, type, and colour preset, then a visible type match, and finally a
|
||||
generic identifier when no preset bundle is available. An empty reported type
|
||||
is changed to `PLA`, so an unknown occupied spool can look like confirmed PLA.
|
||||
An unrecognized type can also reach the visible-preset fallback and be paired
|
||||
with an unrelated visible preset. Treat the resulting preset as a suggestion,
|
||||
not printer-ground truth.
|
||||
|
||||
Snapmaker U1 camera support starts the printer's monitor RPC, then serves the
|
||||
still JPEG through a small local HTML page that reloads it after each load or
|
||||
error. The wrapper is required because a direct still-image URL looks frozen.
|
||||
The RPC is sent from a detached thread so the UI timer does not block on socket
|
||||
I/O. That thread captures `this` directly, so agent destruction can race with
|
||||
the camera command. Do not widen this pattern. Route future asynchronous work
|
||||
through owned lifetime-managed work where possible.
|
||||
|
||||
## Source locations
|
||||
|
||||
- `src/slic3r/Utils/MoonrakerPrinterAgent.cpp`
|
||||
- `src/slic3r/Utils/QidiPrinterAgent.cpp`
|
||||
- `src/slic3r/Utils/SnapmakerPrinterAgent.cpp`
|
||||
@@ -1,158 +0,0 @@
|
||||
# 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<br/>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.
|
||||
@@ -1,96 +0,0 @@
|
||||
# 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<br/>LAN or cloud, not from IPrinterAgent"]
|
||||
MU["Moonraker webcam discovery<br/>/server/webcams/list stream URL"]
|
||||
SU["Snapmaker camera page<br/>local HTML that polls monitor.jpg"]
|
||||
|
||||
BV["wxMediaCtrl2<br/>native Bambu media pipeline"]
|
||||
WV["Device-panel wxWebView<br/>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.
|
||||
@@ -1,248 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,110 +0,0 @@
|
||||
# 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<br/>Moonraker lane_data, or the classic MMU object"]
|
||||
ACQ["1 - Acquire state<br/>Moonraker pull, not subscription"]
|
||||
ST["DevFilaSystem<br/>Bambu-shaped view, synthetic 4-slot groups"]
|
||||
SEL["2 - Select mapping<br/>Send dialog matches project filament to slots"]
|
||||
PAR["PrintParams<br/>ams_mapping fields"]
|
||||
QIDI["Qidi agent writes enable_box<br/>and value_t per tool, then starts the print"]
|
||||
BASE["Base Moonraker, Happy Hare, AFC<br/>upload and start ignore the mapping fields"]
|
||||
OK["Mapping reaches the printer"]
|
||||
DEAD["Mapping never reaches the printer<br/>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.
|
||||
@@ -1,161 +0,0 @@
|
||||
# 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 {
|
||||
<<returned by the agent via get_agent_info>>
|
||||
string id
|
||||
string name
|
||||
string version
|
||||
string description
|
||||
}
|
||||
class PrinterAgentInfo {
|
||||
<<the registry entry>>
|
||||
string id
|
||||
string display_name
|
||||
string plugin_identifier
|
||||
PrinterAgentFactory factory
|
||||
}
|
||||
class PrinterAgentFactory {
|
||||
<<std::function>>
|
||||
takes cloud_agent and log_dir
|
||||
returns shared_ptr~IPrinterAgent~
|
||||
}
|
||||
class NetworkAgentFactory {
|
||||
<<all static>>
|
||||
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
|
||||
`<plugin_key>;<uuid>;<capability_name>` 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`
|
||||
@@ -1,138 +0,0 @@
|
||||
# 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 -<br/>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<br/>the printer prepares - a timeout is not<br/>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.<br/>Never reuse a machine pointer captured<br/>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.
|
||||
@@ -1,137 +0,0 @@
|
||||
# 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.
|
||||
File diff suppressed because one or more lines are too long
@@ -1,280 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Printer-agent manual checklist</title>
|
||||
<style>
|
||||
:root { color-scheme: light dark; --bg:#111827; --panel:#1f2937; --line:#4b5563;
|
||||
--text:#f9fafb; --muted:#cbd5e1; --pass:#34d399; --fail:#f87171; --block:#fbbf24; }
|
||||
* { box-sizing:border-box; }
|
||||
body { max-width:960px; margin:0 auto; padding:24px; font:15px/1.5 system-ui,sans-serif;
|
||||
color:var(--text); background:var(--bg); }
|
||||
h1 { margin:0 0 4px; } h2 { margin-top:32px; } p, li { max-width:78ch; }
|
||||
.muted { color:var(--muted); } .meta { display:grid; grid-template-columns:repeat(2,minmax(0,1fr));
|
||||
gap:12px; margin:20px 0; } label { display:grid; gap:4px; }
|
||||
input, textarea, button { font:inherit; } input, textarea { width:100%; padding:8px;
|
||||
border:1px solid var(--line); border-radius:6px; color:inherit; background:var(--panel); }
|
||||
textarea { min-height:58px; margin-top:8px; } .toolbar { display:flex; flex-wrap:wrap;
|
||||
gap:8px; align-items:center; margin:16px 0; } button { border:1px solid var(--line);
|
||||
border-radius:6px; padding:6px 10px; cursor:pointer; color:inherit; background:var(--panel); }
|
||||
button[data-state="pass"].active { color:#062d1d; background:var(--pass); }
|
||||
button[data-state="fail"].active { color:#3b0808; background:var(--fail); }
|
||||
button[data-state="blocked"].active { color:#3b2600; background:var(--block); }
|
||||
.progress { flex:1 1 240px; height:10px; overflow:hidden; border-radius:999px; background:var(--line); }
|
||||
.progress > div { height:100%; width:0; background:var(--pass); transition:width .15s; }
|
||||
.case { margin:10px 0; padding:12px; border:1px solid var(--line); border-left:4px solid var(--line);
|
||||
border-radius:6px; background:var(--panel); } .case.pass { border-left-color:var(--pass); }
|
||||
.case.fail { border-left-color:var(--fail); } .case.blocked { border-left-color:var(--block); }
|
||||
.case-head { display:flex; justify-content:space-between; gap:12px; align-items:start; }
|
||||
.case h3 { margin:0; font-size:1rem; } .actions { display:flex; gap:5px; white-space:nowrap; }
|
||||
.restore-warning { padding:8px 10px; border:1px solid var(--block); border-radius:6px;
|
||||
color:var(--block); background:var(--panel); }
|
||||
code { overflow-wrap:anywhere; } @media (max-width:600px) { body { padding:16px; }
|
||||
.meta { grid-template-columns:1fr; } .case-head { display:block; } .actions { margin-top:8px; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Printer-agent manual checklist</h1>
|
||||
<p class="muted">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.</p>
|
||||
|
||||
<section class="meta" aria-label="Test context">
|
||||
<label>Agent and printer<input id="target" placeholder="for example: Moonraker - printer model"></label>
|
||||
<label>Firmware and configuration<input id="environment" placeholder="firmware, MMU, camera, API key setup"></label>
|
||||
<label>Build or package<input id="build" placeholder="version or build identifier"></label>
|
||||
<label>Tester and date<input id="tester" placeholder="name and date"></label>
|
||||
</section>
|
||||
|
||||
<div class="toolbar">
|
||||
<strong id="summary">0 of 0 cases marked</strong>
|
||||
<div class="progress" aria-label="Checklist progress"><div id="bar"></div></div>
|
||||
<button id="export" type="button">Export Markdown</button>
|
||||
<button id="reset" type="button">Reset checklist</button>
|
||||
</div>
|
||||
|
||||
<p id="restore-warning" class="restore-warning" hidden>Saved checklist data could not be
|
||||
restored. You can export the current blank checklist or use Reset to remove the saved data.</p>
|
||||
|
||||
<p class="muted">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.</p>
|
||||
|
||||
<section>
|
||||
<h2>Connect and observe status</h2>
|
||||
<div class="case" data-id="connect">
|
||||
<div class="case-head"><div><h3>Connect to the selected printer</h3>
|
||||
<p>Expected: the Device tab receives a fresh status update after connection. Do not use a
|
||||
successful connection return alone as the result.</p></div><div class="actions"></div></div><textarea placeholder="Evidence, result, or blocker"></textarea></div>
|
||||
<div class="case" data-id="status">
|
||||
<div class="case-head"><div><h3>Observe live status changes</h3>
|
||||
<p>Expected: temperature and target changes, fan state, print state, filename, progress,
|
||||
elapsed time, and homing state reach the UI while the printer changes state.</p></div><div class="actions"></div></div><textarea placeholder="Evidence, result, or blocker"></textarea></div>
|
||||
<div class="case" data-id="reconnect">
|
||||
<div class="case-head"><div><h3>Disconnect and reconnect</h3>
|
||||
<p>Expected: a second connection produces new status messages and does not create a duplicate
|
||||
device. Record the post-reconnect status evidence.</p></div><div class="actions"></div></div><textarea placeholder="Evidence, result, or blocker"></textarea></div>
|
||||
<div class="case" data-id="network-errors">
|
||||
<div class="case-head"><div><h3>Handle network and response failures</h3>
|
||||
<p>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.</p></div><div class="actions"></div></div><textarea placeholder="Operation, injected failure, UI result, retry, and evidence"></textarea></div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Controls and printing</h2>
|
||||
<div class="case" data-id="controls">
|
||||
<div class="case-head"><div><h3>Exercise safe controls</h3>
|
||||
<p>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.</p></div><div class="actions"></div></div><textarea placeholder="Commands sent and printer-side result"></textarea></div>
|
||||
<div class="case" data-id="fifo-order">
|
||||
<div class="case-head"><div><h3>Verify queued command order under latency</h3>
|
||||
<p>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.</p></div><div class="actions"></div></div><textarea placeholder="Queued markers, latency method, printer-side order, and result"></textarea></div>
|
||||
<div class="case" data-id="send-only">
|
||||
<div class="case-head"><div><h3>Send a file without starting it</h3>
|
||||
<p>Expected: the file appears on the printer and no print starts.</p></div><div class="actions"></div></div><textarea placeholder="Filename and observed result"></textarea></div>
|
||||
<div class="case" data-id="print">
|
||||
<div class="case-head"><div><h3>Start a small print</h3>
|
||||
<p>Expected: upload completes, the printer starts the selected file, and status transitions
|
||||
to printing.</p></div><div class="actions"></div></div><textarea placeholder="Filename, response, and observed result"></textarea></div>
|
||||
<div class="case" data-id="active-print-controls">
|
||||
<div class="case-head"><div><h3>Pause, resume, and cancel an active print</h3>
|
||||
<p>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.</p></div><div class="actions"></div></div><textarea placeholder="State transitions and printer-side result"></textarea></div>
|
||||
<div class="case" data-id="print-failure">
|
||||
<div class="case-head"><div><h3>Check upload failure handling</h3>
|
||||
<p>Expected: cancellation during upload and a missing input fail clearly and do not begin a
|
||||
partial or unintended print.</p></div><div class="actions"></div></div><textarea placeholder="Failure path and observed result"></textarea></div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Filament, camera, and agent-specific checks</h2>
|
||||
<div class="case" data-id="filament-read">
|
||||
<div class="case-head"><div><h3>Refresh material-system data</h3>
|
||||
<p>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.</p></div><div class="actions"></div></div><textarea placeholder="MMU or box configuration and observed result"></textarea></div>
|
||||
<div class="case" data-id="filament-cleanup">
|
||||
<div class="case-head"><div><h3>Clear material data when filament or the system is absent</h3>
|
||||
<p>Expected: remove filament or disable the material system, refresh, and confirm the UI no
|
||||
longer shows obsolete slots, material names, or colours.</p></div><div class="actions"></div></div><textarea placeholder="Change made, refresh evidence, and remaining or cleared data"></textarea></div>
|
||||
<div class="case" data-id="filament-write">
|
||||
<div class="case-head"><div><h3>Verify print-time mapping where supported</h3>
|
||||
<p>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.</p></div><div class="actions"></div></div><textarea placeholder="Mapping path, printer configuration, and result"></textarea></div>
|
||||
<div class="case" data-id="qidi-nozzle-preflight">
|
||||
<div class="case-head"><div><h3>Check Qidi Send preflight with missing nozzle identity</h3>
|
||||
<p>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
|
||||
<code>PrintStatusNozzleDataInvalid</code>. Record any reported identity and any mismatch
|
||||
result separately; this does not approve a known mismatch.</p></div><div class="actions"></div></div><textarea placeholder="Slice, reported nozzle data, preflight result, and printer-side result"></textarea></div>
|
||||
<div class="case" data-id="camera">
|
||||
<div class="case-head"><div><h3>Verify a camera feed</h3>
|
||||
<p>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-<code>this</code> lifetime risk.</p></div><div class="actions"></div></div><textarea placeholder="Camera type, timestamps, agent swap or shutdown result, and evidence"></textarea></div>
|
||||
<div class="case" data-id="thumbnail">
|
||||
<div class="case-head"><div><h3>Verify the print thumbnail</h3>
|
||||
<p>Expected: test a reused filename after its thumbnail changes, response payloads with both
|
||||
<code>thumbnail_path</code> and <code>relative_path</code>, and a path below the G-code
|
||||
root. The displayed image must match the current file in each case.</p></div><div class="actions"></div></div><textarea placeholder="Filename, endpoint key and path, displayed image, and observed result"></textarea></div>
|
||||
<div class="case" data-id="plugin">
|
||||
<div class="case-head"><div><h3>Reload a Python printer-agent plug-in</h3>
|
||||
<p>Expected: the capability registers once, duplicate agent IDs are rejected visibly, and
|
||||
disable or unload removes the agent cleanly.</p></div><div class="actions"></div></div><textarea placeholder="Plug-in identifier, actions, and observed result"></textarea></div>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
const storageKey = 'orca-printer-agent-manual-checklist-v1';
|
||||
const cases = [...document.querySelectorAll('.case')];
|
||||
const inputs = [...document.querySelectorAll('input')];
|
||||
const restoreWarning = document.querySelector('#restore-warning');
|
||||
|
||||
function emptyState() {
|
||||
return { meta: {}, cases: {} };
|
||||
}
|
||||
function loadState() {
|
||||
const saved = localStorage.getItem(storageKey);
|
||||
if (!saved) return emptyState();
|
||||
try {
|
||||
const parsed = JSON.parse(saved);
|
||||
if (!parsed || Array.isArray(parsed) || typeof parsed !== 'object' ||
|
||||
(parsed.meta !== undefined && (Array.isArray(parsed.meta) || typeof parsed.meta !== 'object')) ||
|
||||
(parsed.cases !== undefined && (Array.isArray(parsed.cases) || typeof parsed.cases !== 'object'))) {
|
||||
throw new Error('incompatible saved checklist state');
|
||||
}
|
||||
return { meta: parsed.meta || {}, cases: parsed.cases || {} };
|
||||
} catch (error) {
|
||||
restoreWarning.hidden = false;
|
||||
return emptyState();
|
||||
}
|
||||
}
|
||||
const state = loadState();
|
||||
|
||||
function caseText(caseElement) {
|
||||
return caseElement.querySelector('h3').textContent.trim();
|
||||
}
|
||||
function caseCriteria(caseElement) {
|
||||
return [...caseElement.querySelectorAll('.case-head p')]
|
||||
.map(paragraph => markdownParagraph(paragraph.textContent))
|
||||
.filter(Boolean)
|
||||
.join('\n\n');
|
||||
}
|
||||
function save() {
|
||||
const data = { meta: {}, cases: {} };
|
||||
inputs.forEach(input => { data.meta[input.id] = input.value; });
|
||||
cases.forEach(item => {
|
||||
data.cases[item.dataset.id] = { status: item.dataset.status || '', note: item.querySelector('textarea').value };
|
||||
});
|
||||
localStorage.setItem(storageKey, JSON.stringify(data));
|
||||
updateProgress();
|
||||
}
|
||||
function renderStatus(item, status) {
|
||||
item.dataset.status = status;
|
||||
item.classList.toggle('pass', status === 'pass');
|
||||
item.classList.toggle('fail', status === 'fail');
|
||||
item.classList.toggle('blocked', status === 'blocked');
|
||||
item.querySelectorAll('button[data-state]').forEach(button => {
|
||||
button.classList.toggle('active', button.dataset.state === status);
|
||||
});
|
||||
}
|
||||
function applyStatus(item, status) {
|
||||
renderStatus(item, status);
|
||||
save();
|
||||
}
|
||||
function updateProgress() {
|
||||
const marked = cases.filter(item => item.dataset.status).length;
|
||||
document.querySelector('#summary').textContent = `${marked} of ${cases.length} cases marked`;
|
||||
document.querySelector('#bar').style.width = `${cases.length ? marked * 100 / cases.length : 0}%`;
|
||||
}
|
||||
function markdown() {
|
||||
const meta = stateFromInputs();
|
||||
const lines = ['# Printer-agent manual verification', '',
|
||||
`Target: ${markdownInline(meta.target) || 'not recorded'}`,
|
||||
`Firmware and configuration: ${markdownInline(meta.environment) || 'not recorded'}`,
|
||||
`Build or package: ${markdownInline(meta.build) || 'not recorded'}`,
|
||||
`Tester and date: ${markdownInline(meta.tester) || 'not recorded'}`, ''];
|
||||
cases.forEach(item => {
|
||||
const status = item.dataset.status || 'unmarked';
|
||||
const note = markdownInline(item.querySelector('textarea').value);
|
||||
lines.push(`## ${markdownInline(caseText(item))}`, '',
|
||||
`Status: ${status}`, '',
|
||||
'### Test criteria and expected result', '',
|
||||
caseCriteria(item) || 'Not recorded', '',
|
||||
'### Notes', '', note || 'None', '');
|
||||
});
|
||||
return lines.join('\n');
|
||||
}
|
||||
function markdownParagraph(value) {
|
||||
return markdownInline(value);
|
||||
}
|
||||
function markdownInline(value) {
|
||||
return String(value || '').replace(/\r\n?|\n/g, ' ').replace(/[\\`*_{}[\]<>#+!|]/g, '\\$&')
|
||||
.replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
function stateFromInputs() {
|
||||
return Object.fromEntries(inputs.map(input => [input.id, input.value]));
|
||||
}
|
||||
function downloadMarkdown() {
|
||||
const blob = new Blob([markdown()], { type: 'text/markdown;charset=utf-8' });
|
||||
const link = document.createElement('a');
|
||||
link.href = URL.createObjectURL(blob);
|
||||
link.download = 'printer-agent-manual-verification.md';
|
||||
link.click();
|
||||
URL.revokeObjectURL(link.href);
|
||||
}
|
||||
|
||||
cases.forEach(item => {
|
||||
const actions = item.querySelector('.actions');
|
||||
['pass', 'fail', 'blocked'].forEach(status => {
|
||||
const button = document.createElement('button');
|
||||
button.type = 'button';
|
||||
button.dataset.state = status;
|
||||
button.textContent = status[0].toUpperCase() + status.slice(1);
|
||||
button.addEventListener('click', () => applyStatus(item, status));
|
||||
actions.appendChild(button);
|
||||
});
|
||||
const saved = state.cases[item.dataset.id] || {};
|
||||
item.querySelector('textarea').value = typeof saved.note === 'string' ? saved.note : '';
|
||||
if (['pass', 'fail', 'blocked'].includes(saved.status)) renderStatus(item, saved.status);
|
||||
item.querySelector('textarea').addEventListener('input', save);
|
||||
});
|
||||
inputs.forEach(input => {
|
||||
input.value = typeof state.meta[input.id] === 'string' ? state.meta[input.id] : '';
|
||||
input.addEventListener('input', save);
|
||||
});
|
||||
document.querySelector('#export').addEventListener('click', downloadMarkdown);
|
||||
document.querySelector('#reset').addEventListener('click', () => {
|
||||
if (!confirm('Clear all saved checklist data for this browser?')) return;
|
||||
localStorage.removeItem(storageKey);
|
||||
location.reload();
|
||||
});
|
||||
updateProgress();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,249 +0,0 @@
|
||||
# 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
|
||||
`<configured-build-dir>` 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 <configured-build-dir> --config RelWithDebInfo --target slic3rutils_tests
|
||||
cmake --build <configured-build-dir> --config RelWithDebInfo --target printer_agent_plugin_tests
|
||||
ctest --test-dir <configured-build-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 <configured-build-dir> --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`.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -575,7 +575,7 @@ function CapabilityCanRun(plugin, capability) {
|
||||
}
|
||||
|
||||
function IsPluginChecked(plugin) {
|
||||
return plugin.is_loaded;
|
||||
return GetStatus(plugin) === "Activated";
|
||||
}
|
||||
|
||||
function HasMixedCapabilityState(plugin) {
|
||||
@@ -1347,8 +1347,6 @@ function StatusDescription(plugin) {
|
||||
return "This plugin is still loading.";
|
||||
case "Error":
|
||||
return "This plugin is blocked until its error is fixed.";
|
||||
case "RuntimeError":
|
||||
return "This plugin is loaded but a capability reported an error.";
|
||||
case "Inactive":
|
||||
default:
|
||||
return "This plugin is inactive. Activate it to install or load it.";
|
||||
|
||||
@@ -424,11 +424,6 @@ body.pane-resizing {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.status-cell.status-runtimeerror {
|
||||
color: var(--plugin-status-warn);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.status-cell.status-loading {
|
||||
color: var(--plugin-status-warn);
|
||||
font-weight: 600;
|
||||
@@ -685,11 +680,6 @@ body.pane-resizing {
|
||||
color: var(--plugin-status-danger);
|
||||
}
|
||||
|
||||
.detail-status-chip.status-runtimeerror {
|
||||
background: var(--plugin-status-warn-bg);
|
||||
color: var(--plugin-status-warn);
|
||||
}
|
||||
|
||||
.detail-status-chip.status-loading {
|
||||
background: var(--plugin-status-warn-bg);
|
||||
color: var(--plugin-status-warn);
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
#include <boost/filesystem.hpp>
|
||||
#include <boost/algorithm/clamp.hpp>
|
||||
#include <boost/algorithm/string/predicate.hpp>
|
||||
#include <boost/algorithm/string/join.hpp>
|
||||
#include <boost/range/adaptor/transformed.hpp>
|
||||
#include <boost/nowide/cstdio.hpp>
|
||||
#include <boost/nowide/fstream.hpp>
|
||||
@@ -3388,27 +3387,6 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
|
||||
ConfigOptionStrings *filament_color_type = project_config.option<ConfigOptionStrings>("filament_colour_type");
|
||||
ConfigOptionInts * filament_map = project_config.option<ConfigOptionInts>("filament_map");
|
||||
ConfigOptionInts * filament_volume_map = project_config.option<ConfigOptionInts>("filament_volume_map");
|
||||
// why: project filament_multi_colour stores space-joined components per
|
||||
// filament; decode once so every merge branch seeds from the same view,
|
||||
// falling back to the main color where the project has no components.
|
||||
auto decode_project_multi_colors = [this](const std::vector<std::string> &main_colors) {
|
||||
std::vector<std::vector<std::string>> decoded(main_colors.size());
|
||||
for (size_t i = 0; i < main_colors.size(); ++i) decoded[i] = {main_colors[i]};
|
||||
const ConfigOptionStrings *project_multi_color = project_config.option<ConfigOptionStrings>("filament_multi_colour");
|
||||
if (project_multi_color) {
|
||||
for (size_t i = 0; i < std::min(decoded.size(), project_multi_color->values.size()); ++i) {
|
||||
std::vector<std::string> colors = split_string(project_multi_color->values[i], ' ');
|
||||
// why: a whitespace-only persisted entry splits into empty
|
||||
// tokens, so keep the main-color fallback rather than
|
||||
// serializing an empty component.
|
||||
colors.erase(std::remove_if(colors.begin(), colors.end(),
|
||||
[](const std::string &c) { return c.empty(); }),
|
||||
colors.end());
|
||||
if (!colors.empty()) decoded[i] = colors;
|
||||
}
|
||||
}
|
||||
return decoded;
|
||||
};
|
||||
if (color_only) {
|
||||
auto get_map_index = [&ams_infos](const std::vector<AMSMapInfo> &infos, const AMSMapInfo &temp) {
|
||||
for (int i = 0; i < infos.size(); i++) {
|
||||
@@ -3421,7 +3399,20 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
|
||||
};
|
||||
|
||||
auto exist_colors = filament_color->values;
|
||||
auto exist_multi_color_filment = decode_project_multi_colors(exist_colors);
|
||||
std::vector<std::vector<std::string>> exist_multi_color_filment(exist_colors.size());
|
||||
for (size_t i = 0; i < exist_colors.size(); i++) {
|
||||
exist_multi_color_filment[i] = {exist_colors[i]};
|
||||
}
|
||||
|
||||
ConfigOptionStrings *project_multi_color = project_config.option<ConfigOptionStrings>("filament_multi_colour");
|
||||
if (project_multi_color) {
|
||||
for (size_t i = 0; i < std::min(exist_multi_color_filment.size(), project_multi_color->values.size()); i++) {
|
||||
std::vector<std::string> colors = split_string(project_multi_color->values[i], ' ');
|
||||
if (!colors.empty()) {
|
||||
exist_multi_color_filment[i] = colors;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool mapped_any = false;
|
||||
if (use_map && !maps.empty()) {
|
||||
@@ -3497,7 +3488,11 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
|
||||
auto exist_colors = filament_color->values;
|
||||
auto exist_color_types = filament_color_type->values;
|
||||
auto exist_filament_presets = this->filament_presets;
|
||||
auto exist_multi_color_filment = decode_project_multi_colors(exist_colors);
|
||||
std::vector<std::vector<std::string>> exist_multi_color_filment;
|
||||
exist_multi_color_filment.resize(exist_colors.size());
|
||||
for (int i = 0; i < exist_colors.size(); i++) {
|
||||
exist_multi_color_filment[i] = {exist_colors[i]};
|
||||
}
|
||||
for (size_t i = 0; i < exist_colors.size(); i++) {
|
||||
if (maps.find(i) != maps.end()) {//mapping exist
|
||||
auto valid_index = get_map_index(ams_array_maps, maps[i]);
|
||||
@@ -3505,10 +3500,7 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
|
||||
exist_colors[i] = ams_filament_colors[valid_index];
|
||||
exist_color_types[i] = ams_filament_color_types[valid_index];
|
||||
exist_filament_presets[i] = ams_filament_presets[valid_index];
|
||||
// why: a single-color agent tray reports no components, so
|
||||
// fall back to the printer main color rather than keeping
|
||||
// the replaced project filament's components.
|
||||
exist_multi_color_filment[i] = ams_multi_color_filment[valid_index].empty() ? std::vector<std::string>{ams_filament_colors[valid_index]} : ams_multi_color_filment[valid_index];
|
||||
exist_multi_color_filment[i] = ams_multi_color_filment[valid_index];
|
||||
} else {
|
||||
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << "check error: array bound (mapping exist)";
|
||||
}
|
||||
@@ -3568,7 +3560,7 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
|
||||
exist_filament_presets.push_back(need_append_colors[i].filament_preset);
|
||||
exist_colors.push_back(need_append_colors[i].filament_color);
|
||||
exist_color_types.push_back(need_append_colors[i].filament_color_type);
|
||||
exist_multi_color_filment.push_back(need_append_colors[i].mutli_filament_color.empty() ? std::vector<std::string>{need_append_colors[i].filament_color} : need_append_colors[i].mutli_filament_color);
|
||||
exist_multi_color_filment.push_back(need_append_colors[i].mutli_filament_color);
|
||||
}
|
||||
}
|
||||
filament_color->values = exist_colors;
|
||||
@@ -3586,7 +3578,6 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
|
||||
auto exist_colors = filament_color->values;
|
||||
auto exist_color_types = filament_color_type->values;
|
||||
auto exist_presets = this->filament_presets;
|
||||
auto existing_multi_colors = decode_project_multi_colors(exist_colors);
|
||||
|
||||
size_t tray_count = ams_filament_presets.size();
|
||||
size_t total = std::max(tray_count, exist_presets.size());
|
||||
@@ -3596,34 +3587,25 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
|
||||
std::vector<std::string> result_presets;
|
||||
std::vector<std::vector<std::string>> result_multi_colors;
|
||||
|
||||
for (size_t i = 0; i < total; ++i) {
|
||||
const bool is_loaded = i < ams_infos.size() && ams_infos[i].valid;
|
||||
for (size_t i = 0; i < total; i++) {
|
||||
bool is_loaded = (i < ams_infos.size() && ams_infos[i].valid);
|
||||
|
||||
if (is_loaded) {
|
||||
// The printer replaces the project filament at this index.
|
||||
// Loaded tray: use tray's filament data
|
||||
result_colors.push_back(ams_filament_colors[i]);
|
||||
result_color_types.push_back(ams_filament_color_types[i]);
|
||||
result_presets.push_back(ams_filament_presets[i]);
|
||||
if (i < ams_multi_color_filment.size() && !ams_multi_color_filment[i].empty()) {
|
||||
result_multi_colors.push_back(ams_multi_color_filment[i]);
|
||||
} else {
|
||||
// why: old project components belong to the replaced
|
||||
// filament, so use the new printer filament's main color.
|
||||
result_multi_colors.push_back({ams_filament_colors[i]});
|
||||
}
|
||||
// why: update_multi_material_filament_presets() can grow
|
||||
// filament_presets alone to the extruder count, so presets
|
||||
// may outrun the colour arrays; fall through to a generic
|
||||
// filament rather than read past them.
|
||||
} else if (i < exist_presets.size() && i < exist_colors.size() && i < exist_color_types.size()) {
|
||||
// An empty or absent printer slot retains the project filament.
|
||||
result_multi_colors.push_back(
|
||||
i < ams_multi_color_filment.size() ? ams_multi_color_filment[i]
|
||||
: std::vector<std::string>{ams_filament_colors[i]});
|
||||
} else if (i < exist_presets.size()) {
|
||||
// Empty tray or beyond tray count: keep existing filament
|
||||
result_colors.push_back(exist_colors[i]);
|
||||
result_color_types.push_back(exist_color_types[i]);
|
||||
result_presets.push_back(exist_presets[i]);
|
||||
// note: already carries the project components or its
|
||||
// main-color fallback.
|
||||
result_multi_colors.push_back(existing_multi_colors[i]);
|
||||
result_multi_colors.push_back({exist_colors[i]});
|
||||
} else {
|
||||
// Neither source has a filament, so create a generic one.
|
||||
// New slot beyond existing count: prefer a generic filament preset
|
||||
auto it = std::find_if(filaments.begin(), filaments.end(), [](const Preset &f) {
|
||||
return f.is_compatible && f.is_system
|
||||
&& boost::algorithm::starts_with(f.name, "Generic ");
|
||||
@@ -3669,28 +3651,23 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
|
||||
|
||||
void PresetBundle::update_filament_multi_color()
|
||||
{
|
||||
const ConfigOptionStrings *filament_color = project_config.option<ConfigOptionStrings>("filament_colour");
|
||||
ConfigOptionStrings *filament_multi_colour = project_config.option<ConfigOptionStrings>("filament_multi_colour");
|
||||
// note: cheap defence only. Both keys live in s_project_options and the
|
||||
// constructor applies them unconditionally, so this never fires today.
|
||||
if (!filament_color || !filament_multi_colour) return;
|
||||
// note: this is std::vector::resize(), which safely creates empty inner
|
||||
// vectors. Not ConfigOptionStrings::resize(), which throws without a
|
||||
// default value.
|
||||
ams_multi_color_filment.resize(filament_color->values.size());
|
||||
for (size_t i = 0; i < filament_color->values.size(); ++i) {
|
||||
if (ams_multi_color_filment[i].empty()) {
|
||||
// why: every final main color requires one parallel
|
||||
// multi-color entry.
|
||||
ams_multi_color_filment[i] = {filament_color->values[i]};
|
||||
std::vector<std::string> exsit_multi_colors;
|
||||
for (auto &fil_item : ams_multi_color_filment){
|
||||
if (fil_item.empty()) break;
|
||||
if (fil_item.size() == 1)
|
||||
exsit_multi_colors.push_back(fil_item[0]);
|
||||
else {
|
||||
std::string colors = "";
|
||||
for (auto &color : fil_item){
|
||||
colors += color + " ";
|
||||
}
|
||||
colors.erase(colors.size() - 1); // remove last space
|
||||
exsit_multi_colors.push_back(colors);
|
||||
}
|
||||
}
|
||||
std::vector<std::string> serialized;
|
||||
serialized.reserve(ams_multi_color_filment.size());
|
||||
for (const auto &components : ams_multi_color_filment)
|
||||
serialized.push_back(boost::algorithm::join(components, " "));
|
||||
// why: assignment sets both size and contents, so no resize is needed.
|
||||
filament_multi_colour->values = std::move(serialized);
|
||||
ConfigOptionStrings *filament_multi_colour = project_config.option<ConfigOptionStrings>("filament_multi_colour");
|
||||
filament_multi_colour->resize(exsit_multi_colors.size());
|
||||
filament_multi_colour->values = exsit_multi_colors;
|
||||
}
|
||||
|
||||
std::vector<int> PresetBundle::get_used_tpu_filaments(const std::vector<int> &used_filaments)
|
||||
|
||||
@@ -163,7 +163,7 @@ void ConnectPrinterDialog::on_button_confirm(wxCommandEvent &event)
|
||||
}
|
||||
}
|
||||
if (m_obj) {
|
||||
m_obj->set_access_code(code.ToStdString());
|
||||
m_obj->set_user_access_code(code.ToStdString());
|
||||
}
|
||||
EndModal(wxID_OK);
|
||||
}
|
||||
|
||||
@@ -15,19 +15,6 @@
|
||||
|
||||
using namespace nlohmann;
|
||||
|
||||
namespace {
|
||||
// Orca: access_code and the now-removed user_access_code field used to be persisted under
|
||||
// separate AppConfig keys. Fall back to the legacy key so upgrading users don't lose a
|
||||
// previously-saved code.
|
||||
std::string get_access_code_with_legacy_fallback(Slic3r::AppConfig* config, const std::string& dev_id)
|
||||
{
|
||||
std::string code = config->get("access_code", dev_id);
|
||||
if (code.empty())
|
||||
code = config->get("user_access_code", dev_id);
|
||||
return code;
|
||||
}
|
||||
}
|
||||
|
||||
namespace Slic3r
|
||||
{
|
||||
DeviceManager::DeviceManager(NetworkAgent* agent)
|
||||
@@ -61,7 +48,8 @@ namespace Slic3r
|
||||
obj->bind_sec_link = "secure";
|
||||
obj->m_is_online = true;
|
||||
obj->last_alive = Slic3r::Utils::get_current_time_utc();
|
||||
obj->set_access_code(get_access_code_with_legacy_fallback(config, m.dev_id), false);
|
||||
obj->set_access_code(config->get("access_code", m.dev_id), false);
|
||||
obj->set_user_access_code(config->get("user_access_code", m.dev_id), false);
|
||||
if (obj->has_access_right()) {
|
||||
localMachineList.insert(std::make_pair(m.dev_id, obj));
|
||||
} else {
|
||||
@@ -351,7 +339,8 @@ namespace Slic3r
|
||||
//load access code
|
||||
AppConfig* config = Slic3r::GUI::wxGetApp().app_config;
|
||||
if (config) {
|
||||
obj->set_access_code(get_access_code_with_legacy_fallback(config, dev_id), false);
|
||||
obj->set_access_code(Slic3r::GUI::wxGetApp().app_config->get("access_code", dev_id), false);
|
||||
obj->set_user_access_code(Slic3r::GUI::wxGetApp().app_config->get("user_access_code", dev_id), false);
|
||||
}
|
||||
localMachineList.insert(std::make_pair(dev_id, obj));
|
||||
|
||||
@@ -393,6 +382,7 @@ namespace Slic3r
|
||||
obj->m_is_online = true;
|
||||
obj->last_alive = Slic3r::Utils::get_current_time_utc();
|
||||
obj->set_access_code(access_code, false);
|
||||
obj->set_user_access_code(access_code, false);
|
||||
|
||||
update_local_machine(*obj);
|
||||
|
||||
@@ -588,7 +578,6 @@ namespace Slic3r
|
||||
}
|
||||
else
|
||||
{
|
||||
Slic3r::GUI::wxGetApp().reset_unsigned_plugin_warning();
|
||||
if (m_agent)
|
||||
{
|
||||
if (it->second->connection_type() != "lan" || it->second->connection_type().empty())
|
||||
@@ -622,7 +611,6 @@ namespace Slic3r
|
||||
}
|
||||
|
||||
selected_machine = dev_id;
|
||||
record_user_last_machine(selected_machine);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -887,38 +875,20 @@ namespace Slic3r
|
||||
}
|
||||
}
|
||||
|
||||
void DeviceManager::record_user_last_machine(const std::string& dev_id)
|
||||
{
|
||||
if (Slic3r::GUI::wxGetApp().app_config) {
|
||||
Slic3r::GUI::wxGetApp().app_config->set("user_last_selected_machine", dev_id);
|
||||
}
|
||||
}
|
||||
|
||||
std::string DeviceManager::get_user_last_machine() const
|
||||
{
|
||||
if (Slic3r::GUI::wxGetApp().app_config) {
|
||||
const auto& user_last_machine = Slic3r::GUI::wxGetApp().app_config->get("user_last_selected_machine");
|
||||
if (!user_last_machine.empty()) {
|
||||
return user_last_machine;
|
||||
} else if (m_agent) {
|
||||
return m_agent->get_user_selected_machine();
|
||||
}
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
void DeviceManager::load_last_machine()
|
||||
{
|
||||
// Only reconnect the remembered cloud machine. Do not select an arbitrary
|
||||
// first machine: agent swaps intentionally leave the selection empty until
|
||||
// the new agent explicitly selects its configured printer.
|
||||
if (userMachineList.empty())
|
||||
// Get all available machines, include cloud machines and lan machines that have access right
|
||||
auto all_machines = get_my_machine_list();
|
||||
if (all_machines.empty())
|
||||
return;
|
||||
|
||||
const auto& last_monitor_machine = get_user_last_machine();
|
||||
if (userMachineList.find(last_monitor_machine) != userMachineList.end())
|
||||
set_selected_machine(last_monitor_machine);
|
||||
|
||||
// Reconnect the machine the user last selected, if it's still available.
|
||||
// why: no first-available fallback - auto-connecting an arbitrary machine
|
||||
// fights the agent-swap reset, which intentionally leaves nothing selected.
|
||||
const std::string last_monitor_machine = m_agent ? m_agent->get_user_selected_machine() : "";
|
||||
const auto last_machine = all_machines.find(last_monitor_machine);
|
||||
if (last_machine != all_machines.end())
|
||||
this->set_selected_machine(last_machine->second->get_dev_id());
|
||||
}
|
||||
|
||||
void DeviceManager::OnMachineBindStateChanged(MachineObject* obj, const std::string& new_state)
|
||||
|
||||
@@ -52,9 +52,6 @@ public:
|
||||
// swap path can reuse it instead of duplicating the two sidebar calls.
|
||||
void OnSelectedMachineLost();
|
||||
|
||||
void record_user_last_machine(const std::string& dev_id);
|
||||
std::string get_user_last_machine() const;
|
||||
|
||||
// local machine
|
||||
void set_local_selected_machine(std::string dev_id) { local_selected_machine = dev_id; };
|
||||
MachineObject* get_local_selected_machine() const { return get_local_machine(local_selected_machine); }
|
||||
@@ -146,4 +143,4 @@ public:
|
||||
protected:
|
||||
virtual void on_timer(wxTimerEvent& event);
|
||||
};
|
||||
};
|
||||
};
|
||||
@@ -449,7 +449,9 @@ bool MachineObject::HasRecentLanMessage()
|
||||
|
||||
std::string MachineObject::get_access_code() const
|
||||
{
|
||||
return access_code;
|
||||
if (get_user_access_code().empty())
|
||||
return access_code;
|
||||
return get_user_access_code();
|
||||
}
|
||||
|
||||
void MachineObject::set_access_code(std::string code, bool only_refresh)
|
||||
@@ -468,6 +470,37 @@ void MachineObject::set_access_code(std::string code, bool only_refresh)
|
||||
}
|
||||
}
|
||||
|
||||
void MachineObject::erase_user_access_code()
|
||||
{
|
||||
this->user_access_code = "";
|
||||
AppConfig* config = GUI::wxGetApp().app_config;
|
||||
if (config) {
|
||||
GUI::wxGetApp().app_config->erase("user_access_code", get_dev_id());
|
||||
//GUI::wxGetApp().app_config->save();
|
||||
}
|
||||
}
|
||||
|
||||
void MachineObject::set_user_access_code(std::string code, bool only_refresh)
|
||||
{
|
||||
this->user_access_code = code;
|
||||
if (only_refresh && !code.empty()) {
|
||||
AppConfig* config = GUI::wxGetApp().app_config;
|
||||
if (config && !code.empty()) {
|
||||
GUI::wxGetApp().app_config->set_str("user_access_code", get_dev_id(), code);
|
||||
DeviceManager::update_local_machine(*this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::string MachineObject::get_user_access_code() const
|
||||
{
|
||||
AppConfig* config = GUI::wxGetApp().app_config;
|
||||
if (config) {
|
||||
return GUI::wxGetApp().app_config->get("user_access_code", get_dev_id());
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
std::string MachineObject::get_show_printer_type() const
|
||||
{
|
||||
std::string printer_type = this->printer_type;
|
||||
@@ -1700,11 +1733,9 @@ int MachineObject::command_ams_user_settings(bool start_read_opt, bool tray_read
|
||||
|
||||
int MachineObject::command_ams_calibrate(int ams_id)
|
||||
{
|
||||
if (!m_agent) return -1;
|
||||
int rtn = m_agent->command_ams_calibrate(get_dev_id(), ams_id, MachineObject::m_sequence_id++, is_lan_mode_printer());
|
||||
if (rtn == ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED || rtn == ORCA_NETWORK_ERR_CAP_NOT_AVAILABLE)
|
||||
show_unsupported_dlg(rtn);
|
||||
return rtn;
|
||||
std::string gcode_cmd = (boost::format("M620 C%1% \n") % ams_id).str();
|
||||
BOOST_LOG_TRIVIAL(trace) << "ams_debug: gcode_cmd" << gcode_cmd;
|
||||
return this->publish_gcode(gcode_cmd);
|
||||
}
|
||||
|
||||
int MachineObject::command_ams_filament_settings(int ams_id, int slot_id, std::string filament_id, std::string setting_id, std::string tray_color, std::string tray_type, int nozzle_temp_min, int nozzle_temp_max)
|
||||
@@ -1742,11 +1773,9 @@ int MachineObject::command_ams_filament_settings(int ams_id, int slot_id, std::s
|
||||
|
||||
int MachineObject::command_ams_refresh_rfid(std::string tray_id)
|
||||
{
|
||||
if (!m_agent) return -1;
|
||||
int rtn = m_agent->command_ams_refresh_rfid(get_dev_id(), tray_id, MachineObject::m_sequence_id++, is_lan_mode_printer());
|
||||
if (rtn == ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED || rtn == ORCA_NETWORK_ERR_CAP_NOT_AVAILABLE)
|
||||
show_unsupported_dlg(rtn);
|
||||
return rtn;
|
||||
std::string gcode_cmd = (boost::format("M620 R%1% \n") % tray_id).str();
|
||||
BOOST_LOG_TRIVIAL(trace) << "ams_debug: gcode_cmd" << gcode_cmd;
|
||||
return this->publish_gcode(gcode_cmd);
|
||||
}
|
||||
|
||||
int MachineObject::command_ams_refresh_rfid2(int ams_id, int slot_id)
|
||||
@@ -1759,22 +1788,12 @@ int MachineObject::command_ams_refresh_rfid2(int ams_id, int slot_id)
|
||||
return this->publish_json(j);
|
||||
}
|
||||
|
||||
int MachineObject::command_start_camera()
|
||||
{
|
||||
if (!m_agent) return -1;
|
||||
// why: this fires from the camera view's renew timer, so a refusal must stay silent -
|
||||
// show_unsupported_dlg() here would pop a dialog every ~5 min on every other printer.
|
||||
return m_agent->command_start_camera(get_dev_id());
|
||||
}
|
||||
|
||||
|
||||
int MachineObject::command_ams_select_tray(std::string tray_id)
|
||||
{
|
||||
if (!m_agent) return -1;
|
||||
int rtn = m_agent->command_ams_select_tray(get_dev_id(), tray_id, MachineObject::m_sequence_id++, is_lan_mode_printer());
|
||||
if (rtn == ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED || rtn == ORCA_NETWORK_ERR_CAP_NOT_AVAILABLE)
|
||||
show_unsupported_dlg(rtn);
|
||||
return rtn;
|
||||
std::string gcode_cmd = (boost::format("M620 P%1% \n") % tray_id).str();
|
||||
BOOST_LOG_TRIVIAL(trace) << "ams_debug: gcode_cmd" << gcode_cmd;
|
||||
return this->publish_gcode(gcode_cmd);
|
||||
}
|
||||
|
||||
int MachineObject::command_ams_control(std::string action)
|
||||
@@ -2598,12 +2617,7 @@ void MachineObject::reset()
|
||||
vt_slot.erase(vt_slot.begin() + 1);
|
||||
}
|
||||
}
|
||||
// why: reset reuses MachineObject, so release its lazy subtask
|
||||
// before dropping the pointer to prevent reconnect leaks.
|
||||
if (subtask_) {
|
||||
delete subtask_;
|
||||
subtask_ = nullptr;
|
||||
}
|
||||
subtask_ = nullptr;
|
||||
has_extra_flow_type = false;
|
||||
m_partskip_ids.clear();
|
||||
}
|
||||
@@ -2613,20 +2627,6 @@ void MachineObject::set_print_state(std::string status)
|
||||
print_status = status;
|
||||
}
|
||||
|
||||
// why: printer agents can report progress without BBL cloud task identity.
|
||||
void MachineObject::update_print_progress(const json& value)
|
||||
{
|
||||
if (value.is_string())
|
||||
mc_print_percent = stoi(value.get<std::string>());
|
||||
else if (value.is_number_integer())
|
||||
mc_print_percent = value.get<int>();
|
||||
else
|
||||
return;
|
||||
|
||||
if (BBLSubTask* curr_task = get_subtask())
|
||||
curr_task->task_progress = mc_print_percent;
|
||||
}
|
||||
|
||||
int MachineObject::connect(bool use_openssl)
|
||||
{
|
||||
if (get_dev_ip().empty()) return -1;
|
||||
@@ -2740,14 +2740,6 @@ int MachineObject::publish_json(const json& json_item, int qos, int flag)
|
||||
BOOST_LOG_TRIVIAL(error) << "publish_json: " << json_item.dump() << " code: " << rtn;
|
||||
}
|
||||
|
||||
// why: the agent is the only thing that knows what it can translate, so it reports
|
||||
// not-supported in its return value and this - the single funnel every command_* builder
|
||||
// passes through - is the one place that turns it into something the user sees. No list of
|
||||
// unsupported commands is needed anywhere: an agent that has no case for a command says so.
|
||||
if (rtn == ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED || rtn == ORCA_NETWORK_ERR_CAP_NOT_AVAILABLE) {
|
||||
show_unsupported_dlg(rtn);
|
||||
}
|
||||
|
||||
return rtn;
|
||||
}
|
||||
|
||||
@@ -2915,6 +2907,7 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_
|
||||
std::string access_code = j_pre["system"]["access_code"].get<std::string>();
|
||||
if (!access_code.empty()) {
|
||||
set_access_code(access_code);
|
||||
set_user_access_code(access_code);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3304,7 +3297,10 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_
|
||||
print_type = jj["print_type"].get<std::string>();
|
||||
}
|
||||
if (jj.contains("mc_percent")) {
|
||||
update_print_progress(jj["mc_percent"]);
|
||||
if (jj["mc_percent"].is_string())
|
||||
mc_print_percent = stoi(j["print"]["mc_percent"].get<std::string>());
|
||||
else if (jj["mc_percent"].is_number_integer())
|
||||
mc_print_percent = j["print"]["mc_percent"].get<int>();
|
||||
}
|
||||
if (jj.contains("mc_print_sub_stage")) {
|
||||
if (jj["mc_print_sub_stage"].is_number_integer())
|
||||
@@ -3474,9 +3470,6 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_
|
||||
this->task_id_ = jj["task_id"].get<std::string>();
|
||||
}
|
||||
|
||||
if (jj.contains("thumbnail_url") && jj["thumbnail_url"].is_string())
|
||||
m_agent_thumbnail_url = jj["thumbnail_url"].get<std::string>();
|
||||
|
||||
if (jj.contains("job_attr")) {
|
||||
int jobAttr = jj["job_attr"].get<int>();
|
||||
jobState_ = get_flag_bits(jobAttr, 4, 4);
|
||||
@@ -3522,6 +3515,7 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_
|
||||
update_slice_info(jj["project_id"].get<std::string>(), jj["profile_id"].get<std::string>(), jj["subtask_id"].get<std::string>(), plate_index);
|
||||
BBLSubTask* curr_task = get_subtask();
|
||||
if (curr_task) {
|
||||
curr_task->task_progress = mc_print_percent;
|
||||
curr_task->printing_status = print_status;
|
||||
curr_task->task_id = jj["subtask_id"].get<std::string>();
|
||||
}
|
||||
@@ -3824,7 +3818,6 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_
|
||||
has_ipcam = true;
|
||||
} else {
|
||||
has_ipcam = false;
|
||||
webcam_stream_url.clear();
|
||||
}
|
||||
}
|
||||
if (ipcam.contains("resolution")) {
|
||||
@@ -3859,9 +3852,6 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_
|
||||
liveview_local = local_rtsp_url.empty() ? LVL_None : local_rtsp_url == "disable"
|
||||
? LVL_Disable : boost::algorithm::starts_with(local_rtsp_url, "rtsps") ? LVL_Rtsps : LVL_Rtsp;
|
||||
}
|
||||
if (ipcam.contains("stream_url") && ipcam["stream_url"].is_string()) {
|
||||
webcam_stream_url = ipcam["stream_url"].get<std::string>();
|
||||
}
|
||||
if (ipcam.contains("tutk_server")) {
|
||||
tutk_state = ipcam["tutk_server"].get<std::string>();
|
||||
}
|
||||
@@ -4657,40 +4647,6 @@ void MachineObject::set_ctt_dlg( wxString text){
|
||||
}
|
||||
}
|
||||
|
||||
void MachineObject::show_unsupported_dlg(int code)
|
||||
{
|
||||
// why: a dead control invites repeat clicks, and the frame is modeless - without the guard
|
||||
// every click stacks another one. Same shape as set_ctt_dlg above, including the reset on
|
||||
// both hide and close so a dismissed dialog can reappear on the next attempt.
|
||||
if (m_unsupported_dlg_shown) {
|
||||
return;
|
||||
}
|
||||
m_unsupported_dlg_shown = true;
|
||||
|
||||
// why: two codes so the user learns which kind of dead end this is - the slicer having no
|
||||
// translation for the command, or the printer's own config lacking the hardware to run it.
|
||||
const wxString text = (code == ORCA_NETWORK_ERR_CAP_NOT_AVAILABLE) ?
|
||||
_L("This printer is not configured with the hardware this control needs.") :
|
||||
_L("This control is not supported on this printer.");
|
||||
|
||||
// note: constructed directly rather than through CallAfter because every publish_json caller
|
||||
// is on the UI thread - clicks come from wx handlers, and the agent marshals its own push
|
||||
// callbacks back to main before parse_json runs. set_ctt_dlg relies on the same property.
|
||||
auto unsupported_dlg = new GUI::SecondaryCheckDialog(nullptr, wxID_ANY, _L("Warning"),
|
||||
GUI::SecondaryCheckDialog::VisibleButtons::ONLY_CONFIRM);
|
||||
unsupported_dlg->update_text(text);
|
||||
unsupported_dlg->Bind(wxEVT_SHOW, [this](auto& e) {
|
||||
if (!e.IsShown()) {
|
||||
m_unsupported_dlg_shown = false;
|
||||
}
|
||||
});
|
||||
unsupported_dlg->Bind(wxEVT_CLOSE_WINDOW, [this](auto& e) {
|
||||
e.Skip();
|
||||
m_unsupported_dlg_shown = false;
|
||||
});
|
||||
unsupported_dlg->on_show();
|
||||
}
|
||||
|
||||
int MachineObject::publish_gcode(std::string gcode_str)
|
||||
{
|
||||
json j;
|
||||
|
||||
@@ -113,6 +113,7 @@ private:
|
||||
std::string dev_name;
|
||||
std::string dev_ip;
|
||||
std::string access_code;
|
||||
std::string user_access_code;
|
||||
|
||||
// type, time stamp, delay
|
||||
std::vector<std::tuple<std::string, uint64_t, uint64_t>> message_delay;
|
||||
@@ -227,6 +228,11 @@ public:
|
||||
std::string get_access_code() const;
|
||||
void set_access_code(std::string code, bool only_refresh = true);
|
||||
|
||||
/*user access code*/
|
||||
void set_user_access_code(std::string code, bool only_refresh = true);
|
||||
void erase_user_access_code();
|
||||
std::string get_user_access_code() const;
|
||||
|
||||
//PRINTER_TYPE printer_type = PRINTER_3DPrinter_UKNOWN;
|
||||
std::string printer_type; /* model_id */
|
||||
std::string get_show_printer_type() const;
|
||||
@@ -266,11 +272,9 @@ public:
|
||||
bool m_is_online;
|
||||
bool m_lan_mode_connection_state{false};
|
||||
bool m_set_ctt_dlg{ false };
|
||||
bool m_unsupported_dlg_shown{ false };
|
||||
void set_lan_mode_connection_state(bool state) {m_lan_mode_connection_state = state;};
|
||||
bool get_lan_mode_connection_state() {return m_lan_mode_connection_state;};
|
||||
void set_ctt_dlg( wxString text);
|
||||
void show_unsupported_dlg(int code);
|
||||
int parse_msg_count = 0;
|
||||
int keep_alive_count = 0;
|
||||
std::chrono::system_clock::time_point last_update_time; /* last received print data from machine */
|
||||
@@ -537,7 +541,6 @@ public:
|
||||
bool xcam_first_layer_inspector { false };
|
||||
time_t xcam_first_layer_hold_start = 0;
|
||||
std::string local_rtsp_url;
|
||||
std::string webcam_stream_url;
|
||||
std::string tutk_state;
|
||||
enum LiveviewLocal {
|
||||
LVL_None,
|
||||
@@ -699,8 +702,6 @@ public:
|
||||
std::string subtask_id_;
|
||||
std::string job_id_;
|
||||
std::string last_subtask_id_;
|
||||
// note: printer-agent-supplied thumbnail url, empty when the agent supplies none.
|
||||
std::string m_agent_thumbnail_url;
|
||||
BBLSliceInfo* slice_info {nullptr};
|
||||
boost::thread* get_slice_info_thread { nullptr };
|
||||
boost::thread* get_model_task_thread { nullptr };
|
||||
@@ -806,7 +807,6 @@ public:
|
||||
int command_ams_select_tray(std::string tray_id);
|
||||
int command_ams_refresh_rfid(std::string tray_id);
|
||||
int command_ams_refresh_rfid2(int ams_id, int slot_id);
|
||||
int command_start_camera();
|
||||
int command_ams_control(std::string action);
|
||||
int command_ams_drying_stop();
|
||||
int command_start_extrusion_cali(int tray_index, int nozzle_temp, int bed_temp, float max_volumetric_speed, std::string setting_id = "");
|
||||
@@ -888,7 +888,6 @@ public:
|
||||
static bool is_in_printing_status(std::string status);
|
||||
|
||||
void set_print_state(std::string status);
|
||||
void update_print_progress(const json& value);
|
||||
|
||||
bool is_connected();
|
||||
bool is_connecting();
|
||||
|
||||
@@ -2166,12 +2166,7 @@ void GUI_App::init_networking_callbacks()
|
||||
obj->is_tunnel_mqtt = tunnel;
|
||||
obj->command_request_push_all(true);
|
||||
obj->command_get_version();
|
||||
// Do NOT erase the access code. Erasing will cause has_access_right to be false
|
||||
// whenever the device slot isn't populated yet (e.g. LAN reselect after logout).
|
||||
// This filters this printer out of get_my_machine_list, silently dropping every status message
|
||||
// AND the get_access_code reply that would refill the code, leaving a permanently
|
||||
// dead "connected but no live data" state.
|
||||
// obj -> set_access_code("");
|
||||
obj->erase_user_access_code();
|
||||
obj->command_get_access_code();
|
||||
if (m_agent)
|
||||
m_agent->install_device_cert(obj->get_dev_id(), obj->is_lan_mode_printer());
|
||||
@@ -2221,6 +2216,7 @@ void GUI_App::init_networking_callbacks()
|
||||
wxString text;
|
||||
if (msg == "5") {
|
||||
obj->set_access_code("");
|
||||
obj->erase_user_access_code();
|
||||
text = wxString::Format(_L("Incorrect password"));
|
||||
wxGetApp().show_dialog(text);
|
||||
} else {
|
||||
@@ -8290,7 +8286,7 @@ bool GUI_App::show_modal_ip_address_enter_dialog(bool input_sn, wxString title)
|
||||
wxGetApp().app_config->save();
|
||||
|
||||
obj->set_dev_ip(ip_address.ToStdString());
|
||||
obj->set_access_code(access_code.ToStdString());
|
||||
obj->set_user_access_code(access_code.ToStdString());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -11,7 +11,6 @@ namespace Slic3r
|
||||
// IMPORTANT: ordinal order is the Plugins dialog Status sort priority.
|
||||
Activated,
|
||||
Error,
|
||||
RuntimeError,
|
||||
Inactive,
|
||||
Loading
|
||||
};
|
||||
@@ -22,28 +21,11 @@ namespace Slic3r
|
||||
{
|
||||
case PluginStatus::Activated: return "Activated";
|
||||
case PluginStatus::Error: return "Error";
|
||||
case PluginStatus::RuntimeError: return "RuntimeError";
|
||||
case PluginStatus::Inactive: return "Inactive";
|
||||
case PluginStatus::Loading: return "Loading";
|
||||
}
|
||||
|
||||
return "Inactive";
|
||||
}
|
||||
|
||||
// why: a plugin whose module is live but whose catalog carries an error is a
|
||||
// RUNTIME fault (e.g. a capability rejected at register time) - it stays
|
||||
// loaded/checked and is only flagged, distinct from a load-time Error where
|
||||
// the module never came up. Loading wins over both so an in-flight reload
|
||||
// never flashes an error.
|
||||
inline PluginStatus resolve_plugin_status(bool loading, bool has_error, bool is_loaded)
|
||||
{
|
||||
if (loading)
|
||||
return PluginStatus::Loading;
|
||||
if (has_error)
|
||||
return is_loaded ? PluginStatus::RuntimeError : PluginStatus::Error;
|
||||
if (is_loaded)
|
||||
return PluginStatus::Activated;
|
||||
return PluginStatus::Inactive;
|
||||
}
|
||||
}
|
||||
} // namespace Slic3r::GUI
|
||||
|
||||
@@ -236,7 +236,6 @@ nlohmann::json build_plugin_payload_item(const PluginDialogItem& dialog_item)
|
||||
payload_item["label"] = dialog_item.display_name;
|
||||
payload_item["source"] = to_string(dialog_item.source);
|
||||
payload_item["status"] = to_string(dialog_item.status);
|
||||
payload_item["is_loaded"] = dialog_item.is_loaded;
|
||||
payload_item["error"] = dialog_item.error_text;
|
||||
payload_item["update_status"] = to_string(dialog_item.update_status);
|
||||
payload_item["unauthorized"] = dialog_item.unauthorized;
|
||||
@@ -381,7 +380,14 @@ PluginDialogItem build_plugin_dialog_item(const PluginDescriptor& descriptor)
|
||||
item.sharing_token = descriptor.sharing_token;
|
||||
item.thumbnail_url = descriptor.thumbnail_url;
|
||||
|
||||
item.status = resolve_plugin_status(item.loading, item.has_error, item.is_loaded);
|
||||
if (item.loading)
|
||||
item.status = PluginStatus::Loading;
|
||||
else if (item.has_error)
|
||||
item.status = PluginStatus::Error;
|
||||
else if (item.is_loaded)
|
||||
item.status = PluginStatus::Activated;
|
||||
else
|
||||
item.status = PluginStatus::Inactive;
|
||||
|
||||
item.available_actions = evaluate_action_policy(item);
|
||||
const bool has_enabled_script = std::any_of(item.capabilities.begin(), item.capabilities.end(),
|
||||
@@ -657,9 +663,6 @@ void PluginsDialog::toggle_plugin(const std::string& plugin_key, bool enabled)
|
||||
}
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << "Plugin unloaded from Plugins dialog: " << plugin_key;
|
||||
// A user-disabled plugin has no meaningful error state.
|
||||
if (!manager.clear_plugin_error(plugin_key))
|
||||
BOOST_LOG_TRIVIAL(warning) << "Failed to clear plugin error for " << plugin_key << " (failed to find)";
|
||||
// A prior activation of this plugin is moot now; drop it so no stale "Activated" arrives later.
|
||||
if (m_activating_plugin_key == plugin_key)
|
||||
m_activating_plugin_key.clear();
|
||||
|
||||
@@ -1991,7 +1991,7 @@ void InputIpAddressDialog::workerThreadFunc(std::string str_ip, std::string str_
|
||||
if (w.expired()) return;
|
||||
|
||||
if (m_obj) {
|
||||
m_obj->set_access_code(str_access_code);
|
||||
m_obj->set_user_access_code(str_access_code);
|
||||
wxGetApp().getDeviceManager()->set_selected_machine(m_obj->get_dev_id());
|
||||
}
|
||||
|
||||
|
||||
@@ -3695,32 +3695,10 @@ void SelectMachineDialog::on_send_print()
|
||||
m_print_job->on_success([this]() { finish_mode(); });
|
||||
|
||||
m_print_job->on_check_ip_address_fail([this]() {
|
||||
// Invoked from the PrintJob worker thread when the LAN pre-flight (file upload
|
||||
// verification) fails. Marshal device/UI access to the main thread.
|
||||
CallAfter([this]()
|
||||
{
|
||||
// Reset the dialog out of sending mode so the user can retry.
|
||||
wxCommandEvent* evt = new wxCommandEvent(EVT_CLEAR_IPADDRESS);
|
||||
wxQueueEvent(this, evt);
|
||||
|
||||
DeviceManager* dev = wxGetApp().getDeviceManager();
|
||||
MachineObject* obj = dev ? dev->get_selected_machine() : nullptr;
|
||||
|
||||
if (obj && obj->is_connected())
|
||||
{
|
||||
// Connected: failed on file upload
|
||||
MessageDialog dlg(this,
|
||||
_L("Failed to upload the file to the printer's storage. Please try again."),
|
||||
_L("Send Failed"), wxOK | wxICON_ERROR);
|
||||
dlg.ShowModal();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Not connected: reenter ip and access code
|
||||
wxGetApp().show_ip_address_enter_dialog();
|
||||
}
|
||||
});
|
||||
});
|
||||
wxCommandEvent* evt = new wxCommandEvent(EVT_CLEAR_IPADDRESS);
|
||||
wxQueueEvent(this, evt);
|
||||
wxGetApp().show_ip_address_enter_dialog();
|
||||
});
|
||||
|
||||
// update ota version
|
||||
NetworkAgent* agent = wxGetApp().getAgent();
|
||||
@@ -4551,13 +4529,11 @@ bool SelectMachineDialog::CheckErrorExtruderNozzleWithSlicing(MachineObject* obj
|
||||
|
||||
// check nozzle data valid
|
||||
{
|
||||
// Commented out the following as ntUndefine and 0.0f are default values
|
||||
// (signifying that the value is not given) that should PASS, not fail
|
||||
// if (installed_ext_nozzle.GetNozzleType() == NozzleType::ntUndefine ||
|
||||
// installed_ext_nozzle.GetNozzleDiameter() <= 0.0f) {
|
||||
// show_status(PrintDialogStatus::PrintStatusNozzleDataInvalid);
|
||||
// return false;
|
||||
// }
|
||||
if (installed_ext_nozzle.GetNozzleType() == NozzleType::ntUndefine ||
|
||||
installed_ext_nozzle.GetNozzleDiameter() <= 0.0f) {
|
||||
show_status(PrintDialogStatus::PrintStatusNozzleDataInvalid);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (obj_->is_nozzle_flow_type_supported() &&
|
||||
installed_ext_nozzle.GetNozzleFlowType() == NozzleFlowType::NONE_FLOWTYPE) {
|
||||
@@ -4586,10 +4562,7 @@ bool SelectMachineDialog::CheckErrorExtruderNozzleWithSlicing(MachineObject* obj
|
||||
|
||||
// check nozzle diameter
|
||||
{
|
||||
// 0.0f is default when there is no nozzle diameter is given.
|
||||
// In nozzle_diameter == 0.0f case, it passes and does not require a comparison
|
||||
if (installed_ext_nozzle.GetNozzleDiameter() > 0.0f &&
|
||||
slicing_ext.nozzle_diameter != installed_ext_nozzle.GetNozzleDiameter()) {
|
||||
if (slicing_ext.nozzle_diameter != installed_ext_nozzle.GetNozzleDiameter()) {
|
||||
std::vector<wxString> msg_params;
|
||||
if (ext_sys->GetTotalExtderCount() == 2) {
|
||||
const wxString& mismatch_nozzle_str = _get_nozzle_name(ext_sys->GetTotalExtderCount(), slicing_ext_idx);
|
||||
|
||||
@@ -704,6 +704,7 @@ void SelectMachinePopup::update_user_devices()
|
||||
}
|
||||
|
||||
mobj->set_access_code("");
|
||||
mobj->erase_user_access_code();
|
||||
}
|
||||
|
||||
if (GUI::wxGetApp().plater())
|
||||
|
||||
@@ -300,7 +300,7 @@ SendToPrinterDialog::SendToPrinterDialog(Plater *plater)
|
||||
m_storage_panel->Layout();
|
||||
|
||||
// try to connect
|
||||
m_statictext_printer_msg = new wxStaticText(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize(FromDIP(400), -1), wxALIGN_CENTER_HORIZONTAL);
|
||||
m_statictext_printer_msg = new wxStaticText(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxALIGN_CENTER_HORIZONTAL);
|
||||
m_statictext_printer_msg->SetFont(::Label::Body_13);
|
||||
m_statictext_printer_msg->SetForegroundColour(*wxBLACK);
|
||||
m_statictext_printer_msg->Hide();
|
||||
@@ -760,25 +760,9 @@ void SendToPrinterDialog::update_priner_status_msg(wxString msg, bool is_warning
|
||||
if (str_new != str_old) {
|
||||
if (m_statictext_printer_msg->GetLabel() != msg) {
|
||||
m_statictext_printer_msg->SetLabel(msg);
|
||||
const int wrap_width = FromDIP(400);
|
||||
m_statictext_printer_msg->Wrap(wrap_width);
|
||||
int line_count = 1;
|
||||
const wxString wrapped_label = m_statictext_printer_msg->GetLabel();
|
||||
for (size_t i = 0; i < wrapped_label.length(); ++i) {
|
||||
if (wrapped_label[i] == '\n')
|
||||
++line_count;
|
||||
}
|
||||
wxCoord text_width = 0;
|
||||
wxCoord text_height = 0;
|
||||
m_statictext_printer_msg->GetTextExtent(msg, &text_width, &text_height);
|
||||
const int extent_line_count = text_width > 0 ?
|
||||
std::max(1, (static_cast<int>(text_width) + wrap_width - 1) / wrap_width) : 1;
|
||||
line_count = std::max(line_count, extent_line_count);
|
||||
const int line_height = std::max(m_statictext_printer_msg->GetCharHeight(), static_cast<int>(text_height));
|
||||
const int min_height = std::max(m_statictext_printer_msg->GetBestSize().GetHeight(),
|
||||
line_count * line_height + FromDIP(2));
|
||||
m_statictext_printer_msg->SetMinSize(wxSize(wrap_width, min_height));
|
||||
m_statictext_printer_msg->SetMaxSize(wxDefaultSize);
|
||||
m_statictext_printer_msg->SetMinSize(wxSize(FromDIP(400), -1));
|
||||
m_statictext_printer_msg->SetMaxSize(wxSize(FromDIP(400), -1));
|
||||
m_statictext_printer_msg->Wrap(FromDIP(400));
|
||||
m_statictext_printer_msg->Show();
|
||||
Layout();
|
||||
Fit();
|
||||
@@ -1504,9 +1488,6 @@ void SendToPrinterDialog::show_status(PrintDialogStatus status, std::vector<wxSt
|
||||
Enable_Send_Button(false);
|
||||
Enable_Refresh_Button(true);
|
||||
} else if (status == PrintDialogStatus::PrintStatusPublicInitFailed) {
|
||||
wxString msg_text = _L(
|
||||
"Failed to initialize the printer file transfer. Please check the connection and try again.");
|
||||
update_print_status_msg(msg_text, true, true);
|
||||
Enable_Send_Button(false);
|
||||
Enable_Refresh_Button(true);
|
||||
} else if (status == PrintDialogStatus::PrintStatusPublicUploadFiled) {
|
||||
@@ -1684,18 +1665,30 @@ extern void refresh_agora_url(char const *device, char const *dev_ver, char
|
||||
void SendToPrinterDialog::GetConnection()
|
||||
{
|
||||
DeviceManager *dm = GUI::wxGetApp().getDeviceManager();
|
||||
MachineObject *obj = dm ? dm->get_selected_machine() : nullptr;
|
||||
|
||||
if (!obj)
|
||||
MachineObject *obj = dm->get_selected_machine();
|
||||
if (obj == nullptr) {
|
||||
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " : obj is empty";
|
||||
if (obj && !obj->get_file_remote())
|
||||
m_connection_status = ConnectionStatus::NOT_START;
|
||||
}
|
||||
|
||||
int remote_proto = obj->get_file_remote();
|
||||
if (!remote_proto) {
|
||||
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " : remote_proto is not support";
|
||||
if (obj && obj->is_camera_busy_off())
|
||||
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " : camera is busy";
|
||||
m_connection_status = ConnectionStatus::NOT_START;
|
||||
}
|
||||
|
||||
NetworkAgent* agent = wxGetApp().getAgent();
|
||||
if (obj->is_camera_busy_off()) {
|
||||
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " : camera is busy";
|
||||
m_connection_status = ConnectionStatus::NOT_START;
|
||||
}
|
||||
|
||||
if (m_url_timer && m_url_timer->IsRunning())
|
||||
NetworkAgent *agent = wxGetApp().getAgent();
|
||||
std::string agent_version = agent ? agent->get_version() : "";
|
||||
std::string dev_ver = obj->get_ota_version();
|
||||
std::string dev_id = obj->get_dev_id();
|
||||
|
||||
if (m_url_timer && m_url_timer->IsRunning())
|
||||
{
|
||||
m_url_timer->Stop();
|
||||
}
|
||||
@@ -1718,40 +1711,19 @@ void SendToPrinterDialog::GetConnection()
|
||||
m_url_timer->GetId());
|
||||
m_url_timer->StartOnce(8000);
|
||||
|
||||
if (obj && agent)
|
||||
{
|
||||
std::string dev_ver = obj->get_ota_version();
|
||||
std::string dev_id = obj->get_dev_id();
|
||||
|
||||
if (agent) {
|
||||
if (m_tcp_try_connect) {
|
||||
std::string devIP = obj->get_dev_ip();
|
||||
std::string accessCode = obj->get_access_code();
|
||||
std::string url = "bambu:///local/" + devIP + "?port=6000&user=" + "bblp" + "&passwd=" + accessCode;
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": Connect method tcp, dev_id=" << dev_id
|
||||
<< ", dev_ip=" << devIP << ", access_code_len=" << accessCode.size();
|
||||
|
||||
try
|
||||
{
|
||||
m_filetransfer_tunnel = std::make_unique<FileTransferTunnel>(module(), url);
|
||||
m_filetransfer_tunnel->on_connection([this](bool is_success, int err_code, std::string error_msg)
|
||||
{
|
||||
CallAfter([this, is_success, err_code, error_msg]()
|
||||
{
|
||||
OnConnection(is_success, err_code, error_msg);
|
||||
});
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": Connect method tcp";
|
||||
m_filetransfer_tunnel = std::make_unique<FileTransferTunnel>(module(), url);
|
||||
m_filetransfer_tunnel->on_connection([this](bool is_success, int err_code, std::string error_msg) {
|
||||
CallAfter([this, is_success, err_code, error_msg]() {
|
||||
OnConnection(is_success, err_code, error_msg);
|
||||
});
|
||||
m_filetransfer_tunnel->start_connect();
|
||||
}
|
||||
catch (const std::exception& e)
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": tcp FileTransferTunnel unavailable for dev_id=" <<
|
||||
dev_id
|
||||
<< " dev_ip=" << devIP << ": " << e.what();
|
||||
if (m_url_timer && m_url_timer->IsRunning()) m_url_timer->Stop();
|
||||
m_filetransfer_tunnel.reset();
|
||||
m_connection_status = ConnectionStatus::CONNECTION_FAILED;
|
||||
show_status(PrintDialogStatus::PrintStatusPublicInitFailed);
|
||||
}
|
||||
});
|
||||
m_filetransfer_tunnel->start_connect();
|
||||
}
|
||||
else if (m_tutk_try_connect)
|
||||
{
|
||||
@@ -1779,28 +1751,11 @@ void SendToPrinterDialog::GetConnection()
|
||||
if (boost::algorithm::starts_with(url, "bambu:///"))
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": Connect method tutk";
|
||||
try
|
||||
{
|
||||
m_filetransfer_tunnel = std::make_unique<FileTransferTunnel>(module(), url);
|
||||
m_filetransfer_tunnel->on_connection(
|
||||
[this](bool is_success, int err_code, std::string error_msg)
|
||||
{
|
||||
CallAfter([this, is_success, err_code, error_msg]()
|
||||
{
|
||||
OnConnection(is_success, err_code, error_msg);
|
||||
});
|
||||
});
|
||||
m_filetransfer_tunnel->start_connect();
|
||||
}
|
||||
catch (const std::exception& e)
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": tutk FileTransferTunnel unavailable: " << e.
|
||||
what();
|
||||
if (m_url_timer && m_url_timer->IsRunning()) m_url_timer->Stop();
|
||||
m_filetransfer_tunnel.reset();
|
||||
m_connection_status = ConnectionStatus::CONNECTION_FAILED;
|
||||
show_status(PrintDialogStatus::PrintStatusPublicInitFailed);
|
||||
}
|
||||
m_filetransfer_tunnel = std::make_unique<FileTransferTunnel>(module(), url);
|
||||
m_filetransfer_tunnel->on_connection([this](bool is_success, int err_code, std::string error_msg) {
|
||||
CallAfter([this, is_success, err_code, error_msg]() { OnConnection(is_success, err_code, error_msg); });
|
||||
});
|
||||
m_filetransfer_tunnel->start_connect();
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -1899,17 +1854,8 @@ void SendToPrinterDialog::ResetTunnelAndJob()
|
||||
|
||||
void SendToPrinterDialog::CreateMediaAbilityJob()
|
||||
{
|
||||
nlohmann::json media_ability = {{"cmd_type", 7}};
|
||||
try
|
||||
{
|
||||
m_filetransfer_mediability_job = std::make_unique<FileTransferJob>(module(), std::string(media_ability.dump()));
|
||||
}
|
||||
catch (const std::exception& e)
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": FileTransferJob unavailable: " << e.what();
|
||||
show_status(PrintDialogStatus::PrintStatusPublicInitFailed);
|
||||
return;
|
||||
}
|
||||
nlohmann::json media_ability = {{"cmd_type", 7}};
|
||||
m_filetransfer_mediability_job = std::make_unique<FileTransferJob>(module(), std::string(media_ability.dump()));
|
||||
m_filetransfer_mediability_job->on_result([this](int res, int resp_ec, std::string json_res, std::vector<std::byte> bin_res) {
|
||||
//this pl
|
||||
CallAfter([this, res, resp_ec, json_res] {
|
||||
@@ -1964,20 +1910,11 @@ void SendToPrinterDialog::CreateUploadFileJob(const std::string &path, const std
|
||||
{"cmd_type", 5},
|
||||
};
|
||||
upload_params["dest_storage"] = m_selected_storage;
|
||||
upload_params["dest_name"] = name; // filenme no path
|
||||
upload_params["file_path"] = path;
|
||||
upload_params["dest_name"] = name; // filenme no path
|
||||
upload_params["file_path"] = path;
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": Begin CreateUploadFileJob";
|
||||
try
|
||||
{
|
||||
m_filetransfer_uploadfile_job = std::make_unique<FileTransferJob>(module(), std::string(upload_params.dump()));
|
||||
}
|
||||
catch (const std::exception& e)
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": FileTransferJob unavailable: " << e.what();
|
||||
show_status(PrintDialogStatus::PrintStatusPublicUploadFiled);
|
||||
return;
|
||||
}
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": Begin CreateUploadFileJob";
|
||||
m_filetransfer_uploadfile_job = std::make_unique<FileTransferJob>(module(), std::string(upload_params.dump()));
|
||||
m_filetransfer_uploadfile_job->on_result([this](int res, int resp_ec, std::string json_res, std::vector<std::byte> bin_res) { //
|
||||
CallAfter([this, res, resp_ec, json_res, bin_res] {
|
||||
UploadFileRessultCallback(res, resp_ec,json_res, bin_res);
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
|
||||
#include "MsgDialog.hpp"
|
||||
#include "slic3r/Utils/Http.hpp"
|
||||
#include "slic3r/Utils/MoonrakerPrinterAgent.hpp"
|
||||
#include "libslic3r/Thread.hpp"
|
||||
#include "DeviceErrorDialog.hpp"
|
||||
|
||||
@@ -2312,40 +2311,6 @@ void StatusPanel::update_camera_state(MachineObject* obj)
|
||||
{
|
||||
if (!obj) return;
|
||||
|
||||
const bool has_printer_webcam = !obj->webcam_stream_url.empty();
|
||||
if (has_printer_webcam) {
|
||||
if (m_printer_webcam_url != obj->webcam_stream_url) {
|
||||
// why: start timing belongs to the loaded camera.
|
||||
// carrying it across printers suppresses the new camera's initial start.
|
||||
m_camera_start_sent = std::chrono::steady_clock::time_point{};
|
||||
m_custom_camera_view->LoadURL(obj->webcam_stream_url);
|
||||
m_custom_camera_view->Show();
|
||||
m_media_ctrl->Hide();
|
||||
m_media_play_ctrl->Hide();
|
||||
m_printer_webcam_url = obj->webcam_stream_url;
|
||||
}
|
||||
m_camera_switch_button->Hide();
|
||||
if (!m_custom_camera_view->IsShown()) {
|
||||
// why: do not compare or reload the WebView URL per tick, or redirects can cause a reload loop.
|
||||
m_custom_camera_view->Show();
|
||||
m_media_ctrl->Hide();
|
||||
m_media_play_ctrl->Hide();
|
||||
}
|
||||
// why: printers like the U1 capture only while asked and retire the capture task ~362 s
|
||||
// after each start, so the open camera view has to renew ahead of that. 300 s matches
|
||||
// Snapmaker's own client. Agents that do not need it refuse the call silently.
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
if (m_camera_start_sent == std::chrono::steady_clock::time_point{} ||
|
||||
now - m_camera_start_sent >= std::chrono::seconds(300)) {
|
||||
obj->command_start_camera();
|
||||
m_camera_start_sent = now;
|
||||
}
|
||||
} else if (!m_printer_webcam_url.empty()) {
|
||||
handle_camera_source_change();
|
||||
m_printer_webcam_url.clear();
|
||||
m_camera_start_sent = std::chrono::steady_clock::time_point{};
|
||||
}
|
||||
|
||||
//sdcard
|
||||
auto sdcard_state = obj->GetStorage()->get_sdcard_state();
|
||||
if (m_last_sdcard != sdcard_state) {
|
||||
@@ -2377,12 +2342,7 @@ void StatusPanel::update_camera_state(MachineObject* obj)
|
||||
m_last_recording = obj->is_recording() ? 1 : 0;
|
||||
}
|
||||
|
||||
if (has_printer_webcam) {
|
||||
if (m_bitmap_recording_img->IsShown()) {
|
||||
m_bitmap_recording_img->Hide();
|
||||
m_panel_monitoring_title->Layout();
|
||||
}
|
||||
} else if (!m_bitmap_recording_img->IsShown()) {
|
||||
if (!m_bitmap_recording_img->IsShown()) {
|
||||
m_bitmap_recording_img->Show();
|
||||
m_panel_monitoring_title->Layout();
|
||||
}
|
||||
@@ -2439,8 +2399,6 @@ void StatusPanel::update_camera_state(MachineObject* obj)
|
||||
bool show_vcamera = m_media_play_ctrl->IsStreaming();
|
||||
m_camera_popup->update(show_vcamera);
|
||||
}
|
||||
|
||||
m_setting_button->Show(!has_printer_webcam);
|
||||
}
|
||||
|
||||
StatusPanel::StatusPanel(wxWindow *parent, wxWindowID id, const wxPoint &pos, const wxSize &size, long style, const wxString &name)
|
||||
@@ -2728,8 +2686,7 @@ void StatusPanel::on_subtask_partskip(wxCommandEvent &event)
|
||||
void StatusPanel::on_subtask_pause_resume(wxCommandEvent &event)
|
||||
{
|
||||
if (obj) {
|
||||
const bool was_resume = obj->can_resume();
|
||||
if (was_resume) {
|
||||
if (obj->can_resume()) {
|
||||
BOOST_LOG_TRIVIAL(info) << "monitor: resume current print task dev_id =" << obj->get_dev_id();
|
||||
obj->command_task_resume();
|
||||
}
|
||||
@@ -2737,13 +2694,6 @@ void StatusPanel::on_subtask_pause_resume(wxCommandEvent &event)
|
||||
BOOST_LOG_TRIVIAL(info) << "monitor: pause current print task dev_id =" << obj->get_dev_id();
|
||||
obj->command_task_pause();
|
||||
}
|
||||
if (is_moonraker_agent()) {
|
||||
m_pause_resume_pending = true;
|
||||
m_pause_resume_was_resume = was_resume;
|
||||
m_pause_resume_deadline = std::chrono::steady_clock::now() + std::chrono::seconds(6);
|
||||
m_pause_resume_machine_id = obj->get_dev_id();
|
||||
m_project_task_panel->enable_pause_resume_button(false, was_resume ? "resume_disable" : "pause_disable");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2755,12 +2705,6 @@ void StatusPanel::on_subtask_abort(wxCommandEvent &event)
|
||||
if (obj) {
|
||||
BOOST_LOG_TRIVIAL(info) << "monitor: stop current print task dev_id =" << obj->get_dev_id();
|
||||
obj->command_task_abort();
|
||||
if (is_moonraker_agent()) {
|
||||
m_abort_pending = true;
|
||||
m_abort_deadline = std::chrono::steady_clock::now() + std::chrono::seconds(6);
|
||||
m_abort_machine_id = obj->get_dev_id();
|
||||
m_project_task_panel->enable_abort_button(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -3734,25 +3678,6 @@ void StatusPanel::update_model_info()
|
||||
void StatusPanel::update_subtask(MachineObject *obj)
|
||||
{
|
||||
if (!obj) return;
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
if (m_pause_resume_pending) {
|
||||
if (!is_moonraker_agent() || m_pause_resume_machine_id != obj->get_dev_id() ||
|
||||
obj->can_resume() != m_pause_resume_was_resume) {
|
||||
m_pause_resume_pending = false;
|
||||
} else if (now >= m_pause_resume_deadline) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "StatusPanel: Moonraker pause/resume command did not change printer state";
|
||||
m_pause_resume_pending = false;
|
||||
}
|
||||
}
|
||||
if (m_abort_pending) {
|
||||
if (!is_moonraker_agent() || m_abort_machine_id != obj->get_dev_id() || obj->print_status == "FAILED" ||
|
||||
obj->print_status == "FINISH" || obj->print_status == "IDLE") {
|
||||
m_abort_pending = false;
|
||||
} else if (now >= m_abort_deadline) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "StatusPanel: Moonraker abort command did not change printer state";
|
||||
m_abort_pending = false;
|
||||
}
|
||||
}
|
||||
if (m_current_print_mode != PRINGINT) {
|
||||
if (calib_bitmap == nullptr) {
|
||||
m_calib_mode = get_obj_calibration_mode(obj, m_calib_method, cali_stage);
|
||||
@@ -3860,12 +3785,10 @@ void StatusPanel::update_subtask(MachineObject *obj)
|
||||
}
|
||||
update_basic_print_data(false);
|
||||
} else {
|
||||
if (!m_pause_resume_pending) {
|
||||
if (obj->can_resume()) {
|
||||
m_project_task_panel->enable_pause_resume_button(true, "resume");
|
||||
} else {
|
||||
m_project_task_panel->enable_pause_resume_button(true, "pause");
|
||||
}
|
||||
if (obj->can_resume()) {
|
||||
m_project_task_panel->enable_pause_resume_button(true, "resume");
|
||||
} else {
|
||||
m_project_task_panel->enable_pause_resume_button(true, "pause");
|
||||
}
|
||||
m_project_task_panel->enable_partskip_button(obj, true);
|
||||
// update printing stage
|
||||
@@ -3922,9 +3845,7 @@ void StatusPanel::update_subtask(MachineObject *obj)
|
||||
m_project_task_panel->market_scoring_hide();
|
||||
}
|
||||
} else { // model printing is not finished, hide scoring page
|
||||
if (!m_abort_pending) {
|
||||
m_project_task_panel->enable_abort_button(true);
|
||||
}
|
||||
m_project_task_panel->enable_abort_button(true);
|
||||
m_project_task_panel->market_scoring_hide();
|
||||
m_project_task_panel->get_request_failed_panel()->Hide();
|
||||
}
|
||||
@@ -3999,61 +3920,39 @@ void StatusPanel::update_cloud_subtask(MachineObject *obj)
|
||||
update_calib_bitmap();
|
||||
if (obj->slice_info) {
|
||||
m_request_url = wxString(obj->slice_info->thumbnail_url);
|
||||
load_thumbnail_from_url(m_request_url, obj);
|
||||
if (!m_request_url.IsEmpty()) {
|
||||
wxImage img;
|
||||
std::map<wxString, wxImage>::iterator it = img_list.find(m_request_url);
|
||||
if (it != img_list.end()) {
|
||||
if (m_current_print_mode != PrintingTaskType::CALIBRATION ||(m_calib_mode == CalibMode::Calib_Flow_Rate && m_calib_method == CalibrationMethod::CALI_METHOD_MANUAL)) {
|
||||
img = it->second;
|
||||
wxImage resize_img = img.Scale(m_project_task_panel->get_bitmap_thumbnail()->GetSize().x, m_project_task_panel->get_bitmap_thumbnail()->GetSize().y);
|
||||
m_project_task_panel->set_thumbnail_img(resize_img, "");
|
||||
m_project_task_panel->set_brightness_value(get_brightness_value(resize_img));
|
||||
}
|
||||
if (this->obj) {
|
||||
m_project_task_panel->set_plate_index(obj->m_plate_index);
|
||||
} else {
|
||||
m_project_task_panel->set_plate_index(-1);
|
||||
}
|
||||
task_thumbnail_state = ThumbnailState::TASK_THUMBNAIL;
|
||||
BOOST_LOG_TRIVIAL(trace) << "web_request: use cache image";
|
||||
} else {
|
||||
web_request = wxWebSession::GetDefault().CreateRequest(this, m_request_url);
|
||||
BOOST_LOG_TRIVIAL(trace) << "monitor: start request thumbnail, url = " << m_request_url;
|
||||
web_request.Start();
|
||||
m_start_loading_thumbnail = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool StatusPanel::load_thumbnail_from_url(const wxString &url, MachineObject *obj)
|
||||
{
|
||||
if (url.IsEmpty())
|
||||
return false;
|
||||
|
||||
wxImage img;
|
||||
std::map<wxString, wxImage>::iterator it = img_list.find(url);
|
||||
if (it != img_list.end()) {
|
||||
if (m_current_print_mode != PrintingTaskType::CALIBRATION ||(m_calib_mode == CalibMode::Calib_Flow_Rate && m_calib_method == CalibrationMethod::CALI_METHOD_MANUAL)) {
|
||||
img = it->second;
|
||||
wxImage resize_img = img.Scale(m_project_task_panel->get_bitmap_thumbnail()->GetSize().x, m_project_task_panel->get_bitmap_thumbnail()->GetSize().y);
|
||||
m_project_task_panel->set_thumbnail_img(resize_img, "");
|
||||
m_project_task_panel->set_brightness_value(get_brightness_value(resize_img));
|
||||
}
|
||||
if (this->obj) {
|
||||
m_project_task_panel->set_plate_index(obj->m_plate_index);
|
||||
} else {
|
||||
m_project_task_panel->set_plate_index(-1);
|
||||
}
|
||||
task_thumbnail_state = ThumbnailState::TASK_THUMBNAIL;
|
||||
BOOST_LOG_TRIVIAL(trace) << "web_request: use cache image";
|
||||
} else {
|
||||
m_request_url = url;
|
||||
web_request = wxWebSession::GetDefault().CreateRequest(this, m_request_url);
|
||||
BOOST_LOG_TRIVIAL(trace) << "monitor: start request thumbnail, url = " << m_request_url;
|
||||
web_request.Start();
|
||||
m_start_loading_thumbnail = false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void StatusPanel::update_sdcard_subtask(MachineObject *obj)
|
||||
{
|
||||
if (!obj) return;
|
||||
|
||||
const wxString thumbnail_url = wxString(obj->m_agent_thumbnail_url);
|
||||
if (!thumbnail_url.IsEmpty()) {
|
||||
// why: Moonraker has no prediction or weight data, so keep it on the sdcard path.
|
||||
if (m_request_url != thumbnail_url || !m_load_sdcard_thumbnail) {
|
||||
if (web_request.IsOk() && web_request.GetState() == wxWebRequest::State_Active)
|
||||
web_request.Cancel();
|
||||
update_calib_bitmap();
|
||||
m_request_url = thumbnail_url;
|
||||
load_thumbnail_from_url(thumbnail_url, obj);
|
||||
m_load_sdcard_thumbnail = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!m_load_sdcard_thumbnail || !m_request_url.IsEmpty()) {
|
||||
if (!m_load_sdcard_thumbnail) {
|
||||
update_calib_bitmap();
|
||||
if (m_current_print_mode != PrintingTaskType::CALIBRATION) {
|
||||
m_project_task_panel->get_bitmap_thumbnail()->SetBitmap(m_thumbnail_sdcard.bmp());
|
||||
@@ -4061,14 +3960,11 @@ void StatusPanel::update_sdcard_subtask(MachineObject *obj)
|
||||
}
|
||||
task_thumbnail_state = ThumbnailState::SDCARD_THUMBNAIL;
|
||||
m_load_sdcard_thumbnail = true;
|
||||
m_request_url.clear();
|
||||
}
|
||||
}
|
||||
|
||||
void StatusPanel::reset_printing_values()
|
||||
{
|
||||
m_pause_resume_pending = false;
|
||||
m_abort_pending = false;
|
||||
m_project_task_panel->enable_partskip_button(nullptr, false);
|
||||
m_project_task_panel->enable_pause_resume_button(false, "pause_disable");
|
||||
m_project_task_panel->enable_abort_button(false);
|
||||
@@ -4094,12 +3990,6 @@ void StatusPanel::reset_printing_values()
|
||||
this->Layout();
|
||||
}
|
||||
|
||||
bool StatusPanel::is_moonraker_agent() const
|
||||
{
|
||||
auto* agent = wxGetApp().getAgent();
|
||||
return agent && std::dynamic_pointer_cast<Slic3r::MoonrakerPrinterAgent>(agent->get_printer_agent()) != nullptr;
|
||||
}
|
||||
|
||||
void StatusPanel::on_axis_ctrl_xy(wxCommandEvent &event)
|
||||
{
|
||||
if (!obj) return;
|
||||
@@ -5281,10 +5171,6 @@ bool StatusPanel::is_stage_list_info_changed(MachineObject *obj)
|
||||
void StatusPanel::set_default()
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(trace) << "status_panel: set_default";
|
||||
if (!m_printer_webcam_url.empty()) {
|
||||
handle_camera_source_change();
|
||||
m_printer_webcam_url.clear();
|
||||
}
|
||||
obj = nullptr;
|
||||
last_subtask = nullptr;
|
||||
last_tray_exist_bits = -1;
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
#include <wx/sizer.h>
|
||||
#include <wx/gbsizer.h>
|
||||
#include <wx/webrequest.h>
|
||||
#include <chrono>
|
||||
#include "wxMediaCtrl2.h"
|
||||
#include "MediaPlayCtrl.h"
|
||||
#include "AMSSetting.hpp"
|
||||
@@ -631,7 +630,6 @@ class StatusPanel : public StatusBasePanel
|
||||
{
|
||||
private:
|
||||
friend class MonitorPanel;
|
||||
bool load_thumbnail_from_url(const wxString &url, MachineObject *obj);
|
||||
|
||||
protected:
|
||||
std::shared_ptr<SliceInfoPopup> m_slice_info_popup;
|
||||
@@ -665,9 +663,6 @@ protected:
|
||||
int m_last_timelapse = -1;
|
||||
int m_last_extrusion = -1;
|
||||
int m_last_vcamera = -1;
|
||||
std::string m_printer_webcam_url;
|
||||
// note: zero = not started; see update_camera_state() for the renew interval.
|
||||
std::chrono::steady_clock::time_point m_camera_start_sent{};
|
||||
int m_model_mall_request_count = 0;
|
||||
bool m_is_load_with_temp = false;
|
||||
json m_rating_result;
|
||||
@@ -691,13 +686,6 @@ protected:
|
||||
CalibrationMethod m_calib_method;
|
||||
int cali_stage;
|
||||
PrintingTaskType m_current_print_mode = PrintingTaskType::NOT_CLEAR;
|
||||
bool m_pause_resume_pending = false;
|
||||
bool m_pause_resume_was_resume = false;
|
||||
std::chrono::steady_clock::time_point m_pause_resume_deadline;
|
||||
std::string m_pause_resume_machine_id;
|
||||
bool m_abort_pending = false;
|
||||
std::chrono::steady_clock::time_point m_abort_deadline;
|
||||
std::string m_abort_machine_id;
|
||||
|
||||
void init_scaled_buttons();
|
||||
void create_tasklist_info();
|
||||
@@ -800,7 +788,6 @@ protected:
|
||||
void update_calib_bitmap();
|
||||
|
||||
void reset_printing_values();
|
||||
bool is_moonraker_agent() const;
|
||||
void on_webrequest_state(wxWebRequestEvent &evt);
|
||||
bool is_task_changed(MachineObject* obj);
|
||||
|
||||
|
||||
@@ -2,9 +2,7 @@
|
||||
#include "BBLNetworkPlugin.hpp"
|
||||
#include "NetworkAgentFactory.hpp"
|
||||
|
||||
#include <boost/format.hpp>
|
||||
#include <boost/log/trivial.hpp>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
@@ -22,65 +20,6 @@ void BBLPrinterAgent::set_cloud_agent(std::shared_ptr<ICloudServiceAgent> cloud)
|
||||
// Communication
|
||||
// ============================================================================
|
||||
|
||||
std::string BBLPrinterAgent::ams_refresh_rfid_gcode(const std::string& tray_id)
|
||||
{
|
||||
return (boost::format("M620 R%1% \n") % tray_id).str();
|
||||
}
|
||||
|
||||
std::string BBLPrinterAgent::ams_calibrate_gcode(int ams_id)
|
||||
{
|
||||
return (boost::format("M620 C%1% \n") % ams_id).str();
|
||||
}
|
||||
|
||||
std::string BBLPrinterAgent::ams_select_tray_gcode(const std::string& tray_id)
|
||||
{
|
||||
return (boost::format("M620 P%1% \n") % tray_id).str();
|
||||
}
|
||||
|
||||
int BBLPrinterAgent::command_ams_refresh_rfid(std::string dev_id, std::string tray_id, int sequence_id, bool lan_mode)
|
||||
{
|
||||
const std::string gcode = ams_refresh_rfid_gcode(tray_id);
|
||||
BOOST_LOG_TRIVIAL(trace) << "ams_debug: gcode_cmd" << gcode;
|
||||
nlohmann::json j;
|
||||
j["print"]["command"] = "gcode_line";
|
||||
j["print"]["param"] = gcode;
|
||||
j["print"]["sequence_id"] = std::to_string(sequence_id);
|
||||
return publish(dev_id, j, lan_mode);
|
||||
}
|
||||
|
||||
int BBLPrinterAgent::command_ams_calibrate(std::string dev_id, int ams_id, int sequence_id, bool lan_mode)
|
||||
{
|
||||
const std::string gcode = ams_calibrate_gcode(ams_id);
|
||||
BOOST_LOG_TRIVIAL(trace) << "ams_debug: gcode_cmd" << gcode;
|
||||
nlohmann::json j;
|
||||
j["print"]["command"] = "gcode_line";
|
||||
j["print"]["param"] = gcode;
|
||||
j["print"]["sequence_id"] = std::to_string(sequence_id);
|
||||
return publish(dev_id, j, lan_mode);
|
||||
}
|
||||
|
||||
int BBLPrinterAgent::command_ams_select_tray(std::string dev_id, std::string tray_id, int sequence_id, bool lan_mode)
|
||||
{
|
||||
const std::string gcode = ams_select_tray_gcode(tray_id);
|
||||
BOOST_LOG_TRIVIAL(trace) << "ams_debug: gcode_cmd" << gcode;
|
||||
nlohmann::json j;
|
||||
j["print"]["command"] = "gcode_line";
|
||||
j["print"]["param"] = gcode;
|
||||
j["print"]["sequence_id"] = std::to_string(sequence_id);
|
||||
return publish(dev_id, j, lan_mode);
|
||||
}
|
||||
|
||||
int BBLPrinterAgent::publish(const std::string& dev_id, const nlohmann::json& j, bool lan_mode)
|
||||
{
|
||||
const int rtn = lan_mode ? send_message_to_printer(dev_id, j.dump(), 0, 0) : send_message(dev_id, j.dump(), 0, 0);
|
||||
if (rtn == 0) {
|
||||
BOOST_LOG_TRIVIAL(info) << "publish_json: " << j.dump() << " code: " << rtn;
|
||||
} else {
|
||||
BOOST_LOG_TRIVIAL(error) << "publish_json: " << j.dump() << " code: " << rtn;
|
||||
}
|
||||
return rtn;
|
||||
}
|
||||
|
||||
int BBLPrinterAgent::send_message(std::string dev_id, std::string json_str, int qos, int flag)
|
||||
{
|
||||
auto& plugin = BBLNetworkPlugin::instance();
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
#include "ICloudServiceAgent.hpp"
|
||||
#include <string>
|
||||
#include <memory>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
@@ -29,12 +28,6 @@ public:
|
||||
|
||||
// Communication
|
||||
int send_message(std::string dev_id, std::string json_str, int qos, int flag) override;
|
||||
static std::string ams_refresh_rfid_gcode(const std::string& tray_id);
|
||||
static std::string ams_calibrate_gcode(int ams_id);
|
||||
static std::string ams_select_tray_gcode(const std::string& tray_id);
|
||||
int command_ams_refresh_rfid(std::string dev_id, std::string tray_id, int sequence_id, bool lan_mode) override;
|
||||
int command_ams_calibrate(std::string dev_id, int ams_id, int sequence_id, bool lan_mode) override;
|
||||
int command_ams_select_tray(std::string dev_id, std::string tray_id, int sequence_id, bool lan_mode) override;
|
||||
int connect_printer(std::string dev_id, std::string dev_ip, std::string username, std::string password, bool use_ssl) override;
|
||||
int disconnect_printer() override;
|
||||
int send_message_to_printer(std::string dev_id, std::string json_str, int qos, int flag) override;
|
||||
@@ -92,9 +85,6 @@ public:
|
||||
FilamentSyncMode get_filament_sync_mode() const override;
|
||||
|
||||
private:
|
||||
// why: the lan/cloud DECISION stays machine-side; keep this mechanical branch in sync with publish_json.
|
||||
int publish(const std::string& dev_id, const nlohmann::json& j, bool lan_mode);
|
||||
|
||||
std::shared_ptr<ICloudServiceAgent> m_cloud_agent;
|
||||
};
|
||||
|
||||
|
||||
@@ -2,13 +2,6 @@
|
||||
#define __I_PRINTER_AGENT_HPP__
|
||||
|
||||
#include "bambu_networking.hpp"
|
||||
// why: these extend the BAMBU_NETWORK_* return space rather than opening a new one - the value
|
||||
// flows through the same int domain callers already compare against BAMBU_NETWORK_SUCCESS.
|
||||
// They live here and not in bambu_networking.hpp because that file is a vendor header replaced
|
||||
// wholesale by header-sync commits (see c09252ce11), which would silently clobber them.
|
||||
// -70xx is free: the vendor occupies -1..-25 and -10xx through -60xx.
|
||||
#define ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED -7010 // no translation exists for this command
|
||||
#define ORCA_NETWORK_ERR_CAP_NOT_AVAILABLE -7020 // a translation exists; this printer lacks the capability
|
||||
#include <string>
|
||||
#include <memory>
|
||||
|
||||
@@ -84,21 +77,6 @@ public:
|
||||
*/
|
||||
virtual int send_message(std::string dev_id, std::string json_str, int qos, int flag) = 0;
|
||||
|
||||
// why: gcode is firmware dialect, not a waist concept - commands whose body is Bambu-dialect
|
||||
// gcode live on the agent that speaks it; the default is an honest refusal that MachineObject's
|
||||
// publish funnel turns into a dialog.
|
||||
virtual int command_ams_refresh_rfid(std::string, std::string, int, bool)
|
||||
{ return ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED; }
|
||||
virtual int command_ams_calibrate(std::string, int, int, bool)
|
||||
{ return ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED; }
|
||||
virtual int command_ams_select_tray(std::string, std::string, int, bool)
|
||||
{ return ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED; }
|
||||
// why: some printers emit camera frames only while explicitly asked, and retire the
|
||||
// capture task on their own - the camera view starts it and renews it. Printers with an
|
||||
// always-on stream need nothing here, hence the honest refusal by default.
|
||||
virtual int command_start_camera(std::string)
|
||||
{ return ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED; }
|
||||
|
||||
/**
|
||||
* Establish a direct LAN connection to a printer.
|
||||
*/
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -9,16 +9,11 @@
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <condition_variable>
|
||||
#include <deque>
|
||||
#include <functional>
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
bool moonraker_is_light_name(const std::string& name);
|
||||
|
||||
class MoonrakerPrinterAgent : public IPrinterAgent
|
||||
{
|
||||
public:
|
||||
@@ -117,7 +112,7 @@ protected:
|
||||
// Helpers
|
||||
bool is_numeric(const std::string& value);
|
||||
std::string normalize_base_url(std::string host, const std::string& port);
|
||||
std::string sanitize_filename(const std::string& filename) const;
|
||||
std::string sanitize_filename(const std::string& filename);
|
||||
std::string join_url(const std::string& base_url, const std::string& path) const;
|
||||
|
||||
// Trim whitespace and convert to uppercase
|
||||
@@ -126,22 +121,6 @@ protected:
|
||||
// Map filament type to OrcaFilamentLibrary preset ID for AMS sync compatibility
|
||||
static std::string map_filament_type_to_generic_id(const std::string& filament_type);
|
||||
|
||||
// Send a G-code script via Moonraker (/printer/gcode/script)
|
||||
bool send_gcode(const std::string& dev_id, const std::string& gcode) const;
|
||||
bool send_gcode(const std::string& dev_id, const std::string& gcode,
|
||||
const std::string& base_url, const std::string& api_key) const;
|
||||
bool post_print_action(const std::string& action) const;
|
||||
bool post_print_action(const std::string& action,
|
||||
const std::string& base_url, const std::string& api_key) const;
|
||||
|
||||
// Send one JSON-RPC call over a short-lived Moonraker websocket. Returns true when the
|
||||
// request was written; it never waits for a reply.
|
||||
bool send_ws_rpc(const std::string& method, const nlohmann::json& params);
|
||||
|
||||
// why: a printer with no /server/webcams/list entry can still name its stream directly;
|
||||
// returning empty (the default) keeps the normal Moonraker discovery path.
|
||||
virtual std::string webcam_stream_override(const std::string& base_url) const { return {}; }
|
||||
|
||||
private:
|
||||
int handle_request(const std::string& dev_id, const std::string& json_str);
|
||||
int send_version_info(const std::string& dev_id);
|
||||
@@ -149,7 +128,7 @@ private:
|
||||
|
||||
bool fetch_object_list(const std::string& base_url, const std::string& api_key, std::set<std::string>& objects, std::string& error) const;
|
||||
bool query_printer_status(const std::string& base_url, const std::string& api_key, nlohmann::json& status, std::string& error) const;
|
||||
bool fetch_webcam_info(const std::string& base_url, const std::string& api_key, uint64_t generation);
|
||||
bool send_gcode(const std::string& dev_id, const std::string& gcode) const;
|
||||
|
||||
void announce_printhost_device();
|
||||
void dispatch_local_connect(int state, const std::string& dev_id, const std::string& msg);
|
||||
@@ -158,8 +137,7 @@ private:
|
||||
void start_status_stream(const std::string& dev_id, const std::string& base_url, const std::string& api_key);
|
||||
void stop_status_stream();
|
||||
void run_status_stream(std::string dev_id, std::string base_url, std::string api_key);
|
||||
void handle_ws_message(std::string dev_id, std::string payload, std::string base_url, std::string api_key);
|
||||
void refresh_thumbnail_url(std::string base_url, std::string api_key);
|
||||
void handle_ws_message(const std::string& dev_id, const std::string& payload);
|
||||
void update_status_cache(const nlohmann::json& updates);
|
||||
nlohmann::json build_print_payload_locked() const;
|
||||
|
||||
@@ -173,10 +151,9 @@ private:
|
||||
const std::string& base_url, const std::string& api_key,
|
||||
OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn);
|
||||
|
||||
// Start a print of a previously uploaded G-code file (path relative to the
|
||||
// Moonraker gcodes root).
|
||||
bool start_print_file(const std::string& base_url, const std::string& api_key,
|
||||
const std::string& filename, std::string& error_msg) const;
|
||||
// JSON-RPC helper
|
||||
bool send_jsonrpc_command(const std::string& base_url, const std::string& api_key,
|
||||
const nlohmann::json& request, std::string& response) const;
|
||||
|
||||
// Connection thread management
|
||||
void perform_connection_async(const std::string& dev_id,
|
||||
@@ -212,15 +189,9 @@ private:
|
||||
|
||||
mutable std::recursive_mutex payload_mutex;
|
||||
nlohmann::json status_cache;
|
||||
// note: guarded by payload_mutex; filled by refresh_thumbnail_url(), empty url = looked up, none found
|
||||
std::string thumbnail_filename;
|
||||
std::string thumbnail_url;
|
||||
std::string webcam_stream_url;
|
||||
unsigned thumbnail_lookup_attempts = 0;
|
||||
|
||||
std::atomic<int> next_jsonrpc_id{1};
|
||||
std::set<std::string> available_objects; // Track for feature detection
|
||||
bool assumed_light_on = false;
|
||||
|
||||
std::atomic<bool> ws_stop{false};
|
||||
std::atomic<bool> ws_reconnect_requested{false}; // Flag to trigger reconnection
|
||||
@@ -236,15 +207,7 @@ private:
|
||||
// Connection thread management
|
||||
std::atomic<uint64_t> connect_generation{0};
|
||||
std::thread connect_thread;
|
||||
mutable std::recursive_mutex connect_mutex;
|
||||
|
||||
void enqueue_command(std::function<void()> fn);
|
||||
void run_command_worker();
|
||||
std::thread cmd_thread;
|
||||
std::deque<std::function<void()>> cmd_queue;
|
||||
std::mutex cmd_mutex;
|
||||
std::condition_variable cmd_cv;
|
||||
bool cmd_stop = false;
|
||||
std::recursive_mutex connect_mutex;
|
||||
};
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
@@ -767,34 +767,6 @@ int NetworkAgent::send_message(std::string dev_id, std::string json_str, int qos
|
||||
return -1;
|
||||
}
|
||||
|
||||
int NetworkAgent::command_ams_refresh_rfid(std::string dev_id, std::string tray_id, int sequence_id, bool lan_mode)
|
||||
{
|
||||
if (m_printer_agent)
|
||||
return m_printer_agent->command_ams_refresh_rfid(dev_id, tray_id, sequence_id, lan_mode);
|
||||
return -1;
|
||||
}
|
||||
|
||||
int NetworkAgent::command_ams_calibrate(std::string dev_id, int ams_id, int sequence_id, bool lan_mode)
|
||||
{
|
||||
if (m_printer_agent)
|
||||
return m_printer_agent->command_ams_calibrate(dev_id, ams_id, sequence_id, lan_mode);
|
||||
return -1;
|
||||
}
|
||||
|
||||
int NetworkAgent::command_ams_select_tray(std::string dev_id, std::string tray_id, int sequence_id, bool lan_mode)
|
||||
{
|
||||
if (m_printer_agent)
|
||||
return m_printer_agent->command_ams_select_tray(dev_id, tray_id, sequence_id, lan_mode);
|
||||
return -1;
|
||||
}
|
||||
|
||||
int NetworkAgent::command_start_camera(std::string dev_id)
|
||||
{
|
||||
if (m_printer_agent)
|
||||
return m_printer_agent->command_start_camera(dev_id);
|
||||
return -1;
|
||||
}
|
||||
|
||||
int NetworkAgent::connect_printer(std::string dev_id, std::string dev_ip, std::string username, std::string password, bool use_ssl)
|
||||
{
|
||||
if (m_printer_agent)
|
||||
|
||||
@@ -142,10 +142,6 @@ public:
|
||||
int set_on_local_message_fn(OnMessageFn fn);
|
||||
int set_server_callback(OnServerErrFn fn);
|
||||
int send_message(std::string dev_id, std::string json_str, int qos, int flag);
|
||||
int command_ams_refresh_rfid(std::string dev_id, std::string tray_id, int sequence_id, bool lan_mode);
|
||||
int command_ams_calibrate(std::string dev_id, int ams_id, int sequence_id, bool lan_mode);
|
||||
int command_ams_select_tray(std::string dev_id, std::string tray_id, int sequence_id, bool lan_mode);
|
||||
int command_start_camera(std::string dev_id);
|
||||
int connect_printer(std::string dev_id, std::string dev_ip, std::string username, std::string password, bool use_ssl);
|
||||
int disconnect_printer();
|
||||
int send_message_to_printer(std::string dev_id, std::string json_str, int qos, int flag);
|
||||
|
||||
@@ -16,10 +16,8 @@
|
||||
#include <mutex>
|
||||
#include <utility>
|
||||
#include <slic3r/GUI/GUI_App.hpp>
|
||||
#include <slic3r/GUI/I18N.hpp>
|
||||
#include <slic3r/plugin/PluginDescriptor.hpp>
|
||||
#include <slic3r/plugin/PythonPluginInterface.hpp>
|
||||
#include <wx/msgdlg.h>
|
||||
|
||||
namespace Slic3r {
|
||||
namespace {
|
||||
@@ -316,21 +314,6 @@ void NetworkAgentFactory::register_python_printer_agent(const std::string& plugi
|
||||
|
||||
std::shared_ptr<IPrinterAgent> cached_agent;
|
||||
|
||||
auto reject_conflicting_capability = [plugin_key, capability_name](const std::string& error_message) {
|
||||
if (!wxTheApp || GUI::wxGetApp().is_closing())
|
||||
return;
|
||||
GUI::wxGetApp().CallAfter([plugin_key, capability_name, error_message]() {
|
||||
if (GUI::wxGetApp().is_closing())
|
||||
return;
|
||||
PluginManager& manager = PluginManager::instance();
|
||||
manager.set_plugin_error(plugin_key, error_message);
|
||||
// note: the unload callback triggered by disabling will call deregister,
|
||||
// which will be a no-op since the printer agent is never registered
|
||||
manager.set_capability_enabled({PluginCapabilityType::PrinterConnection, capability_name, plugin_key}, false);
|
||||
wxMessageBox(wxString::FromUTF8(error_message.c_str()), _L("Plugins"), wxOK | wxICON_WARNING, GUI::wxGetApp().GetTopWindow());
|
||||
});
|
||||
};
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(s_registry_mutex);
|
||||
|
||||
@@ -349,10 +332,9 @@ void NetworkAgentFactory::register_python_printer_agent(const std::string& plugi
|
||||
auto& python_agent_ids = get_python_printer_agent_ids();
|
||||
for (const auto& pair : python_agent_ids) {
|
||||
if (pair.first != capability_key && pair.second == info.id) {
|
||||
const std::string error_message = "Printer-agent '" + info.name + "' could not be enabled: agent ID '" + info.id +
|
||||
"' is already registered by capability '" + pair.first.second + "' from plugin '" + pair.first.first + "'.";
|
||||
BOOST_LOG_TRIVIAL(warning) << error_message;
|
||||
reject_conflicting_capability(error_message);
|
||||
BOOST_LOG_TRIVIAL(warning) << "Printer-agent plugin '" << capability_name << "' uses duplicate agent ID '" << info.id
|
||||
<< "' already registered by capability '" << pair.first.second << "' from plugin '"
|
||||
<< pair.first.first << "'";
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -372,22 +354,12 @@ void NetworkAgentFactory::register_python_printer_agent(const std::string& plugi
|
||||
|
||||
auto& agents = get_printer_agents();
|
||||
auto agent_it = agents.find(info.id);
|
||||
// why: reject only when the ID is owned by SOMEONE ELSE - a built-in has an empty
|
||||
// plugin_identifier, another plugin/capability has a different plugin_full_ref. When it
|
||||
// IS the same plugin_full_ref, this capability is just re-registering itself, so fall
|
||||
// through and refresh.
|
||||
if (agent_it != agents.end() && agent_it->second.plugin_identifier != plugin_full_ref) {
|
||||
const std::string error_message = "Printer-agent '" + info.name + "' could not be enabled: agent ID '" + info.id +
|
||||
"' is already registered by '" + agent_it->second.display_name + "'.";
|
||||
BOOST_LOG_TRIVIAL(warning) << error_message;
|
||||
reject_conflicting_capability(error_message);
|
||||
BOOST_LOG_TRIVIAL(warning) << "Printer-agent plugin '" << capability_name << "' uses agent ID '" << info.id
|
||||
<< "' already registered by '" << agent_it->second.display_name << "'";
|
||||
return;
|
||||
}
|
||||
|
||||
// why: insert_or_assign, not emplace - reaching here means the ID is new, or the same
|
||||
// capability is re-registering (same plugin_full_ref). In the re-register case we WANT to
|
||||
// overwrite so the factory closure points at the current live capability instance; emplace
|
||||
// would silently keep the stale entry.
|
||||
agents.insert_or_assign(info.id, PrinterAgentInfo(info.id, info.name, plugin_full_ref, std::move(factory)));
|
||||
python_agent_ids[capability_key] = info.id;
|
||||
|
||||
|
||||
@@ -74,81 +74,6 @@ bool QidiPrinterAgent::fetch_filament_info(std::string dev_id)
|
||||
return true;
|
||||
}
|
||||
|
||||
bool QidiPrinterAgent::apply_box_mapping(const PrintParams& params) const
|
||||
{
|
||||
// enable_box mirrors task_use_ams: engage the multi-color box only when this
|
||||
// job actually routes filament through it. (See qidi-ams-findings.md §2/§8.3 —
|
||||
// if firmware treats enable_box as "a box exists" rather than "use it this job",
|
||||
// switch this gate to HasAms()/box_count instead.)
|
||||
const int enable = params.task_use_ams ? 1 : 0;
|
||||
if (!send_gcode(device_info.dev_id, "SAVE_VARIABLE VARIABLE=enable_box VALUE=" + std::to_string(enable))) {
|
||||
BOOST_LOG_TRIVIAL(error) << "QidiPrinterAgent::apply_box_mapping: failed to set enable_box";
|
||||
return false;
|
||||
}
|
||||
|
||||
// When the box isn't used this job, leave the existing value_t<tool> slot
|
||||
// assignments untouched (enable_box=0 is enough to disengage it).
|
||||
if (!enable)
|
||||
return true;
|
||||
|
||||
if (params.ams_mapping.empty()) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "QidiPrinterAgent::apply_box_mapping: enable_box set but ams_mapping is empty";
|
||||
return true;
|
||||
}
|
||||
|
||||
// ams_mapping (v0) is a JSON array indexed by filament/tool; each value is the
|
||||
// physical box slot (-1 = unmapped). Mirror it onto the printer's value_t<tool>
|
||||
// variables: SAVE_VARIABLE VARIABLE=value_t<tool> VALUE='slot<n>'.
|
||||
auto mapping = nlohmann::json::parse(params.ams_mapping, nullptr, /*allow_exceptions*/ false);
|
||||
if (mapping.is_discarded() || !mapping.is_array()) {
|
||||
BOOST_LOG_TRIVIAL(error) << "QidiPrinterAgent::apply_box_mapping: invalid ams_mapping: " << params.ams_mapping;
|
||||
return false;
|
||||
}
|
||||
|
||||
for (size_t tool = 0; tool < mapping.size(); ++tool) {
|
||||
if (!mapping[tool].is_number_integer())
|
||||
continue;
|
||||
const int slot = mapping[tool].get<int>();
|
||||
if (slot < 0)
|
||||
continue; // unmapped filament — skip
|
||||
const std::string gcode = "SAVE_VARIABLE VARIABLE=value_t" + std::to_string(tool) +
|
||||
" VALUE=\"'slot" + std::to_string(slot) + "'\"";
|
||||
if (!send_gcode(device_info.dev_id, gcode)) {
|
||||
BOOST_LOG_TRIVIAL(error) << "QidiPrinterAgent::apply_box_mapping: failed to set value_t" << tool;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
int QidiPrinterAgent::start_local_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn)
|
||||
{
|
||||
if (!apply_box_mapping(params))
|
||||
return BAMBU_NETWORK_ERR_PRINT_LP_PUBLISH_MSG_FAILED;
|
||||
return MoonrakerPrinterAgent::start_local_print(std::move(params), update_fn, cancel_fn);
|
||||
}
|
||||
|
||||
int QidiPrinterAgent::start_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn)
|
||||
{
|
||||
if (!apply_box_mapping(params))
|
||||
return BAMBU_NETWORK_ERR_PRINT_LP_PUBLISH_MSG_FAILED;
|
||||
return MoonrakerPrinterAgent::start_print(std::move(params), update_fn, cancel_fn, wait_fn);
|
||||
}
|
||||
|
||||
int QidiPrinterAgent::start_local_print_with_record(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn)
|
||||
{
|
||||
if (!apply_box_mapping(params))
|
||||
return BAMBU_NETWORK_ERR_PRINT_WR_UPLOAD_FTP_FAILED;
|
||||
return MoonrakerPrinterAgent::start_local_print_with_record(std::move(params), update_fn, cancel_fn, wait_fn);
|
||||
}
|
||||
|
||||
int QidiPrinterAgent::start_sdcard_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn)
|
||||
{
|
||||
if (!apply_box_mapping(params))
|
||||
return BAMBU_NETWORK_ERR_PRINT_LP_PUBLISH_MSG_FAILED;
|
||||
return MoonrakerPrinterAgent::start_sdcard_print(std::move(params), update_fn, cancel_fn);
|
||||
}
|
||||
|
||||
bool QidiPrinterAgent::fetch_slot_info(const std::string& base_url,
|
||||
const std::string& api_key,
|
||||
const QidiFilamentDict& dict,
|
||||
@@ -193,10 +118,20 @@ bool QidiPrinterAgent::fetch_slot_info(const std::string& base_url,
|
||||
return false;
|
||||
}
|
||||
|
||||
nlohmann::json status;
|
||||
nlohmann::json variables;
|
||||
if (!parse_slot_response(response_body, status, variables, error))
|
||||
auto json = nlohmann::json::parse(response_body, nullptr, false, true);
|
||||
if (json.is_discarded()) {
|
||||
error = "Invalid JSON response";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!json.contains("result") || !json["result"].contains("status") || !json["result"]["status"].contains("save_variables") ||
|
||||
!json["result"]["status"]["save_variables"].contains("variables")) {
|
||||
error = "Unexpected JSON structure";
|
||||
return false;
|
||||
}
|
||||
|
||||
auto& variables = json["result"]["status"]["save_variables"]["variables"];
|
||||
auto& status = json["result"]["status"];
|
||||
|
||||
box_count = variables.value("box_count", 1);
|
||||
if (box_count < 0) {
|
||||
@@ -271,31 +206,6 @@ bool QidiPrinterAgent::fetch_slot_info(const std::string& base_url,
|
||||
return true;
|
||||
}
|
||||
|
||||
bool QidiPrinterAgent::parse_slot_response(const std::string& response_body,
|
||||
nlohmann::json& status,
|
||||
nlohmann::json& variables,
|
||||
std::string& error)
|
||||
{
|
||||
auto json = nlohmann::json::parse(response_body, nullptr, false, true);
|
||||
if (json.is_discarded()) {
|
||||
error = "Invalid JSON response";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!json.is_object() || !json.contains("result") || !json["result"].is_object() || !json["result"].contains("status") ||
|
||||
!json["result"]["status"].is_object() || !json["result"]["status"].contains("save_variables") ||
|
||||
!json["result"]["status"]["save_variables"].is_object() || !json["result"]["status"]["save_variables"].contains("variables") ||
|
||||
!json["result"]["status"]["save_variables"]["variables"].is_object()) {
|
||||
// why: Qidi firmware may send null here, but json::value() throws for it.
|
||||
error = "Unexpected JSON structure: save_variables.variables must be an object";
|
||||
return false;
|
||||
}
|
||||
|
||||
status = json["result"]["status"];
|
||||
variables = status["save_variables"]["variables"];
|
||||
return true;
|
||||
}
|
||||
|
||||
bool QidiPrinterAgent::fetch_filament_dict(const std::string& base_url,
|
||||
const std::string& api_key,
|
||||
QidiFilamentDict& dict,
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
#define __QIDI_PRINTER_AGENT_HPP__
|
||||
|
||||
#include "MoonrakerPrinterAgent.hpp"
|
||||
#include "nlohmann/json_fwd.hpp"
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
@@ -22,21 +21,7 @@ public:
|
||||
// Override filament sync (Qidi-specific implementation)
|
||||
bool fetch_filament_info(std::string dev_id) override;
|
||||
|
||||
static bool parse_slot_response(const std::string& response_body,
|
||||
nlohmann::json& status,
|
||||
nlohmann::json& variables,
|
||||
std::string& error);
|
||||
|
||||
// Print operations — emit QiDi multi-color box config, then delegate to base.
|
||||
int start_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) override;
|
||||
int start_local_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn) override;
|
||||
int start_local_print_with_record(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) override;
|
||||
int start_sdcard_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn) override;
|
||||
|
||||
private:
|
||||
// Push enable_box + value_t<tool> SAVE_VARIABLEs before a print starts.
|
||||
// Returns false if any command fails (caller should abort the print).
|
||||
bool apply_box_mapping(const PrintParams& params) const;
|
||||
struct QidiFilamentDict
|
||||
{
|
||||
std::map<int, std::string> colors;
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
#include "SnapmakerPrinterAgent.hpp"
|
||||
#include "Http.hpp"
|
||||
#include "libslic3r/PresetBundle.hpp"
|
||||
#include "libslic3r/Utils.hpp"
|
||||
#include "slic3r/GUI/GUI_App.hpp"
|
||||
|
||||
#include "nlohmann/json.hpp"
|
||||
#include <boost/filesystem.hpp>
|
||||
#include <boost/log/trivial.hpp>
|
||||
#include <boost/nowide/fstream.hpp>
|
||||
#include <thread>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
@@ -71,69 +67,6 @@ std::string find_closest_color_preset_by_vendor_and_type(const PresetCollection&
|
||||
|
||||
SnapmakerPrinterAgent::SnapmakerPrinterAgent(std::string log_dir) : MoonrakerPrinterAgent(std::move(log_dir)) {}
|
||||
|
||||
int SnapmakerPrinterAgent::command_start_camera(std::string dev_id)
|
||||
{
|
||||
(void) dev_id;
|
||||
// why: the printer executes this over the websocket but answers only over MQTT, and the
|
||||
// call itself blocks on socket I/O - it fires from the camera view's renew timer on the UI
|
||||
// thread, so run it detached rather than block the caller on a reply that never comes.
|
||||
// note: interval is dead time in SECONDS on top of a ~0.455 s capture, so 0 is the 2.15 fps
|
||||
// ceiling (1 measures 0.63 fps), and it cannot be changed while a capture task is running.
|
||||
std::thread([this] {
|
||||
send_ws_rpc("camera.start_monitor",
|
||||
{{"domain", "lan"}, {"interval", 0}, {"expect_pw", false}});
|
||||
}).detach();
|
||||
return BAMBU_NETWORK_SUCCESS;
|
||||
}
|
||||
|
||||
std::string SnapmakerPrinterAgent::webcam_stream_override(const std::string& base_url) const
|
||||
{
|
||||
const std::string snapshot_url = join_url(base_url, "/server/files/camera/monitor.jpg");
|
||||
|
||||
// why: one wrapper file per printer - two U1s would otherwise overwrite each other's URL.
|
||||
const boost::filesystem::path page = boost::filesystem::path(data_dir()) / "cache" /
|
||||
("snapmaker_camera_" + sanitize_filename(device_info.dev_ip) + ".html");
|
||||
|
||||
// why: the printer writes a still JPEG at ~2 fps, so the page polls it with a cache buster
|
||||
// instead of consuming a stream. Chaining the next request off onload (never a bare
|
||||
// setInterval) keeps requests from piling up when the printer is slow to answer.
|
||||
const std::string html =
|
||||
"<!DOCTYPE html><html><head><meta charset=\"utf-8\"><title>Camera</title><style>"
|
||||
"html,body{margin:0;height:100%;background:#000;overflow:hidden}"
|
||||
"img{width:100%;height:100%;object-fit:contain;display:block}</style></head>"
|
||||
"<body><img id=\"frame\" alt=\"\"><script>\n"
|
||||
"var src=\"" + snapshot_url + "\";\n"
|
||||
"var img=document.getElementById(\"frame\");\n"
|
||||
"function next(){img.src=src+\"?_nocache=\"+Date.now()+\"_\"+Math.floor(Math.random()*10000);}\n"
|
||||
"img.onload=function(){setTimeout(next,250);};\n"
|
||||
"img.onerror=function(){setTimeout(next,1000);};\n"
|
||||
"next();\n"
|
||||
"</script></body></html>\n";
|
||||
|
||||
std::string write_error;
|
||||
try {
|
||||
boost::filesystem::create_directories(page.parent_path());
|
||||
boost::nowide::ofstream out(page.string().c_str(), std::ios::binary | std::ios::trunc);
|
||||
out << html;
|
||||
out.close();
|
||||
// note: an ofstream reports a failed write in its state, not by throwing.
|
||||
if (!out) {
|
||||
write_error = "write failed";
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
write_error = e.what();
|
||||
}
|
||||
if (!write_error.empty()) {
|
||||
// why: no wrapper means no camera - a raw monitor.jpg URL would render one frozen frame
|
||||
// and read as a broken feed, so fall back to showing nothing and say why in the log.
|
||||
BOOST_LOG_TRIVIAL(warning) << "SnapmakerPrinterAgent: could not write camera page " << page.string()
|
||||
<< ": " << write_error;
|
||||
return {};
|
||||
}
|
||||
|
||||
return "file://" + page.generic_string();
|
||||
}
|
||||
|
||||
AgentInfo SnapmakerPrinterAgent::get_agent_info_static()
|
||||
{
|
||||
return AgentInfo{"snapmaker", "Snapmaker", SNAPMAKER_AGENT_VERSION, "Snapmaker printer agent"};
|
||||
|
||||
@@ -16,10 +16,6 @@ public:
|
||||
AgentInfo get_agent_info() override { return get_agent_info_static(); }
|
||||
|
||||
bool fetch_filament_info(std::string dev_id) override;
|
||||
int command_start_camera(std::string dev_id) override;
|
||||
|
||||
protected:
|
||||
std::string webcam_stream_override(const std::string& base_url) const override;
|
||||
|
||||
private:
|
||||
// Combine filament_type + filament_sub_type into a unified type string
|
||||
|
||||
@@ -2,16 +2,12 @@ get_filename_component(_TEST_NAME ${CMAKE_CURRENT_LIST_DIR} NAME)
|
||||
add_executable(${_TEST_NAME}_tests
|
||||
${_TEST_NAME}_tests_main.cpp
|
||||
test_dev_mapping.cpp
|
||||
test_device_progress.cpp
|
||||
test_network_versions.cpp
|
||||
test_action_source.cpp
|
||||
test_plugin_host_api.cpp
|
||||
test_plugin_capability_config.cpp
|
||||
test_plugin_config.cpp
|
||||
test_plugin_capabilities_in_use.cpp
|
||||
test_plugin_status.cpp
|
||||
test_printer_agent.cpp
|
||||
test_qidi_printer_agent.cpp
|
||||
test_plugin_install.cpp
|
||||
test_plugin_lifecycle.cpp
|
||||
test_slicing_pipeline_bindings.cpp
|
||||
@@ -59,22 +55,3 @@ elseif (APPLE)
|
||||
endif()
|
||||
|
||||
orcaslicer_discover_tests(${_TEST_NAME}_tests)
|
||||
|
||||
# why: the loader runs on a detached worker thread, so its Python interpreter
|
||||
# ownership model cannot share the embedded interpreter in the main test binary.
|
||||
add_executable(printer_agent_plugin_tests test_printer_agent_plugin.cpp)
|
||||
|
||||
if (MSVC)
|
||||
target_link_libraries(printer_agent_plugin_tests Setupapi.lib)
|
||||
endif ()
|
||||
|
||||
target_link_libraries(printer_agent_plugin_tests test_common libslic3r_gui libslic3r pybind11::embed Catch2::Catch2)
|
||||
set_property(TARGET printer_agent_plugin_tests PROPERTY FOLDER "tests")
|
||||
|
||||
# why: the existing target stages the complete bundled Python home under
|
||||
# python/, which is the layout PythonInterpreter discovers beside the test exe.
|
||||
add_dependencies(printer_agent_plugin_tests ${_TEST_NAME}_tests)
|
||||
|
||||
orcaslicer_copy_test_dlls()
|
||||
|
||||
orcaslicer_discover_tests(printer_agent_plugin_tests)
|
||||
|
||||
@@ -1,141 +0,0 @@
|
||||
// why: match the GUI include order to avoid rpcndr.h byte/std::byte
|
||||
// ambiguity in the Windows COM headers.
|
||||
// why: wx/timer.h must precede DeviceManager.hpp because
|
||||
// DeviceErrorDialog.hpp uses wxTimerEvent.
|
||||
#ifdef WIN32
|
||||
#ifndef WIN32_LEAN_AND_MEAN
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#endif
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include <Windows.h>
|
||||
#endif
|
||||
|
||||
#include <catch2/catch_all.hpp>
|
||||
|
||||
#include <stdexcept>
|
||||
|
||||
#include <wx/timer.h>
|
||||
|
||||
#include "slic3r/GUI/DeviceManager.hpp"
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
using json = nlohmann::json;
|
||||
using namespace Slic3r;
|
||||
|
||||
TEST_CASE("Integer progress reaches the shared subtask", "[DeviceManager][Progress]")
|
||||
{
|
||||
MachineObject machine(nullptr, nullptr, "test", "test-device", "127.0.0.1");
|
||||
|
||||
machine.update_print_progress(json(37));
|
||||
|
||||
REQUIRE(machine.mc_print_percent == 37);
|
||||
BBLSubTask* subtask = machine.get_subtask();
|
||||
REQUIRE(subtask != nullptr);
|
||||
CHECK(subtask->task_progress == 37);
|
||||
}
|
||||
|
||||
TEST_CASE("String progress reaches the shared subtask", "[DeviceManager][Progress]")
|
||||
{
|
||||
MachineObject machine(nullptr, nullptr, "test", "test-device", "127.0.0.1");
|
||||
|
||||
machine.update_print_progress(json("41"));
|
||||
|
||||
REQUIRE(machine.mc_print_percent == 41);
|
||||
BBLSubTask* subtask = machine.get_subtask();
|
||||
REQUIRE(subtask != nullptr);
|
||||
CHECK(subtask->task_progress == 41);
|
||||
}
|
||||
|
||||
TEST_CASE("Floating-point progress preserves the previous shared value", "[DeviceManager][Progress]")
|
||||
{
|
||||
MachineObject machine(nullptr, nullptr, "test", "test-device", "127.0.0.1");
|
||||
|
||||
machine.update_print_progress(json(29));
|
||||
REQUIRE(machine.mc_print_percent == 29);
|
||||
BBLSubTask* subtask = machine.get_subtask();
|
||||
REQUIRE(subtask != nullptr);
|
||||
REQUIRE(subtask->task_progress == 29);
|
||||
|
||||
machine.update_print_progress(json(29.5));
|
||||
BBLSubTask* current_subtask = machine.get_subtask();
|
||||
REQUIRE(current_subtask != nullptr);
|
||||
REQUIRE(current_subtask == subtask);
|
||||
CHECK(machine.mc_print_percent == 29);
|
||||
CHECK(current_subtask->task_progress == 29);
|
||||
}
|
||||
|
||||
TEST_CASE("Unsupported progress values leave a fresh machine unchanged", "[DeviceManager][Progress]")
|
||||
{
|
||||
SECTION("boolean") {
|
||||
MachineObject machine(nullptr, nullptr, "test", "test-device", "127.0.0.1");
|
||||
REQUIRE(machine.subtask_ == nullptr);
|
||||
|
||||
machine.update_print_progress(json(true));
|
||||
|
||||
CHECK(machine.mc_print_percent == 0);
|
||||
CHECK(machine.subtask_ == nullptr);
|
||||
}
|
||||
|
||||
SECTION("null") {
|
||||
MachineObject machine(nullptr, nullptr, "test", "test-device", "127.0.0.1");
|
||||
REQUIRE(machine.subtask_ == nullptr);
|
||||
|
||||
machine.update_print_progress(json(nullptr));
|
||||
|
||||
CHECK(machine.mc_print_percent == 0);
|
||||
CHECK(machine.subtask_ == nullptr);
|
||||
}
|
||||
|
||||
SECTION("object") {
|
||||
MachineObject machine(nullptr, nullptr, "test", "test-device", "127.0.0.1");
|
||||
REQUIRE(machine.subtask_ == nullptr);
|
||||
|
||||
machine.update_print_progress(json::object());
|
||||
|
||||
CHECK(machine.mc_print_percent == 0);
|
||||
CHECK(machine.subtask_ == nullptr);
|
||||
}
|
||||
|
||||
SECTION("array") {
|
||||
MachineObject machine(nullptr, nullptr, "test", "test-device", "127.0.0.1");
|
||||
REQUIRE(machine.subtask_ == nullptr);
|
||||
|
||||
machine.update_print_progress(json::array());
|
||||
|
||||
CHECK(machine.mc_print_percent == 0);
|
||||
CHECK(machine.subtask_ == nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Malformed string progress leaves a fresh machine unchanged", "[DeviceManager][Progress]")
|
||||
{
|
||||
MachineObject machine(nullptr, nullptr, "test", "test-device", "127.0.0.1");
|
||||
REQUIRE(machine.subtask_ == nullptr);
|
||||
|
||||
CHECK_THROWS_AS(machine.update_print_progress(json("not-a-percent")), std::invalid_argument);
|
||||
CHECK(machine.mc_print_percent == 0);
|
||||
CHECK(machine.subtask_ == nullptr);
|
||||
}
|
||||
|
||||
TEST_CASE("Zero progress replaces active shared progress", "[DeviceManager][Progress]")
|
||||
{
|
||||
MachineObject machine(nullptr, nullptr, "test", "test-device", "127.0.0.1");
|
||||
|
||||
machine.update_print_progress(json(63));
|
||||
BBLSubTask* subtask = machine.get_subtask();
|
||||
REQUIRE(subtask != nullptr);
|
||||
REQUIRE(machine.mc_print_percent == 63);
|
||||
REQUIRE(subtask->task_progress == 63);
|
||||
|
||||
machine.set_print_state("FAILED");
|
||||
machine.update_print_progress(json(0));
|
||||
|
||||
BBLSubTask* current_subtask = machine.get_subtask();
|
||||
REQUIRE(current_subtask != nullptr);
|
||||
REQUIRE(current_subtask == subtask);
|
||||
REQUIRE(machine.mc_print_percent == 0);
|
||||
CHECK(current_subtask->task_progress == 0);
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
#include <catch2/catch_all.hpp>
|
||||
|
||||
#include <slic3r/GUI/PluginStatus.hpp>
|
||||
|
||||
using Slic3r::GUI::PluginStatus;
|
||||
using Slic3r::GUI::resolve_plugin_status;
|
||||
|
||||
TEST_CASE("resolve_plugin_status precedence", "[plugin][status]") {
|
||||
// the new branch: loaded module + error -> runtime fault, not a load failure.
|
||||
REQUIRE(resolve_plugin_status(/*loading*/ false, /*has_error*/ true, /*is_loaded*/ true) == PluginStatus::RuntimeError);
|
||||
|
||||
// error without a live module is a load-time Error.
|
||||
REQUIRE(resolve_plugin_status(false, true, false) == PluginStatus::Error);
|
||||
|
||||
// loading wins over a pending error so a reload never flashes red.
|
||||
REQUIRE(resolve_plugin_status(true, true, true) == PluginStatus::Loading);
|
||||
|
||||
// healthy loaded plugin.
|
||||
REQUIRE(resolve_plugin_status(false, false, true) == PluginStatus::Activated);
|
||||
|
||||
// nothing loaded, no error.
|
||||
REQUIRE(resolve_plugin_status(false, false, false) == PluginStatus::Inactive);
|
||||
}
|
||||
@@ -1,237 +0,0 @@
|
||||
#include <catch2/catch_all.hpp>
|
||||
|
||||
#include <slic3r/Utils/BBLPrinterAgent.hpp>
|
||||
#include <slic3r/Utils/MoonrakerPrinterAgent.hpp>
|
||||
#include <slic3r/Utils/NetworkAgentFactory.hpp>
|
||||
#include <slic3r/plugin/PythonPluginBridge.hpp>
|
||||
|
||||
#include <pybind11/embed.h>
|
||||
#include <pybind11/pybind11.h>
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
using namespace Slic3r;
|
||||
namespace py = pybind11;
|
||||
|
||||
// why: these builders preserve the Bambu firmware dialect byte-for-byte, including its trailing space.
|
||||
TEST_CASE("unit: BBL AMS gcode builders preserve command bytes", "[unit][bbl]")
|
||||
{
|
||||
CHECK(BBLPrinterAgent::ams_refresh_rfid_gcode("123") == "M620 R123 \n");
|
||||
CHECK(BBLPrinterAgent::ams_calibrate_gcode(123) == "M620 C123 \n");
|
||||
CHECK(BBLPrinterAgent::ams_select_tray_gcode("123") == "M620 P123 \n");
|
||||
}
|
||||
|
||||
// why: an agent without a Bambu-dialect translation must refuse these commands before any network or wx path.
|
||||
TEST_CASE("unit: default AMS commands report not supported", "[unit][moonraker]")
|
||||
{
|
||||
MoonrakerPrinterAgent agent("");
|
||||
|
||||
CHECK(agent.command_ams_refresh_rfid("dev", "123", 1, false) == ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED);
|
||||
CHECK(agent.command_ams_calibrate("dev", 1, 2, false) == ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED);
|
||||
CHECK(agent.command_ams_select_tray("dev", "123", 3, false) == ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED);
|
||||
}
|
||||
|
||||
TEST_CASE("unit: Moonraker light name matching", "[unit][moonraker]")
|
||||
{
|
||||
CHECK(moonraker_is_light_name("caselight"));
|
||||
CHECK(moonraker_is_light_name("LED_STRIP"));
|
||||
CHECK_FALSE(moonraker_is_light_name("beeper"));
|
||||
CHECK(moonraker_is_light_name("FLASHLIGHT_SWITCH"));
|
||||
CHECK(moonraker_is_light_name("MODLELIGHT_SWITCH"));
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// UNIT - handle_request's not-supported default.
|
||||
// The agent is the only thing that knows what it can translate, so an untranslated
|
||||
// command has to say so instead of returning success and letting the UI believe the
|
||||
// control worked. Guards the inverse too: the pushing namespace is genuinely
|
||||
// satisfied by the websocket status stream, and it re-fires from the keepalive timer
|
||||
// roughly once a second, so it must stay a success or it would raise a dialog on a
|
||||
// timer. Only branches that touch neither the network nor wx are exercised.
|
||||
// ===========================================================================
|
||||
TEST_CASE("unit: Moonraker reports untranslated commands as not supported", "[unit][moonraker]")
|
||||
{
|
||||
MoonrakerPrinterAgent agent("");
|
||||
|
||||
CHECK(agent.send_message("dev", R"({"print":{"command":"ams_change_filament"}})", 0, 0) ==
|
||||
ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED);
|
||||
CHECK(agent.send_message("dev", R"({"system":{"command":"set_door_stat"}})", 0, 0) ==
|
||||
ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED);
|
||||
CHECK(agent.send_message("dev", R"({"xcam":{"command":"xcam_control_set"}})", 0, 0) ==
|
||||
ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED);
|
||||
|
||||
CHECK(agent.send_message("dev", R"({"pushing":{"command":"pushall"}})", 0, 0) == BAMBU_NETWORK_SUCCESS);
|
||||
CHECK(agent.send_message("dev", R"({"pushing":{"command":"start"}})", 0, 0) == BAMBU_NETWORK_SUCCESS);
|
||||
|
||||
// why: malformed input is a different failure than an untranslated command, and the
|
||||
// default must not swallow it into a misleading not-supported verdict.
|
||||
CHECK(agent.send_message("dev", "{not json", 0, 0) == BAMBU_NETWORK_ERR_INVALID_RESULT);
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// UNIT - printer-agent registry duplicate handling.
|
||||
// Confirms a duplicate agent id is rejected so a plugin cannot shadow a built-in
|
||||
// or previously registered agent.
|
||||
// ===========================================================================
|
||||
TEST_CASE("unit: printer-agent registry register / lookup / duplicate-reject", "[registry][unit]")
|
||||
{
|
||||
// why: the registry is process-global state shared by the test binary, and
|
||||
// Catch2 may run cases in any order. Use an id that cannot collide with
|
||||
// built-ins or other cases. Avoid SECTIONs because each section re-runs the
|
||||
// body and would register the same id twice.
|
||||
const std::string id = "orca-test::registry-probe-7f3a";
|
||||
auto stub_factory = [](std::shared_ptr<ICloudServiceAgent>, const std::string&)
|
||||
-> std::shared_ptr<IPrinterAgent> { return nullptr; };
|
||||
|
||||
REQUIRE_FALSE(NetworkAgentFactory::is_printer_agent_registered(id));
|
||||
|
||||
REQUIRE(NetworkAgentFactory::register_printer_agent(id, "Registry Probe", stub_factory));
|
||||
REQUIRE(NetworkAgentFactory::is_printer_agent_registered(id));
|
||||
|
||||
// Re-registering the same id is rejected and does not replace the entry.
|
||||
REQUIRE_FALSE(NetworkAgentFactory::register_printer_agent(id, "Impostor", stub_factory));
|
||||
|
||||
// The first registration's display name survives the rejected duplicate.
|
||||
const PrinterAgentInfo* info = NetworkAgentFactory::get_printer_agent_info(id);
|
||||
REQUIRE(info != nullptr);
|
||||
CHECK(info->display_name == "Registry Probe");
|
||||
|
||||
// It appears exactly once in the UI-population list.
|
||||
auto agents = NetworkAgentFactory::get_registered_printer_agents();
|
||||
int count = 0;
|
||||
for (const auto& a : agents)
|
||||
if (a.id == id)
|
||||
++count;
|
||||
CHECK(count == 1);
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// INTEGRATION - the orca.printer_agent Python binding surface.
|
||||
// Boots the embedded interpreter and asserts the C++ to Python contract that
|
||||
// every printer-agent plugin subclasses. If a binding is renamed or removed,
|
||||
// plugins fail at runtime even though C++ still compiles.
|
||||
// ===========================================================================
|
||||
namespace {
|
||||
|
||||
void ensure_python_initialized()
|
||||
{
|
||||
// why: the `orca` module is embedded in this binary, so a bare interpreter
|
||||
// can import it without a bundled Python home. The app interpreter expects
|
||||
// that deployed layout, which is not present beside this test binary.
|
||||
if (!Py_IsInitialized()) {
|
||||
static py::scoped_interpreter interpreter;
|
||||
(void) interpreter;
|
||||
}
|
||||
}
|
||||
|
||||
py::module_ import_orca_module()
|
||||
{
|
||||
ensure_python_initialized();
|
||||
// Force PythonPluginBridge.cpp into the binary so the embedded
|
||||
// PYBIND11_EMBEDDED_MODULE(orca, ...) registration (incl. printer_agent) exists.
|
||||
(void) Slic3r::PythonPluginBridge::instance();
|
||||
return py::module_::import("orca");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("integration: orca.printer_agent binding surface", "[integration][Python]")
|
||||
{
|
||||
py::module_ orca = import_orca_module();
|
||||
|
||||
REQUIRE(py::hasattr(orca, "printer_agent"));
|
||||
py::object pa = orca.attr("printer_agent");
|
||||
|
||||
// The base class every printer-agent plugin subclasses.
|
||||
REQUIRE(py::hasattr(pa, "PrinterAgentBase"));
|
||||
py::object base = pa.attr("PrinterAgentBase");
|
||||
for (const char* method : { "get_agent_info", "connect_printer", "disconnect_printer",
|
||||
"send_message", "start_discovery", "bind_detect",
|
||||
"start_print", "get_filament_sync_mode" }) {
|
||||
CAPTURE(method);
|
||||
CHECK(py::hasattr(base, method));
|
||||
}
|
||||
|
||||
// AgentInfo value type - the registry identity the host reads (id is the key).
|
||||
REQUIRE(py::hasattr(pa, "AgentInfo"));
|
||||
py::object info = pa.attr("AgentInfo")("moonraker", "Moonraker", "1.0", "test agent");
|
||||
CHECK(info.attr("id").cast<std::string>() == "moonraker");
|
||||
CHECK(info.attr("name").cast<std::string>() == "Moonraker");
|
||||
|
||||
// FilamentSyncMode enum the host queries to pick pull vs subscription.
|
||||
REQUIRE(py::hasattr(pa, "FilamentSyncMode"));
|
||||
py::object mode = pa.attr("FilamentSyncMode");
|
||||
CHECK(py::hasattr(mode, "Pull"));
|
||||
CHECK(py::hasattr(mode, "Subscription"));
|
||||
CHECK(py::hasattr(mode, "None_"));
|
||||
|
||||
// Plugin-type enum exposed at module root (host reads it without the GIL).
|
||||
CHECK(py::hasattr(orca, "PluginType"));
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// INTEGRATION - plugin-registration API and discovery-context guards.
|
||||
// These are the symbols every plugin package uses: the @orca.plugin decorator,
|
||||
// orca.base, orca.register_capability, and the capability base modules. Checking
|
||||
// them in the lightweight embedded-interpreter test catches binding breakage
|
||||
// before the plugin-loader test needs to run.
|
||||
// ===========================================================================
|
||||
TEST_CASE("integration: orca plugin-registration API surface + discovery-context guards", "[integration][Python]")
|
||||
{
|
||||
py::module_ orca = import_orca_module();
|
||||
|
||||
// Module-level surface every plugin package relies on.
|
||||
// note: no "gcode" module here - this branch has no G-code capability module;
|
||||
// PostProcessing exists only as a PluginType value.
|
||||
for (const char* name : { "plugin", "register_capability", "base", "PythonPluginBase",
|
||||
"PluginType", "PluginResult", "script", "printer_agent", "host" }) {
|
||||
CAPTURE(name);
|
||||
CHECK(py::hasattr(orca, name));
|
||||
}
|
||||
|
||||
// Plugin package base and capability base contract.
|
||||
CHECK(py::hasattr(orca.attr("base"), "register_capabilities"));
|
||||
py::object cap_base = orca.attr("PythonPluginBase");
|
||||
for (const char* method : { "get_name", "get_type", "on_load", "on_unload" }) {
|
||||
CAPTURE(method);
|
||||
CHECK(py::hasattr(cap_base, method));
|
||||
}
|
||||
|
||||
// The script capability module exposes its own base class.
|
||||
CHECK(py::hasattr(orca.attr("script"), "ScriptPluginCapabilityBase"));
|
||||
|
||||
// PluginType enum carries the values that route a capability, including PrinterConnection.
|
||||
// note: no PostProcessing value on this branch's binding.
|
||||
py::object types = orca.attr("PluginType");
|
||||
for (const char* value : { "PrinterConnection", "Script" }) {
|
||||
CAPTURE(value);
|
||||
CHECK(py::hasattr(types, value));
|
||||
}
|
||||
|
||||
// note: this is testing behavior, not normal plugin loading.
|
||||
// These APIs should only work while Orca is actively loading a plugin.
|
||||
try {
|
||||
// Calls Python's orca.register_capability(0) from C++.
|
||||
// 0 is intentionally bogus. The important part is that there is no active
|
||||
// plugin load context, so the function should reject the call immediately.
|
||||
orca.attr("register_capability")(py::int_(0));
|
||||
|
||||
// If the call above does NOT throw, the test fails here.
|
||||
FAIL("register_capability outside discovery context must raise");
|
||||
} catch (const py::error_already_set& error) {
|
||||
// pybind11 wraps Python exceptions as py::error_already_set.
|
||||
// This checks the Python exception type is ValueError.
|
||||
CHECK(error.matches(PyExc_ValueError));
|
||||
}
|
||||
|
||||
try {
|
||||
// This is the function behind @orca.plugin.
|
||||
// Same logic as above.
|
||||
orca.attr("plugin")(py::int_(0));
|
||||
|
||||
FAIL("@orca.plugin outside discovery context must raise");
|
||||
} catch (const py::error_already_set& error) {
|
||||
CHECK(error.matches(PyExc_ValueError));
|
||||
}
|
||||
}
|
||||
@@ -1,414 +0,0 @@
|
||||
#include <catch2/catch_all.hpp>
|
||||
|
||||
#include <slic3r/plugin/PluginManager.hpp>
|
||||
#include <slic3r/Utils/NetworkAgentFactory.hpp>
|
||||
#include <libslic3r/Utils.hpp> // for set_data_dir
|
||||
|
||||
#include <boost/filesystem.hpp>
|
||||
#include <boost/system/error_code.hpp>
|
||||
|
||||
#include <catch2/catch_session.hpp>
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
|
||||
using namespace Slic3r;
|
||||
namespace fs = boost::filesystem;
|
||||
|
||||
namespace {
|
||||
|
||||
// why: embedding the fake plugin keeps this test self-contained. The PEP 723
|
||||
// block declares a printer-connection plugin, and the decorated plugin package
|
||||
// registers one PrinterAgentBase capability whose AgentInfo.id is the registry
|
||||
// key asserted below.
|
||||
constexpr const char* kFakePluginSource = R"PY(# /// script
|
||||
# requires-python = ">=3.12"
|
||||
# dependencies = []
|
||||
#
|
||||
# [tool.orcaslicer.plugin]
|
||||
# name = "Lifecycle Test Agent"
|
||||
# description = "Minimal printer-agent plugin for the lifecycle test."
|
||||
# author = "tests"
|
||||
# version = "1.0.0"
|
||||
# type = "printer-connection"
|
||||
# ///
|
||||
import orca
|
||||
|
||||
|
||||
class LifecycleTestAgentCapability(orca.printer_agent.PrinterAgentBase):
|
||||
def get_name(self):
|
||||
return "Lifecycle Test Agent"
|
||||
|
||||
def get_agent_info(self):
|
||||
return orca.printer_agent.AgentInfo(
|
||||
id="lifecycle-test-agent",
|
||||
name="Lifecycle Test Agent",
|
||||
version="1.0.0",
|
||||
description="Lifecycle test printer agent",
|
||||
)
|
||||
|
||||
|
||||
@orca.plugin
|
||||
class LifecycleTestPlugin(orca.base):
|
||||
def register_capabilities(self):
|
||||
orca.register_capability(LifecycleTestAgentCapability)
|
||||
)PY";
|
||||
|
||||
// why: in production GUI_App::init_plugin_gui_wiring subscribes the agent-registry
|
||||
// callbacks; the test binary has no GUI, so install the same UNLOAD-side wiring
|
||||
// once so the tests exercise the production deregister-on-unload path.
|
||||
// note: the load-side (register) wiring is deliberately NOT installed - the two
|
||||
// concurrent load_plugin calls in the duplicate-id test would race for the id;
|
||||
// the tests register manually, in a deterministic order, instead.
|
||||
void install_agent_registry_wiring()
|
||||
{
|
||||
static bool installed = false;
|
||||
if (installed)
|
||||
return;
|
||||
installed = true;
|
||||
|
||||
PluginManager& mgr = PluginManager::instance();
|
||||
mgr.subscribe_on_unload_callback(NetworkAgentFactory::deregister_python_plugin);
|
||||
mgr.subscribe_on_capability_unload_callback([](const PluginCapabilityId& capability) {
|
||||
if (capability.type == PluginCapabilityType::PrinterConnection)
|
||||
NetworkAgentFactory::deregister_python_printer_agent(capability.plugin_key, capability.name);
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// ===========================================================================
|
||||
// PRINTER-AGENT PLUGIN LIFECYCLE: load, register, unload, deregister.
|
||||
//
|
||||
// This test uses its own executable because load_plugin runs on a detached worker
|
||||
// thread that needs the GIL released on the main thread (the PythonInterpreter
|
||||
// model). slic3rutils_tests' other Python tests hold the GIL on the main thread
|
||||
// via a bare scoped_interpreter; the two models can't share one process.
|
||||
//
|
||||
// When bundled Python is unavailable in the test environment, the test is
|
||||
// skipped so source-only or partially staged builds can still run the rest of
|
||||
// the suite.
|
||||
// ===========================================================================
|
||||
TEST_CASE("plugin lifecycle: printer-agent load registers and unload deregisters", "[plugin][lifecycle][Python]")
|
||||
{
|
||||
const std::string plugin_key = "LifecycleTestAgent"; // entry-file stem
|
||||
const std::string agent_id = "lifecycle-test-agent"; // AgentInfo.id from the plugin
|
||||
|
||||
// Stage a throwaway data directory. Plugins are discovered under
|
||||
// <data_dir>/orca_plugins, so this controls which plugin is loaded.
|
||||
const fs::path data_dir = fs::temp_directory_path() / "orca-plugin-lifecycle-test";
|
||||
const fs::path plugin_dir = data_dir / "orca_plugins" / plugin_key;
|
||||
{
|
||||
boost::system::error_code ec;
|
||||
fs::remove_all(data_dir, ec); // clear any stale run
|
||||
}
|
||||
fs::create_directories(plugin_dir);
|
||||
{
|
||||
std::ofstream out((plugin_dir / (plugin_key + ".py")).string(), std::ios::binary);
|
||||
out << kFakePluginSource;
|
||||
}
|
||||
// why: best-effort cleanup even if an assertion throws.
|
||||
struct DirGuard
|
||||
{
|
||||
fs::path p;
|
||||
~DirGuard()
|
||||
{
|
||||
boost::system::error_code ec;
|
||||
fs::remove_all(p, ec);
|
||||
}
|
||||
} guard{data_dir};
|
||||
|
||||
Slic3r::set_data_dir(data_dir.string());
|
||||
|
||||
// Initialize the plugin system on this thread. If the bundled Python home
|
||||
// is not reachable, skip gracefully.
|
||||
PluginManager& mgr = PluginManager::instance();
|
||||
if (!mgr.initialize())
|
||||
SKIP("PythonInterpreter could not initialize (bundled Python home not found in this environment)");
|
||||
install_agent_registry_wiring();
|
||||
|
||||
// Discover synchronously so the catalog holds the descriptor before loading.
|
||||
mgr.discover_plugins(/*async=*/false, /*clear=*/true);
|
||||
INFO("expected plugin_key (entry-file stem): " << plugin_key);
|
||||
PluginDescriptor descriptor;
|
||||
REQUIRE(mgr.try_get_valid_plugin_descriptor(plugin_key, descriptor));
|
||||
|
||||
// Load on the worker thread and block until it finishes.
|
||||
std::string error;
|
||||
mgr.load_plugin(plugin_key, /*skip_deps=*/true);
|
||||
const bool loaded = mgr.wait_for_plugin_load(plugin_key, std::chrono::seconds(60), error);
|
||||
INFO("plugin load error: " << error);
|
||||
REQUIRE(loaded);
|
||||
REQUIRE(mgr.is_plugin_loaded(plugin_key));
|
||||
|
||||
// Resolve the one PrinterConnection capability and register it as an agent.
|
||||
auto caps = mgr.get_plugin_capabilities(plugin_key, PluginCapabilityType::PrinterConnection);
|
||||
REQUIRE(caps.size() == 1);
|
||||
REQUIRE(caps[0] != nullptr);
|
||||
NetworkAgentFactory::register_python_printer_agent(plugin_key, caps[0]->name());
|
||||
|
||||
// The AgentInfo.id returned by the plugin is now in the registry.
|
||||
CHECK(NetworkAgentFactory::is_printer_agent_registered(agent_id));
|
||||
|
||||
// Unloading the plugin deregisters its Python-backed agent.
|
||||
REQUIRE(mgr.unload_plugin(plugin_key));
|
||||
|
||||
// The registry no longer contains the agent id after unload.
|
||||
CHECK_FALSE(NetworkAgentFactory::is_printer_agent_registered(agent_id));
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// why: duplicate-id coverage needs two distinct plugins with different package
|
||||
// keys and classes while both return the same AgentInfo.id.
|
||||
std::string make_agent_plugin_source(const std::string& suffix, const std::string& display_name, const std::string& agent_id)
|
||||
{
|
||||
return std::string{}
|
||||
+ "# /// script\n"
|
||||
+ "# requires-python = \">=3.12\"\n"
|
||||
+ "# dependencies = []\n"
|
||||
+ "#\n"
|
||||
+ "# [tool.orcaslicer.plugin]\n"
|
||||
+ "# name = \"" + display_name + "\"\n"
|
||||
+ "# description = \"Duplicate-id fake printer-agent plugin.\"\n"
|
||||
+ "# author = \"tests\"\n"
|
||||
+ "# version = \"1.0.0\"\n"
|
||||
+ "# type = \"printer-connection\"\n"
|
||||
+ "# ///\n"
|
||||
+ "import orca\n"
|
||||
+ "\n\n"
|
||||
+ "class Cap" + suffix + "(orca.printer_agent.PrinterAgentBase):\n"
|
||||
+ " def get_name(self):\n"
|
||||
+ " return \"" + display_name + "\"\n"
|
||||
+ "\n"
|
||||
+ " def get_agent_info(self):\n"
|
||||
+ " return orca.printer_agent.AgentInfo(\n"
|
||||
+ " id=\"" + agent_id + "\", name=\"" + display_name + "\",\n"
|
||||
+ " version=\"1.0.0\", description=\"duplicate id test\")\n"
|
||||
+ "\n\n"
|
||||
+ "@orca.plugin\n"
|
||||
+ "class Plugin" + suffix + "(orca.base):\n"
|
||||
+ " def register_capabilities(self):\n"
|
||||
+ " orca.register_capability(Cap" + suffix + ")\n";
|
||||
}
|
||||
|
||||
void stage_plugin(const fs::path& data_dir, const std::string& plugin_key, const std::string& source)
|
||||
{
|
||||
const fs::path dir = data_dir / "orca_plugins" / plugin_key;
|
||||
fs::create_directories(dir);
|
||||
std::ofstream out((dir / (plugin_key + ".py")).string(), std::ios::binary);
|
||||
out << source;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// ===========================================================================
|
||||
// DUPLICATE PRINTER-AGENT IDS
|
||||
// When two loaded plugins return the same AgentInfo.id, the first registration
|
||||
// keeps ownership. Unloading it removes the id, and the rejected plugin is not
|
||||
// promoted automatically. Manual re-registration is required.
|
||||
// ===========================================================================
|
||||
TEST_CASE("duplicate agent id is rejected and the winner is not clobbered", "[plugin][lifecycle][Python]")
|
||||
{
|
||||
const std::string key_a = "DuplicateIdAgentA"; // winner, registered first
|
||||
const std::string key_b = "DuplicateIdAgentB"; // duplicate, rejected
|
||||
const std::string dup_id = "duplicate-id-agent"; // both plugins return this AgentInfo.id
|
||||
|
||||
const fs::path data_dir = fs::temp_directory_path() / "orca-duplicate-agent-test";
|
||||
{
|
||||
boost::system::error_code ec;
|
||||
fs::remove_all(data_dir, ec);
|
||||
}
|
||||
stage_plugin(data_dir, key_a, make_agent_plugin_source("A", "Duplicate Id Agent A", dup_id));
|
||||
stage_plugin(data_dir, key_b, make_agent_plugin_source("B", "Duplicate Id Agent B", dup_id));
|
||||
struct DirGuard
|
||||
{
|
||||
fs::path p;
|
||||
~DirGuard()
|
||||
{
|
||||
boost::system::error_code ec;
|
||||
fs::remove_all(p, ec);
|
||||
}
|
||||
} guard{data_dir};
|
||||
|
||||
Slic3r::set_data_dir(data_dir.string());
|
||||
|
||||
PluginManager& mgr = PluginManager::instance();
|
||||
if (!mgr.initialize())
|
||||
SKIP("PythonInterpreter could not initialize (bundled Python home not found in this environment)");
|
||||
install_agent_registry_wiring();
|
||||
|
||||
mgr.discover_plugins(/*async=*/false, /*clear=*/true);
|
||||
PluginDescriptor descriptor_a;
|
||||
PluginDescriptor descriptor_b;
|
||||
REQUIRE(mgr.try_get_valid_plugin_descriptor(key_a, descriptor_a));
|
||||
REQUIRE(mgr.try_get_valid_plugin_descriptor(key_b, descriptor_b));
|
||||
|
||||
std::string error;
|
||||
mgr.load_plugin(key_a, /*skip_deps=*/true);
|
||||
mgr.load_plugin(key_b, /*skip_deps=*/true);
|
||||
const bool loaded_a = mgr.wait_for_plugin_load(key_a, std::chrono::seconds(60), error);
|
||||
INFO("plugin A load error: " << error);
|
||||
REQUIRE(loaded_a);
|
||||
const bool loaded_b = mgr.wait_for_plugin_load(key_b, std::chrono::seconds(60), error);
|
||||
INFO("plugin B load error: " << error);
|
||||
REQUIRE(loaded_b);
|
||||
|
||||
auto caps_a = mgr.get_plugin_capabilities(key_a, PluginCapabilityType::PrinterConnection);
|
||||
auto caps_b = mgr.get_plugin_capabilities(key_b, PluginCapabilityType::PrinterConnection);
|
||||
REQUIRE(caps_a.size() == 1);
|
||||
REQUIRE(caps_b.size() == 1);
|
||||
|
||||
// Register A first as the owner, then B with the duplicate id.
|
||||
NetworkAgentFactory::register_python_printer_agent(key_a, caps_a[0]->name());
|
||||
NetworkAgentFactory::register_python_printer_agent(key_b, caps_b[0]->name());
|
||||
|
||||
// The shared id is registered, and still owned by A; B did not replace it.
|
||||
CHECK(NetworkAgentFactory::is_printer_agent_registered(dup_id));
|
||||
const PrinterAgentInfo* info = NetworkAgentFactory::get_printer_agent_info(dup_id);
|
||||
REQUIRE(info != nullptr);
|
||||
CHECK(info->plugin_identifier.find(key_a) != std::string::npos); // owned by A
|
||||
CHECK(info->plugin_identifier.find(key_b) == std::string::npos); // B never took ownership
|
||||
|
||||
// Unload the owner. The id is not promoted to the still loaded duplicate.
|
||||
REQUIRE(mgr.unload_plugin(key_a));
|
||||
CHECK_FALSE(NetworkAgentFactory::is_printer_agent_registered(dup_id));
|
||||
|
||||
mgr.unload_plugin(key_b); // the duplicate was loaded but never registered
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// NATIVE (BUILT-IN) AGENT ID COLLISION
|
||||
// A plugin may not hijack a built-in agent id (e.g. "bbl"). The built-in keeps
|
||||
// ownership and the plugin's agent is rejected.
|
||||
// ===========================================================================
|
||||
TEST_CASE("printer-agent plugin cannot claim a built-in agent id", "[plugin][lifecycle][Python]")
|
||||
{
|
||||
// Register the native built-ins so "bbl"/"orca" occupy the registry.
|
||||
NetworkAgentFactory::register_all_agents();
|
||||
REQUIRE(NetworkAgentFactory::is_printer_agent_registered(BBL_PRINTER_AGENT_ID));
|
||||
|
||||
const std::string plugin_key = "BuiltinClashAgent";
|
||||
|
||||
const fs::path data_dir = fs::temp_directory_path() / "orca-builtin-clash-test";
|
||||
{
|
||||
boost::system::error_code ec;
|
||||
fs::remove_all(data_dir, ec);
|
||||
}
|
||||
stage_plugin(data_dir, plugin_key, make_agent_plugin_source("Clash", "Builtin Clash", BBL_PRINTER_AGENT_ID));
|
||||
struct DirGuard
|
||||
{
|
||||
fs::path p;
|
||||
~DirGuard()
|
||||
{
|
||||
boost::system::error_code ec;
|
||||
fs::remove_all(p, ec);
|
||||
}
|
||||
} guard{data_dir};
|
||||
|
||||
Slic3r::set_data_dir(data_dir.string());
|
||||
|
||||
PluginManager& mgr = PluginManager::instance();
|
||||
if (!mgr.initialize())
|
||||
SKIP("PythonInterpreter could not initialize (bundled Python home not found in this environment)");
|
||||
install_agent_registry_wiring();
|
||||
|
||||
mgr.discover_plugins(/*async=*/false, /*clear=*/true);
|
||||
PluginDescriptor descriptor;
|
||||
REQUIRE(mgr.try_get_valid_plugin_descriptor(plugin_key, descriptor));
|
||||
|
||||
std::string error;
|
||||
mgr.load_plugin(plugin_key, /*skip_deps=*/true);
|
||||
REQUIRE(mgr.wait_for_plugin_load(plugin_key, std::chrono::seconds(60), error));
|
||||
|
||||
auto caps = mgr.get_plugin_capabilities(plugin_key, PluginCapabilityType::PrinterConnection);
|
||||
REQUIRE(caps.size() == 1);
|
||||
NetworkAgentFactory::register_python_printer_agent(plugin_key, caps[0]->name());
|
||||
|
||||
// "bbl" is still the native built-in, not the plugin.
|
||||
const PrinterAgentInfo* info = NetworkAgentFactory::get_printer_agent_info(BBL_PRINTER_AGENT_ID);
|
||||
REQUIRE(info != nullptr);
|
||||
CHECK_FALSE(info->is_plugin());
|
||||
CHECK(info->plugin_identifier.find(plugin_key) == std::string::npos);
|
||||
|
||||
mgr.unload_plugin(plugin_key);
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// RE-REGISTERING THE SAME CAPABILITY IS A REFRESH, NOT A DUPLICATE
|
||||
// The guard rejects only a DIFFERENT owner. The same capability registering its
|
||||
// own id again must stay registered (insert_or_assign refreshes it in place).
|
||||
// ===========================================================================
|
||||
TEST_CASE("re-registering the same capability keeps its agent registered", "[plugin][lifecycle][Python]")
|
||||
{
|
||||
const std::string plugin_key = "ReRegisterAgent";
|
||||
const std::string agent_id = "re-register-agent";
|
||||
|
||||
const fs::path data_dir = fs::temp_directory_path() / "orca-re-register-test";
|
||||
{
|
||||
boost::system::error_code ec;
|
||||
fs::remove_all(data_dir, ec);
|
||||
}
|
||||
stage_plugin(data_dir, plugin_key, make_agent_plugin_source("Re", "Re Register", agent_id));
|
||||
struct DirGuard
|
||||
{
|
||||
fs::path p;
|
||||
~DirGuard()
|
||||
{
|
||||
boost::system::error_code ec;
|
||||
fs::remove_all(p, ec);
|
||||
}
|
||||
} guard{data_dir};
|
||||
|
||||
Slic3r::set_data_dir(data_dir.string());
|
||||
|
||||
PluginManager& mgr = PluginManager::instance();
|
||||
if (!mgr.initialize())
|
||||
SKIP("PythonInterpreter could not initialize (bundled Python home not found in this environment)");
|
||||
install_agent_registry_wiring();
|
||||
|
||||
mgr.discover_plugins(/*async=*/false, /*clear=*/true);
|
||||
PluginDescriptor descriptor;
|
||||
REQUIRE(mgr.try_get_valid_plugin_descriptor(plugin_key, descriptor));
|
||||
|
||||
std::string error;
|
||||
mgr.load_plugin(plugin_key, /*skip_deps=*/true);
|
||||
REQUIRE(mgr.wait_for_plugin_load(plugin_key, std::chrono::seconds(60), error));
|
||||
|
||||
auto caps = mgr.get_plugin_capabilities(plugin_key, PluginCapabilityType::PrinterConnection);
|
||||
REQUIRE(caps.size() == 1);
|
||||
|
||||
NetworkAgentFactory::register_python_printer_agent(plugin_key, caps[0]->name());
|
||||
REQUIRE(NetworkAgentFactory::is_printer_agent_registered(agent_id));
|
||||
const PrinterAgentInfo* first = NetworkAgentFactory::get_printer_agent_info(agent_id);
|
||||
REQUIRE(first != nullptr);
|
||||
const std::string owner = first->plugin_identifier;
|
||||
|
||||
// The same capability registering again is a refresh: still registered, same owner, not rejected.
|
||||
NetworkAgentFactory::register_python_printer_agent(plugin_key, caps[0]->name());
|
||||
CHECK(NetworkAgentFactory::is_printer_agent_registered(agent_id));
|
||||
const PrinterAgentInfo* second = NetworkAgentFactory::get_printer_agent_info(agent_id);
|
||||
REQUIRE(second != nullptr);
|
||||
CHECK(second->plugin_identifier == owner);
|
||||
|
||||
mgr.unload_plugin(plugin_key);
|
||||
}
|
||||
|
||||
// why: this binary embeds CPython through the PythonInterpreter singleton, which
|
||||
// lives for the full process. Normal static destruction can tear Python down
|
||||
// while C++ objects still hold Python handles, producing a Windows heap
|
||||
// corruption after the assertions have finished. The app has an ordered shutdown
|
||||
// path; this test harness does not, so it returns the Catch2 result through
|
||||
// _Exit after flushing output.
|
||||
int main(int argc, char* argv[])
|
||||
{
|
||||
const int result = Catch::Session().run(argc, argv);
|
||||
std::cout.flush();
|
||||
std::cerr.flush();
|
||||
std::fflush(nullptr);
|
||||
std::_Exit(result);
|
||||
}
|
||||
@@ -1,131 +0,0 @@
|
||||
#include <catch2/catch_all.hpp>
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include <slic3r/Utils/QidiPrinterAgent.hpp>
|
||||
|
||||
#include <string>
|
||||
|
||||
using namespace Slic3r;
|
||||
|
||||
TEST_CASE("Qidi slot response rejects null variables without throwing", "[QidiPrinterAgent]")
|
||||
{
|
||||
const std::string response = R"({
|
||||
"result": {
|
||||
"status": {
|
||||
"save_variables": {
|
||||
"variables": null
|
||||
}
|
||||
}
|
||||
}
|
||||
})";
|
||||
nlohmann::json status;
|
||||
nlohmann::json variables;
|
||||
std::string error;
|
||||
bool parsed = true;
|
||||
|
||||
REQUIRE_NOTHROW(parsed = QidiPrinterAgent::parse_slot_response(response, status, variables, error));
|
||||
CHECK_FALSE(parsed);
|
||||
CHECK_THAT(error, Catch::Matchers::ContainsSubstring("variables"));
|
||||
CHECK_THAT(error, Catch::Matchers::ContainsSubstring("object"));
|
||||
}
|
||||
|
||||
TEST_CASE("Qidi slot response rejects missing and non-object fields without throwing", "[QidiPrinterAgent]")
|
||||
{
|
||||
std::string response;
|
||||
|
||||
SECTION("missing result")
|
||||
{
|
||||
response = R"({})";
|
||||
}
|
||||
|
||||
SECTION("non-object result")
|
||||
{
|
||||
response = R"({"result":null})";
|
||||
}
|
||||
|
||||
SECTION("missing status")
|
||||
{
|
||||
response = R"({"result":{}})";
|
||||
}
|
||||
|
||||
SECTION("non-object status")
|
||||
{
|
||||
response = R"({"result":{"status":null}})";
|
||||
}
|
||||
|
||||
SECTION("missing save_variables")
|
||||
{
|
||||
response = R"({"result":{"status":{}}})";
|
||||
}
|
||||
|
||||
SECTION("non-object save_variables")
|
||||
{
|
||||
response = R"({"result":{"status":{"save_variables":null}}})";
|
||||
}
|
||||
|
||||
SECTION("missing variables")
|
||||
{
|
||||
response = R"({"result":{"status":{"save_variables":{}}}})";
|
||||
}
|
||||
|
||||
SECTION("scalar")
|
||||
{
|
||||
response = R"({"result":{"status":{"save_variables":{"variables":42}}}})";
|
||||
}
|
||||
|
||||
SECTION("array")
|
||||
{
|
||||
response = R"({"result":{"status":{"save_variables":{"variables":[]}}}})";
|
||||
}
|
||||
|
||||
nlohmann::json status;
|
||||
nlohmann::json variables;
|
||||
std::string error;
|
||||
bool parsed = true;
|
||||
|
||||
REQUIRE_NOTHROW(parsed = QidiPrinterAgent::parse_slot_response(response, status, variables, error));
|
||||
CHECK_FALSE(parsed);
|
||||
}
|
||||
|
||||
TEST_CASE("Qidi slot response exposes valid status and variables", "[QidiPrinterAgent]")
|
||||
{
|
||||
const std::string response = R"({
|
||||
"result": {
|
||||
"status": {
|
||||
"save_variables": {
|
||||
"variables": {
|
||||
"box_count": 2,
|
||||
"color_slot0": 3
|
||||
}
|
||||
},
|
||||
"box_stepper slot0": {
|
||||
"runout_button": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
})";
|
||||
nlohmann::json status;
|
||||
nlohmann::json variables;
|
||||
std::string error;
|
||||
bool parsed = false;
|
||||
|
||||
REQUIRE_NOTHROW(parsed = QidiPrinterAgent::parse_slot_response(response, status, variables, error));
|
||||
REQUIRE(parsed);
|
||||
CHECK(status.is_object());
|
||||
CHECK(variables.is_object());
|
||||
CHECK(variables.at("box_count") == 2);
|
||||
CHECK(status.contains("box_stepper slot0"));
|
||||
}
|
||||
|
||||
TEST_CASE("Qidi slot response rejects invalid JSON", "[QidiPrinterAgent]")
|
||||
{
|
||||
nlohmann::json status;
|
||||
nlohmann::json variables;
|
||||
std::string error;
|
||||
bool parsed = true;
|
||||
|
||||
REQUIRE_NOTHROW(parsed = QidiPrinterAgent::parse_slot_response("{not json", status, variables, error));
|
||||
CHECK_FALSE(parsed);
|
||||
CHECK(error == "Invalid JSON response");
|
||||
}
|
||||
Reference in New Issue
Block a user