mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-08-05 17:17:42 +00:00
159 lines
7.8 KiB
Markdown
159 lines
7.8 KiB
Markdown
# 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.
|