mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-08-05 09:07:39 +00:00
Compare commits
28 Commits
nightly-bu
...
feat/print
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
07c62112b8 | ||
|
|
f23e4963bf | ||
|
|
95279f7084 | ||
|
|
1014558c91 | ||
|
|
6515062a3a | ||
|
|
8153e26b6d | ||
|
|
79fd33d49a | ||
|
|
cdb662d4b2 | ||
|
|
a27fa7b5df | ||
|
|
cdeed107c5 | ||
|
|
be92f7e78c | ||
|
|
76f9de4cfb | ||
|
|
cd6d2cbf5e | ||
|
|
deaca0189f | ||
|
|
7d54ee286d | ||
|
|
dddfc7a8ad | ||
|
|
673bfb0dee | ||
|
|
b9e8c5a38c | ||
|
|
a2a4ca20e4 | ||
|
|
12075459d2 | ||
|
|
f3770f3106 | ||
|
|
90fbf1770f | ||
|
|
bde94ab37f | ||
|
|
6d25e1777e | ||
|
|
7ce26ca8e5 | ||
|
|
367fcce634 | ||
|
|
9b3a44b339 | ||
|
|
6384191102 |
@@ -465,57 +465,6 @@ static void stb_textedit_click(STB_TEXTEDIT_STRING *str, STB_TexteditState *stat
|
||||
STB_TEXTEDIT_LAYOUTROW(&r, str, 0);
|
||||
y = r.ymin;
|
||||
}
|
||||
else
|
||||
{
|
||||
// In multi-line mode, clamp y to stay within the text vertical bounds.
|
||||
// This lets the click still land at a valid location if the mouse is slightly
|
||||
// above or below the text.
|
||||
StbTexteditRow r;
|
||||
int n = STB_TEXTEDIT_STRINGLEN(str);
|
||||
int i = 0;
|
||||
float base_y = 0, y_min, y_max;
|
||||
|
||||
// Get the first row to establish y_min and start the iteration
|
||||
STB_TEXTEDIT_LAYOUTROW(&r, str, 0);
|
||||
if (r.num_chars <= 0)
|
||||
{
|
||||
state->cursor = 0;
|
||||
state->select_start = state->cursor;
|
||||
state->select_end = state->cursor;
|
||||
state->has_preferred_x = 0;
|
||||
return;
|
||||
}
|
||||
y_min = r.ymin;
|
||||
y_max = base_y + r.ymax;
|
||||
i = r.num_chars;
|
||||
base_y += r.baseline_y_delta;
|
||||
|
||||
// Walk the remaining rows to find the bottom of the last row
|
||||
while (i < n)
|
||||
{
|
||||
STB_TEXTEDIT_LAYOUTROW(&r, str, i);
|
||||
if (r.num_chars <= 0)
|
||||
break;
|
||||
y_max = base_y + r.ymax;
|
||||
i += r.num_chars;
|
||||
base_y += r.baseline_y_delta;
|
||||
}
|
||||
|
||||
// If the text ends with a newline, account for the empty trailing line
|
||||
// so the cursor can be placed on it
|
||||
if (n > 0 && STB_TEXTEDIT_GETCHAR(str, n - 1) == STB_TEXTEDIT_NEWLINE)
|
||||
{
|
||||
STB_TEXTEDIT_LAYOUTROW(&r, str, n);
|
||||
y_max = base_y + r.ymax;
|
||||
}
|
||||
|
||||
// Subtract half the last line height to avoid rounding issues when the mouse
|
||||
// is just barely below the last line (keep cursor on the last line, not after the text)
|
||||
y_max -= (r.ymax - r.ymin) * 0.5f;
|
||||
|
||||
if (y < y_min) y = y_min;
|
||||
if (y > y_max) y = y_max;
|
||||
}
|
||||
|
||||
state->cursor = stb_text_locate_coord(str, x, y);
|
||||
state->select_start = state->cursor;
|
||||
@@ -536,50 +485,6 @@ static void stb_textedit_drag(STB_TEXTEDIT_STRING *str, STB_TexteditState *state
|
||||
STB_TEXTEDIT_LAYOUTROW(&r, str, 0);
|
||||
y = r.ymin;
|
||||
}
|
||||
else
|
||||
{
|
||||
// In multi-line mode, clamp y to stay within the text vertical bounds.
|
||||
// This lets the drag keep working if the mouse goes off the top or bottom of the text.
|
||||
StbTexteditRow r;
|
||||
int n = STB_TEXTEDIT_STRINGLEN(str);
|
||||
int i = 0;
|
||||
float base_y = 0, y_min, y_max;
|
||||
|
||||
// Get the first row to establish y_min and start the iteration
|
||||
STB_TEXTEDIT_LAYOUTROW(&r, str, 0);
|
||||
if (r.num_chars <= 0)
|
||||
return;
|
||||
y_min = r.ymin;
|
||||
y_max = base_y + r.ymax;
|
||||
i = r.num_chars;
|
||||
base_y += r.baseline_y_delta;
|
||||
|
||||
// Walk the remaining rows to find the bottom of the last row
|
||||
while (i < n)
|
||||
{
|
||||
STB_TEXTEDIT_LAYOUTROW(&r, str, i);
|
||||
if (r.num_chars <= 0)
|
||||
break;
|
||||
y_max = base_y + r.ymax;
|
||||
i += r.num_chars;
|
||||
base_y += r.baseline_y_delta;
|
||||
}
|
||||
|
||||
// If the text ends with a newline, account for the empty trailing line
|
||||
// so the cursor can be placed on it
|
||||
if (n > 0 && STB_TEXTEDIT_GETCHAR(str, n - 1) == STB_TEXTEDIT_NEWLINE)
|
||||
{
|
||||
STB_TEXTEDIT_LAYOUTROW(&r, str, n);
|
||||
y_max = base_y + r.ymax;
|
||||
}
|
||||
|
||||
// Subtract half the last line height to avoid rounding issues when the mouse
|
||||
// is just barely below the last line (keep cursor on the last line, not after the text)
|
||||
y_max -= (r.ymax - r.ymin) * 0.5f;
|
||||
|
||||
if (y < y_min) y = y_min;
|
||||
if (y > y_max) y = y_max;
|
||||
}
|
||||
|
||||
if (state->select_start == state->select_end)
|
||||
state->select_start = state->cursor;
|
||||
|
||||
72
docs/printer-agent/README.md
Normal file
72
docs/printer-agent/README.md
Normal file
@@ -0,0 +1,72 @@
|
||||
# Printer agents
|
||||
|
||||
Printer agents let OrcaSlicer communicate with printers through a
|
||||
standardized protocol. They translate between a printer's
|
||||
native API and the application interfaces that the app already
|
||||
uses.
|
||||
|
||||
This documentation explains the compatibility boundary, runtime ownership,
|
||||
connection and status flow, command and feature behavior, built-in and plugin
|
||||
agent implementations, and the testing evidence required for compatibility
|
||||
claims.
|
||||
|
||||
## What printer agents do
|
||||
|
||||
A printer agent has two jobs:
|
||||
|
||||
1. Accept the app's existing commands and translate the ones its
|
||||
printer supports.
|
||||
2. Convert native printer status into correctly-shaped state that
|
||||
`MachineObject` understands.
|
||||
|
||||
Currently, agents work at a compatibility boundary, i.e., making other vendors compatible with Bambu-shaped code, not a vendor-neutral one.
|
||||
Some Bambu concepts remain part of the payload and command vocabulary.
|
||||
End goal is to make the whole command and payload interfaces vendor-neutral.
|
||||
|
||||
## Vocabulary
|
||||
|
||||
Every chapter reuses these terms. The "Is not" column is the part that
|
||||
causes confusion when it is left implicit.
|
||||
|
||||
| Term | Is | Selected by | Is not |
|
||||
| --- | --- | --- | --- |
|
||||
| Agent ID | Which printer agent implementation to use | `printer_agent` on the printer preset; empty is the legacy `bbl`-or-`orca` sentinel | Which printer |
|
||||
| Printer agent | The live `IPrinterAgent` instance for that ID, created and cached once per ID by `NetworkAgentFactory` | Factory lookup on the agent ID | A connection, and not one object per printer |
|
||||
| Device ID | One printer inside that implementation | Bind with Access Code for the Moonraker family, where the entered address becomes the ID; Bambu uses its own discovery identity | Which protocol |
|
||||
| `MachineObject` | The Device tab's view of one selected printer | `DeviceManager::selected_machine`, which stores only an ID | Proof that a printer is reachable |
|
||||
| Freshness | `is_connected()`, a test over the last-update time | Any reset of the update time, including one no status has followed | Proof that status arrived |
|
||||
| Status-confirmed readiness | A push-status message has actually been parsed | The first real status message | The same thing as a successful `connect_printer()` |
|
||||
|
||||
Earlier drafts used "transport" for the printer agent instance. That term
|
||||
is retired: the code selects an implementation, not a wire protocol.
|
||||
|
||||
## How to use this guide
|
||||
|
||||
- [Architecture](architecture.md) describes objects, ownership, lifetimes,
|
||||
error handling, the feature gate, and compatibility contracts for printer
|
||||
agents.
|
||||
- [Connection and status](connection-and-status.md) describes how presets,
|
||||
machines, access codes, status messages, and commands fit together at
|
||||
runtime. Unlike Architecture, it follows the sequence of selecting an
|
||||
agent, connecting, receiving status, and sending commands.
|
||||
- [Printing](printing.md), [filament synchronization](filament.md), and
|
||||
[camera support](camera.md) are separate chapters because they contain
|
||||
per-feature detail rather than because they are universally special:
|
||||
Printing has its send, preflight, recovery, and start contracts; Filament
|
||||
covers acquisition, mapping selection, and print-time delivery; Camera
|
||||
covers the distinct Bambu, Moonraker, and Snapmaker ownership models.
|
||||
- [Built-in agents](agents.md) describes the Moonraker family and the Qidi,
|
||||
Snapmaker variants.
|
||||
- [Python plugin agents](plugin-agents.md) describes the plugin bridge and
|
||||
lifecycle.
|
||||
- [Testing and troubleshooting](testing.md) explains automated checks, manual
|
||||
hardware work, known defects, and the evidence required for compatibility
|
||||
claims.
|
||||
- The [capability matrix](reference/capability-matrix.md) is the compact
|
||||
feature reference. The [manual checklist](reference/manual-checklist.html)
|
||||
is for a live-printer verification pass.
|
||||
|
||||
Treat source code as authoritative when it differs from this guide. In
|
||||
particular, preserve the compatibility rules called out in each chapter:
|
||||
they protect stored presets, existing profiles, and the Device tab's
|
||||
assumptions.
|
||||
205
docs/printer-agent/agents.md
Normal file
205
docs/printer-agent/agents.md
Normal file
@@ -0,0 +1,205 @@
|
||||
# Built-in printer agents
|
||||
|
||||
*Owns the per-vendor behavior of the built-in agents: what each subclass
|
||||
changes and what it inherits unchanged. Defers the interface every agent
|
||||
implements to [Architecture](architecture.md) and
|
||||
[Python plugin agents](plugin-agents.md).*
|
||||
|
||||
This chapter covers the built-in Moonraker family: the general
|
||||
`MoonrakerPrinterAgent` and the Qidi and Snapmaker variants. Creality
|
||||
(`CrealityPrintAgent`) is also a member of this family and inherits the base
|
||||
behavior, but has no section here; see the capability matrix for its
|
||||
per-feature coverage. They share the same connection and status machinery.
|
||||
Change the base class only when the behavior is valid for all of them.
|
||||
|
||||
Each subclass is thin. `MoonrakerPrinterAgent` holds the HTTP connection,
|
||||
the WebSocket status subscription, the REST command worker, thumbnail
|
||||
lookup, the chamber-light heuristic, and the upload-and-start path.
|
||||
`QidiPrinterAgent` overrides filament discovery and adds multi-color box
|
||||
mapping; `SnapmakerPrinterAgent` overrides filament discovery and camera
|
||||
setup; `CrealityPrintAgent` overrides filament refresh. Each derives from
|
||||
`MoonrakerPrinterAgent` and is `final`, which is why the guard rule below
|
||||
must be type-based.
|
||||
|
||||
## Moonraker family
|
||||
|
||||
### Connection and commands
|
||||
|
||||
Moonraker-family agents use plain HTTP for the LAN connection. The connection
|
||||
path deliberately ignores a TLS request because the supported printer stacks
|
||||
serve Moonraker or a reverse proxy over HTTP. Restoring the caller's TLS
|
||||
default can send a connection to an unavailable HTTPS endpoint.
|
||||
|
||||
Status is a Moonraker WebSocket subscription. Commands use REST. Command
|
||||
translation happens immediately, but the resulting HTTP work runs through one
|
||||
agent-owned FIFO worker. Each queued operation captures the current base URL
|
||||
and API key before it is queued, so a later printer switch does not redirect
|
||||
an earlier command. Keep this separation: network work on the UI path makes
|
||||
controls feel stalled, and allowing a queued command to reread connection
|
||||
state can send it to the wrong printer.
|
||||
|
||||
Pause, resume, and cancel use the dedicated Moonraker print endpoints. Do not
|
||||
replace them with queued `PAUSE`, `RESUME`, or `CANCEL_PRINT` G-code. The
|
||||
endpoints interrupt the print directly; a G-code command can wait behind the
|
||||
active print or macro.
|
||||
|
||||
The request router accepts the Bambu-shaped JSON used by the native device
|
||||
tab. Supply object-shaped namespaces such as `print` and `system`. A malformed
|
||||
but parseable payload with a scalar where the router expects an object can
|
||||
still fail before the unsupported-command fallback. The supported generic fan
|
||||
status is the standard `fan` object, which represents the part fan only.
|
||||
Ordinary part-fan control also works through the legacy `gcode_line` path,
|
||||
which sends `M106` while `is_enable_np` is false. Auxiliary and chamber fans
|
||||
are neither reported nor controlled.
|
||||
|
||||
Do not add `cfg`, `fun`, `aux`, and `stat` to the Moonraker status payload just
|
||||
to make it look more complete. Together those fields set `is_enable_np` and
|
||||
make the UI choose its structured fan and extruder commands instead. The
|
||||
Moonraker agent does not translate those commands, so working controls become
|
||||
unsupported no-ops. This is a UI-routing constraint, not a reason to expose
|
||||
structured fan support.
|
||||
|
||||
### Status shown by the native device tab
|
||||
|
||||
The agent translates Moonraker status into the Bambu-shaped status payload the
|
||||
existing Device tab understands. Some fields are necessarily synthetic:
|
||||
|
||||
- The virtual SD-card readiness bit and a basic software-version row make the
|
||||
native UI consider the printer ready. Each pull payload also ensures
|
||||
`m_push_count` and `m_full_msg_count` are at least one and refreshes
|
||||
`last_push_time`. Together with the normal-storage state and a placeholder
|
||||
module version, this satisfies the native `is_info_ready()` and printing
|
||||
gates. These are compatibility scaffolding, not reports of physical storage
|
||||
or OTA support.
|
||||
- Current and total layers are emitted only when `print_stats.info` contains
|
||||
numeric values. Moonraker may send `null`, and many profiles do not emit the
|
||||
`SET_PRINT_STATS_INFO` data needed to populate them. Do not turn that gap
|
||||
into a JSON conversion exception.
|
||||
- Remaining time is estimated from elapsed print time and virtual-SD progress.
|
||||
It is omitted below two percent progress because the early estimate is too
|
||||
unstable. Do not derive an ETA by subtracting Moonraker duration counters:
|
||||
both are elapsed counters, so their difference is overhead, not remaining
|
||||
time.
|
||||
- Temperature readings are available, but nozzle diameter and nozzle type are
|
||||
not supplied in the status payload. The UI can therefore show an unknown
|
||||
nozzle. Do not make print submission depend on those missing fields.
|
||||
|
||||
### Camera thumbnails and lights
|
||||
|
||||
For a running job, the agent asks Moonraker for thumbnails and chooses the
|
||||
widest usable entry, rather than assuming the first entry is useful. It accepts
|
||||
both thumbnail path spellings used by Moonraker versions, encodes each path
|
||||
segment, and caches the result by filename. A failed transient lookup is tried
|
||||
again only a bounded number of times; a clean response without a thumbnail is
|
||||
cached as a negative result. The response shape handling is source-derived,
|
||||
not hardware-verified.
|
||||
|
||||
> **Do not perform this HTTP lookup while holding `payload_mutex`.** The
|
||||
> WebSocket thread builds the status payload under that mutex and the UI
|
||||
> path also needs it, so a thumbnail timeout taken under the lock would
|
||||
> stall status delivery or the UI. The lookup still blocks the WebSocket
|
||||
> thread briefly, so move it to a worker if that becomes measurable.
|
||||
|
||||
Chamber-light control searches Moonraker objects for names that look like a
|
||||
light or a standalone LED, then writes the first matching pin, LED, or macro.
|
||||
The filter exists to avoid treating unrelated objects, such as a beeper, as a
|
||||
lamp. It remains a heuristic. The incoming `led_node` is validated, but only
|
||||
`chamber_light` is acted on; `chamber_light2` is deliberately ignored. A
|
||||
printer with more than one lamp therefore has no reliable node-to-object map.
|
||||
|
||||
### Common maintenance limits
|
||||
|
||||
The same cache is reused for a selected agent ID, not per physical printer.
|
||||
Qidi and Snapmaker inherit this behavior. A stateful feature added
|
||||
to the base class must be reset carefully when a preset switches hosts.
|
||||
|
||||
> **Keep guards for this family type-based** - check whether an agent
|
||||
> derives from `MoonrakerPrinterAgent` rather than comparing its ID to
|
||||
> `moonraker`. An ID-based guard silently excludes Qidi, Snapmaker, and
|
||||
> Creality, even though they share the base behavior.
|
||||
|
||||
The family has no generic implementation for firmware-specific AMS write
|
||||
commands. Keep unsupported commands unsupported until the printer-side macro
|
||||
or API is known. Reporting success for an untranslated command makes the
|
||||
native UI claim that an action happened when it did not.
|
||||
|
||||
## Qidi
|
||||
|
||||
Qidi inherits the Moonraker connection, status, camera, and local-print path.
|
||||
Its differences are Qidi filament discovery and the pre-print multi-color-box
|
||||
mapping.
|
||||
|
||||
### Filament discovery
|
||||
|
||||
Discovery first reads the printer's device information to infer a Qidi series
|
||||
identifier, then falls back to the configured Orca model if needed. Series
|
||||
inference intentionally recognizes only a narrow set of known names. An
|
||||
unknown model still produces usable generic filament data, but not a
|
||||
series-specific preset identifier.
|
||||
|
||||
The agent reads a Qidi filament dictionary and the `save_variables` plus
|
||||
slot-runout data. Failing to fetch the dictionary is non-fatal: slot discovery
|
||||
continues with fallback material and colour values. Failing to fetch or parse
|
||||
slot data is fatal to the refresh. A missing runout value means the agent
|
||||
cannot prove filament is loaded, so it reports that slot as empty. This is an
|
||||
ambiguity in the firmware data, not proof that the box is empty.
|
||||
|
||||
`save_variables.variables` must be an object. Qidi firmware can return `null`
|
||||
there, and generic JSON value access can throw on a present null. The parser
|
||||
rejects that shape without throwing. Preserve the null-slot tests whenever the
|
||||
response parser changes.
|
||||
|
||||
### Multi-color mapping before a print
|
||||
|
||||
Before every Qidi print-start wrapper, the agent writes `enable_box` and, for
|
||||
mapped tools, persistent `value_t<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`
|
||||
158
docs/printer-agent/architecture.md
Normal file
158
docs/printer-agent/architecture.md
Normal file
@@ -0,0 +1,158 @@
|
||||
# Architecture
|
||||
|
||||
*Owns the structural rules: what the objects are, who owns them, what an
|
||||
agent must implement, and which behaviors are compatibility contracts.
|
||||
Defers the runtime sequence - selecting, connecting, receiving status,
|
||||
sending commands - to [Connection and status](connection-and-status.md).*
|
||||
|
||||
## The compatibility boundary
|
||||
|
||||
The Device tab was built around Bambu-style commands and status. A printer
|
||||
agent is the translation boundary between that existing contract and a
|
||||
vendor's native protocol:
|
||||
|
||||
```text
|
||||
Device tab <-> MachineObject <-> NetworkAgent <-> IPrinterAgent
|
||||
<-> vendor protocol
|
||||
```
|
||||
|
||||
Note: end goal is to move beyond this and achieve a truly vendor-neutral translation layer.
|
||||
|
||||
The GUI builds commands and reads `MachineObject` state. An agent owns the
|
||||
vendor request, response, connection, and status translation. Keep vendor
|
||||
details on the agent side of this boundary.
|
||||
|
||||
Status translation is deliberately Bambu-shaped. Agents deliver payloads
|
||||
through the callbacks used by the existing Bambu path, and
|
||||
`MachineObject::parse_json()` interprets them. This preserves the Device
|
||||
tab's established behavior, but it is not a vendor-neutral protocol.
|
||||
|
||||
Important (again): end goal is to move beyond this and achieve a truly vendor-neutral translation layer.
|
||||
|
||||
## Runtime objects and ownership
|
||||
|
||||
`NetworkAgent` is the facade used by the application. It holds one live
|
||||
`IPrinterAgent` pointer, which is initially null and may return to null
|
||||
when a selected ID is unavailable. Callers must handle the null case. An
|
||||
absent agent is an inert state, not permission to fall back to another
|
||||
printer agent. A fallback would connect to a different implementation than
|
||||
the one selected by the preset, and could therefore send commands or status
|
||||
work to the wrong printer.
|
||||
|
||||
`NetworkAgentFactory` registers built-in and plugin implementations by
|
||||
agent ID. It creates and caches one implementation for each ID. The ID
|
||||
selects a printer agent implementation, while a `MachineObject` selects one
|
||||
printer by device ID. The resulting cardinality is one active agent to many
|
||||
machines.
|
||||
|
||||
For example, suppose two Moonraker printers are on the LAN at
|
||||
`192.168.1.20` and `192.168.1.21`. In the Device tab machine-select popup, the
|
||||
user chooses **Bind with Access Code**; `PinCodePanel::on_mouse_left_up` opens
|
||||
`InputIpAddressDialog`, and each entered address is bound as a separate
|
||||
printer. Both presets store the same agent ID, `moonraker`, so
|
||||
`NetworkAgentFactory::create_printer_agent_by_id` returns the same cached
|
||||
`IPrinterAgent` pointer for both presets. Each printer nevertheless has its
|
||||
own `MachineObject` and device ID. For the Moonraker family,
|
||||
`MoonrakerPrinterAgent::bind_detect` calls `init_device_info` with the entered
|
||||
address as both the device ID and address, so the two device IDs are the two
|
||||
addresses.
|
||||
|
||||
That is what one active agent to many machines means. Per-printer state must
|
||||
be keyed by device ID rather than held only on the agent instance, because one
|
||||
agent object is shared by both printers. State stored only on that object
|
||||
would be shared between two different machines and could route status or
|
||||
commands to the wrong one. The same sharing explains why
|
||||
`GUI_App::switch_printer_agent` compares device IDs even when the agent pointer
|
||||
is unchanged: otherwise its unchanged-agent early return would skip
|
||||
reselection when the user switches between these presets, leaving status and
|
||||
filament work aimed at the previous printer.
|
||||
|
||||
> **Do not make an agent instance per printer just to hold device state.**
|
||||
> Keep per-printer state keyed by device ID, because one agent object is
|
||||
> shared by every printer of that type - state held on the instance would
|
||||
> route status or commands to the wrong `MachineObject`.
|
||||
|
||||
> **Do not fall back to another printer agent when the live one is null.**
|
||||
> An absent agent is an inert state. A fallback would connect to a
|
||||
> different implementation than the preset selected.
|
||||
|
||||
## Commands and unsupported work
|
||||
|
||||
An agent must either translate a Device-tab command or return an explicit
|
||||
error. `ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED` means no translation exists.
|
||||
`ORCA_NETWORK_ERR_CAP_NOT_AVAILABLE` means a translation exists but this
|
||||
printer cannot use it. `MachineObject::publish_json()` turns either result
|
||||
into the user-visible unsupported-command response.
|
||||
|
||||
Every Device-tab command must leave by one of these four exits. The fifth
|
||||
path is the one to watch for in review:
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
CMD["Device-tab command JSON"] --> PUSH{"pushing namespace?"}
|
||||
PUSH -- yes --> OK1["Accept - the status stream already satisfies it"]
|
||||
PUSH -- no --> TRANS{"Translation exists for this agent?"}
|
||||
TRANS -- no --> E1["Return ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED"]
|
||||
TRANS -- yes --> CAP{"This printer can use it?"}
|
||||
CAP -- no --> E2["Return ORCA_NETWORK_ERR_CAP_NOT_AVAILABLE"]
|
||||
CAP -- yes --> OK2["Translate and send to the printer"]
|
||||
TRANS -. FORBIDDEN .-> BAD["Return success without translating"]
|
||||
E1 --> PUB["MachineObject::publish_json turns both errors<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.
|
||||
96
docs/printer-agent/camera.md
Normal file
96
docs/printer-agent/camera.md
Normal file
@@ -0,0 +1,96 @@
|
||||
# Camera support
|
||||
|
||||
*Owns the three camera ownership models and what each one renders through.
|
||||
Defers the Snapmaker filament path to [Built-in agents](agents.md), even
|
||||
though the same subclass owns both.*
|
||||
|
||||
Camera support has three ownership models. They share the Device panel,
|
||||
but not a common frame or stream interface.
|
||||
|
||||
Two render surfaces, never one:
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
BU["Bambu URL provider<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.
|
||||
248
docs/printer-agent/connection-and-status.md
Normal file
248
docs/printer-agent/connection-and-status.md
Normal file
@@ -0,0 +1,248 @@
|
||||
# Connection and status
|
||||
|
||||
*Owns the runtime sequence in order: selecting an agent and machine,
|
||||
starting a connection, receiving status, sending commands. Defers the
|
||||
structural rules those steps must obey - ownership, no-fallback, unknown
|
||||
IDs, threading - to [Architecture](architecture.md), and cites them at the
|
||||
point where they bite.*
|
||||
|
||||
## The four runtime concepts
|
||||
|
||||
Keep these concepts separate when tracing a connection problem:
|
||||
|
||||
- A preset stores an agent ID and printer address.
|
||||
- `NetworkAgent` holds the active printer agent for that agent ID.
|
||||
- `MachineObject` represents the selected printer at that address.
|
||||
- Freshness and status-confirmed readiness are separate states.
|
||||
|
||||
An agent ID selects a printer agent implementation. A device ID selects one
|
||||
printer within that implementation.
|
||||
|
||||
A non-Bambu printer reaches the machine list through **Bind with Access
|
||||
Code**, the tile in the Device tab's machine-select popup. The user enters
|
||||
an address and an access code, `bind_detect()` probes the address before
|
||||
any connect, and `DeviceManager::insert_local_device()` creates the
|
||||
`MachineObject`. For the Moonraker family the address itself becomes the
|
||||
device ID: `MoonrakerPrinterAgent::bind_detect()` seeds `dev_name` and
|
||||
`dev_id` from the entered address, so an unreachable or unnamed printer
|
||||
still shows up as its IP rather than blank.
|
||||
|
||||
Binding is the only route for that family. `MoonrakerPrinterAgent::start_discovery()`
|
||||
deliberately announces nothing, because a partial discovery implementation
|
||||
would populate the machine list with stale hosts. Bambu is the exception:
|
||||
it has its own discovery identity and does not use the address as an ID.
|
||||
|
||||
`DeviceManager::selected_machine` is only a selected ID. It can name no
|
||||
resolvable object. `get_selected_machine()` answers whether an object is
|
||||
actually available. `set_selected_machine()` accepting an ID therefore
|
||||
does not prove the printer is connected. The selected ID can remain when its
|
||||
object is unavailable, so connection state must come from the object itself.
|
||||
|
||||
## Selecting the agent and machine
|
||||
|
||||
`GUI_App::switch_printer_agent()` reads the edited printer preset and
|
||||
resolves its stored agent ID through `NetworkAgentFactory`.
|
||||
|
||||
1. An empty stored ID is a legacy sentinel. It resolves to `bbl` for a
|
||||
Bambu vendor preset and to `orca` otherwise.
|
||||
2. If that effective ID is registered, the factory provides the matching
|
||||
printer agent implementation.
|
||||
3. Clear the live printer agent only when a nonempty stored ID is unregistered
|
||||
or the factory cannot construct the matching registered agent.
|
||||
4. When the active printer agent changes, clear the current selection, user
|
||||
selection, stale device discoveries, sidebar state, and AMS state before
|
||||
installing the replacement.
|
||||
5. Select the preset's address-derived machine for non-Bambu agents.
|
||||
|
||||
The lifetimes are easier to see than to read. Note that the agent pointer
|
||||
can be unchanged while the machine still must be re-selected - that is the
|
||||
trap in the same-agent path below:
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant U as User
|
||||
participant P as Printer preset
|
||||
participant G as GUI_App::switch_printer_agent
|
||||
participant F as NetworkAgentFactory
|
||||
participant N as NetworkAgent
|
||||
participant M as MachineObject
|
||||
|
||||
U->>P: Edit or switch the printer preset
|
||||
P->>G: printer_agent value, possibly empty
|
||||
G->>G: resolve_printer_agent_id - empty becomes bbl or orca
|
||||
G->>F: get_printer_agent_info(effective ID)
|
||||
|
||||
alt ID not registered, and stored value was nonempty
|
||||
F-->>G: no info
|
||||
G->>N: clear the live printer agent
|
||||
Note over N: null is inert - never fall back to another agent
|
||||
else ID registered
|
||||
F->>F: create once per ID, then serve from cache
|
||||
F-->>G: the cached IPrinterAgent for this ID
|
||||
alt Agent pointer changed
|
||||
G->>N: clear selection, user selection, discoveries, sidebar, AMS
|
||||
G->>N: install the replacement agent
|
||||
else Same cached pointer returned
|
||||
Note over G,F: Two presets can share one agent ID
|
||||
end
|
||||
G->>M: compare device ID, re-select if the address differs
|
||||
end
|
||||
```
|
||||
|
||||
> **Do not use the first available machine as a fallback** (rule owned by
|
||||
> [Architecture](architecture.md), Runtime objects and ownership). It
|
||||
> connects to a printer the user did not choose, including one owned by a
|
||||
> different printer agent.
|
||||
|
||||
The same-agent path is important too. Two presets can use one agent type
|
||||
but point at different addresses, and the factory caches one agent per ID,
|
||||
so switching between them returns the same pointer and would otherwise
|
||||
skip reselection entirely. Re-select the machine whenever the preset's
|
||||
address changes, even when the factory returned the same active agent.
|
||||
Otherwise status and filament work can continue against the previous
|
||||
printer.
|
||||
|
||||
Note: this is a legacy coupling, not the primary workflow. It reads an
|
||||
address stored on the printer preset itself (`print_host` and
|
||||
`printhost_port`, named here only so the keys can be found in the code)
|
||||
and derives a device ID from it with `dev_id_from_address()`. Those keys
|
||||
predate printer agents and are edited through `PhysicalPrinterDialog`,
|
||||
which despite its name writes the printer preset rather than a
|
||||
`PhysicalPrinter` object - that object is no longer constructed. Printers
|
||||
normally arrive through Bind with Access Code instead, which does not
|
||||
touch the preset. Both routes end at `insert_local_device()`, so they must
|
||||
agree on the device ID: `dev_id_from_address()` strips the URL scheme and
|
||||
drops an empty port, while the bind path stores the address as the user
|
||||
typed it.
|
||||
|
||||
The unknown-`coString` compatibility rule belongs to `architecture.md` under
|
||||
Backward compatibility. Keep a nonempty unknown `printer_agent` ID unchanged
|
||||
and display a missing state if needed; do not rewrite it during plugin unload
|
||||
or choose an arbitrary replacement, so the preset can round-trip while its
|
||||
plugin is temporarily unavailable.
|
||||
|
||||
## Starting a connection
|
||||
|
||||
Machine selection causes `MachineObject::connect()` to invoke the active
|
||||
agent's `connect_printer()` with the selected address and effective access
|
||||
code. A success return means that the connection attempt started. It does
|
||||
not mean that the printer is ready or that a status stream is alive.
|
||||
|
||||
Moonraker-family agents must force HTTP. Moonraker and print-host
|
||||
installations commonly serve plain HTTP, while the generic machine path
|
||||
can request TLS by default. Passing that default through turns a valid
|
||||
connection into an HTTPS request the printer will refuse. The agent therefore
|
||||
must keep the connection on HTTP unless its protocol support changes
|
||||
deliberately and is verified.
|
||||
|
||||
## Access codes: four coordinated slots
|
||||
|
||||
One effective access code can live in four places:
|
||||
|
||||
| Slot | Location | Purpose |
|
||||
| --- | --- | --- |
|
||||
| Device runtime | `MachineObject::access_code` | Code learned from the device. |
|
||||
| User runtime | `MachineObject::user_access_code` | Code entered by the user. |
|
||||
| Device config | `access_code[dev_id]` | Persisted device value. |
|
||||
| User config | `user_access_code[dev_id]` | Persisted user value. |
|
||||
|
||||
The effective code prefers the user value when present, then the device
|
||||
value. Keep user input in the user path and device replies in the device
|
||||
path. Crossing those paths obscures which value should win.
|
||||
|
||||
`set_access_code()` deliberately does not save configuration immediately.
|
||||
Device replies and polls can update it often; forcing a full config write
|
||||
for each message adds unnecessary work. The normal deferred config save
|
||||
persists dirty state later. Do not add an eager save just to make this one
|
||||
path symmetric: device replies and polls update it often, so a config
|
||||
write per message is wasted work.
|
||||
|
||||
> **Do not erase the user access code when a printer connects.** On the
|
||||
> LAN reselection path that code can be the only credential that lets the
|
||||
> machine pass the access check and receive the status or access-code
|
||||
> reply that would refresh it, so erasing it at connection time can leave
|
||||
> the machine permanently unable to receive updates. A failed connection
|
||||
> is the place to handle a proven bad credential.
|
||||
|
||||
## Receiving status
|
||||
|
||||
An agent receives native status, translates it to the existing payload
|
||||
shape, and dispatches it to the matching `MachineObject`. The object
|
||||
parses the payload and records when it last received an update.
|
||||
|
||||
Readiness is four states, and three of them look connected:
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> SelectedIdOnly
|
||||
SelectedIdOnly: Selected ID only
|
||||
SelectedIdOnly: selected_machine names no resolvable object
|
||||
SelectedIdOnly --> FreshWindow: connect_printer returns success
|
||||
|
||||
FreshWindow: Fresh window
|
||||
FreshWindow: reset seeded the update time - no status yet
|
||||
FreshWindow --> Connecting: still fresh, still no push-status
|
||||
|
||||
Connecting: Connecting
|
||||
Connecting: is_connecting true - the honest state
|
||||
Connecting --> Ready: first push-status message parsed
|
||||
|
||||
Ready: Status-confirmed ready
|
||||
Ready: the only state that proves a usable printer
|
||||
Ready --> Stale: update time ages out
|
||||
|
||||
Stale: Stale
|
||||
Stale: is_connected false
|
||||
Stale --> Ready: a later status message arrives
|
||||
|
||||
note right of FreshWindow
|
||||
is_connected() is true from here on.
|
||||
It is a freshness test over the update
|
||||
time - not proof that any status arrived.
|
||||
end note
|
||||
```
|
||||
|
||||
`is_connected()` is a freshness test over the update time. It does not
|
||||
describe whether `connect_printer()` returned success or whether any status
|
||||
message arrived: reset initializes the update time, creating an initial
|
||||
freshness window. `is_connecting()` distinguishes that window from
|
||||
status-confirmed readiness: while the object is fresh and no push-status
|
||||
message has arrived, it remains connecting.
|
||||
|
||||
> **Do not treat freshness or a successful connect as proof of readiness.**
|
||||
> Code that needs a usable printer must wait for status-confirmed
|
||||
> readiness, because the fresh window exists before any status has been
|
||||
> parsed.
|
||||
|
||||
The UI-thread mutation rule belongs to `architecture.md` under Threading rule.
|
||||
Dispatch the status callback to the UI thread before changing device maps,
|
||||
selection, or `MachineObject` state, because network callbacks may run in a
|
||||
worker thread and mutating these structures there races with the Device tab.
|
||||
|
||||
## Sending commands
|
||||
|
||||
`MachineObject` builds the established command JSON and sends it through
|
||||
the active `NetworkAgent`. The agent translates it or returns an explicit
|
||||
unsupported result. It must not report success when no translation exists.
|
||||
|
||||
The `pushing` command exception belongs to `architecture.md` under Commands
|
||||
and unsupported work. It asks for status, and a working status stream already
|
||||
supplies it, so accepting it avoids false unsupported warnings from the Device
|
||||
Manager's repeated keepalive.
|
||||
|
||||
## Maintainer constraints
|
||||
|
||||
- Preserve same-agent reselection by address, because an agent type can
|
||||
serve more than one printer.
|
||||
- Preserve the null-agent, no-fallback, and unknown-`coString` rules in
|
||||
`architecture.md`; selection must remain an explicit user or preset choice,
|
||||
and stale state must not belong to a replacement printer agent.
|
||||
- An empty value is the legacy Bambu-or-Orca sentinel, not a missing printer
|
||||
agent.
|
||||
- Preserve deferred access-code saves and the no-on-connect-erase rule;
|
||||
they prevent excessive config writes and credential-driven status loss.
|
||||
- Keep Moonraker connections HTTP-only unless the agent's protocol support
|
||||
changes deliberately and is verified.
|
||||
- Do not treat freshness as proof that status arrived; wait for
|
||||
status-confirmed readiness. The UI-thread mutation rule is in
|
||||
`architecture.md` under Threading rule.
|
||||
110
docs/printer-agent/filament.md
Normal file
110
docs/printer-agent/filament.md
Normal file
@@ -0,0 +1,110 @@
|
||||
# Filament synchronization and mapping
|
||||
|
||||
*Owns the three filament stages end to end: acquiring printer state,
|
||||
selecting a mapping, and delivering it at print time. Defers the upload
|
||||
and start mechanics to [Printing](printing.md), and per-vendor discovery
|
||||
detail to [Built-in agents](agents.md).*
|
||||
|
||||
Filament support has three separate stages. A successful first stage does
|
||||
not mean that a selected mapping will be delivered to the printer.
|
||||
|
||||
Stage 3 is where the two paths diverge, and only one of them reaches the
|
||||
printer:
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
SRC["Filament source<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.
|
||||
161
docs/printer-agent/plugin-agents.md
Normal file
161
docs/printer-agent/plugin-agents.md
Normal file
@@ -0,0 +1,161 @@
|
||||
# Python printer-agent plugins
|
||||
|
||||
*Owns the plugin bridge: the implementation contract, registration and
|
||||
lifetime, and the audit scope. Defers what the agent must do once live to
|
||||
[Architecture](architecture.md) and
|
||||
[Connection and status](connection-and-status.md).*
|
||||
|
||||
Python printer agents use the current capability bridge. They do not use a
|
||||
separate adapter or a Moonraker-specific plugin path.
|
||||
|
||||
## What an agent must implement
|
||||
|
||||
Not overriding a member of `IPrinterAgent` has four different
|
||||
consequences depending on which tier it is in. This is the whole plugin
|
||||
contract:
|
||||
|
||||
| Tier | Members | Consequence of not overriding |
|
||||
| --- | --- | --- |
|
||||
| Pure virtual | `connect_printer`, `disconnect_printer`, `send_message_to_printer`, the `start_*` print operations, `start_discovery`, `bind`, `bind_detect`, `unbind`, the callback setters, `set_cloud_agent`, `get_agent_info`, and the rest of the pure surface | Compile error |
|
||||
| Concrete, succeeds | `start_subscribe`, `stop_subscribe`, `add_subscribe`, `del_subscribe` | Silently returns `BAMBU_NETWORK_SUCCESS` |
|
||||
| Concrete, declines | `command_ams_refresh_rfid`, `command_ams_calibrate`, `command_ams_select_tray`, `command_start_camera` | Silently returns `ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED` |
|
||||
| Concrete, inert | `get_filament_sync_mode`, `fetch_filament_info` | Reports `FilamentSyncMode::none` and `false` - no filament capability at all |
|
||||
|
||||
The refusal tier is deliberate: those commands carry Bambu-dialect G-code
|
||||
in their bodies, so the honest default is a refusal that
|
||||
`MachineObject::publish_json()` turns into a dialog. The success tier is
|
||||
equally deliberate - a printer whose status already streams needs no
|
||||
subscription call.
|
||||
|
||||
> **Do not assume a missing override quietly inherits useful behavior, and
|
||||
> do not assume it fails loudly either.** Only the first tier fails at
|
||||
> compile time. The second silently reports success, the third silently
|
||||
> declines, and the fourth silently reports no filament capability.
|
||||
|
||||
## The plugin contract
|
||||
|
||||
A plugin subclasses `printer_agent.PrinterAgentBase`, the Python binding for
|
||||
`PrinterAgentPluginCapability`. The capability itself is the live native
|
||||
`IPrinterAgent`; there is no intermediate protocol adapter, because
|
||||
`PrinterAgentPluginCapability` inherits both `PluginCapabilityInterface`
|
||||
and `IPrinterAgent` directly.
|
||||
|
||||
`get_type()` stays a `PluginCapabilityInterface` method and
|
||||
`set_cloud_agent()` remains the native host injection point. A plugin must
|
||||
implement the pure connection, communication, discovery, binding, print,
|
||||
callback-registration, and filament-refresh operations. The certificate,
|
||||
bind-ticket, HMS-snapshot, and user-selected-machine members are pure too;
|
||||
the table above abridges the list.
|
||||
|
||||
The only tracked Python printer-agent implementation is the BBL plugin. There
|
||||
is no Python Moonraker printer agent in the current source tree. Moonraker is
|
||||
implemented by the built-in C++ class.
|
||||
|
||||
## Registration and lifetime
|
||||
|
||||
When an enabled plugin advertises a printer-connection capability, the factory
|
||||
gets its `AgentInfo` and registers a factory under `AgentInfo.id`. This is the
|
||||
same registry used for built-in agents.
|
||||
|
||||
Two similarly named structs are involved, and they are not the same thing.
|
||||
`AgentInfo` is what the agent says about itself; `PrinterAgentInfo` is the
|
||||
registry's entry about it:
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class AgentInfo {
|
||||
<<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`
|
||||
138
docs/printer-agent/printing.md
Normal file
138
docs/printer-agent/printing.md
Normal file
@@ -0,0 +1,138 @@
|
||||
# Printing through printer agents
|
||||
|
||||
*Owns the send path: connection choices, preflight, upload, start, and the
|
||||
two recovery flows. Defers filament mapping delivery to
|
||||
[Filament synchronization](filament.md), which is a separate contract even
|
||||
though it is applied at print time.*
|
||||
|
||||
This chapter describes the printer-agent send path. It is separate from
|
||||
the older print-host implementation, even when both target Moonraker.
|
||||
Keep the paths separate unless their contracts and failure handling can
|
||||
be deliberately reconciled.
|
||||
|
||||
## Connection choices
|
||||
|
||||
Three connection paths are in use:
|
||||
|
||||
| Target | Connection path | Use |
|
||||
| --- | --- | --- |
|
||||
| Native Bambu | Custom TLS tunnel on port 6000 | Send and optional eMMC preflight |
|
||||
| Bambu Python agent | Implicit FTPS on port 990 | Upload and Bambu preflight fallback |
|
||||
| Moonraker family | HTTP | Upload and start print |
|
||||
|
||||
The Bambu connection paths are independent. Selecting one does not prove that
|
||||
the other is available. The Moonraker agent uploads with a multipart
|
||||
request to its `gcodes` storage and then starts the uploaded filename;
|
||||
it does not reuse the legacy `Moonraker` print-host class.
|
||||
|
||||
## Bambu native tunnel
|
||||
|
||||
The native tunnel depends on the versioned networking DLL and its
|
||||
file-transfer module. `InitFTModule()` is a single-owner initialization:
|
||||
it rejects a second call. Any future shared initialization must therefore
|
||||
be idempotent, while `BBLNetworkPlugin` remains the single teardown owner.
|
||||
It must call `UnloadFTModule()` before freeing the DLL, otherwise the
|
||||
module's function pointers can point into unloaded code.
|
||||
|
||||
There is currently an initialization gap: selecting a printer agent does
|
||||
not initialize this module. It is initialized only when the
|
||||
`installed_networking` option causes the native BBL network plugin to
|
||||
initialize. Calls to the tunnel must continue to fail safely until that
|
||||
path has initialized the module. The Send UI catches this failure and
|
||||
reports an initialization error instead of letting an exception leave a
|
||||
wx event handler.
|
||||
|
||||
## Bambu FTPS upload
|
||||
|
||||
The Python Bambu agent uses implicit FTPS on port 990. Its live upload
|
||||
path closes the data connection, then waits at most two seconds for the
|
||||
control response with `voidresp()`. A `TimeoutError` is accepted as a
|
||||
completed transfer. An `error_reply` is also accepted when its reply
|
||||
begins with `200`. This is the behavior to preserve.
|
||||
|
||||
Do not describe the path as using TLS `unwrap()`: the live construction
|
||||
does not enable it. Enabling it without a bounded wait could hang while
|
||||
waiting for the peer's TLS close notification. The current timeout-based
|
||||
handling has not been verified on hardware against every printer and FTP
|
||||
server combination.
|
||||
|
||||
## Print preflight and recovery
|
||||
|
||||
For normal LAN prints, `PrintJob` performs a preflight before the real
|
||||
send. When eMMC is eligible it tries the native tunnel, then it sends a
|
||||
small `verify_job` upload through the selected agent. The latter is a real
|
||||
upload, not a special protocol command. Non-Bambu agents therefore upload
|
||||
the probe too.
|
||||
|
||||
> **Do not re-enable eMMC by default** without hardware coverage for the
|
||||
> affected devices. It is opt-in because the tunnel can hang during upload
|
||||
> on some printers.
|
||||
|
||||
The whole send, including the thread hop and the recovery fork:
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant UI as Send UI (UI thread)
|
||||
participant J as PrintJob (worker)
|
||||
participant A as Selected IPrinterAgent
|
||||
participant P as Printer
|
||||
|
||||
UI->>J: Start send
|
||||
opt eMMC eligible - off by default
|
||||
J->>P: Native Bambu tunnel attempt
|
||||
Note over J,P: Can hang on some printers
|
||||
end
|
||||
J->>A: verify_job preflight
|
||||
Note over J,A: A real upload, not a protocol probe -<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.
|
||||
137
docs/printer-agent/reference/capability-matrix.md
Normal file
137
docs/printer-agent/reference/capability-matrix.md
Normal file
@@ -0,0 +1,137 @@
|
||||
# Printer-agent capability matrix
|
||||
|
||||
This is a compact lookup for the built-in Moonraker family. It combines
|
||||
implementation state with recorded evidence. It is not a promise that every
|
||||
firmware configuration behaves the same way. Python plug-in behavior depends
|
||||
on the plug-in, not on this matrix.
|
||||
|
||||
Use [testing and troubleshooting](../testing.md) before calling a live-printer
|
||||
result complete.
|
||||
|
||||
For a quicker tour of the controls users actually see, open the
|
||||
[annotated Device-tab view](device-tab-annotations.html). The annotations
|
||||
explain the important routing constraints; this matrix remains the compact
|
||||
cross-agent reference.
|
||||
|
||||
## Status definitions
|
||||
|
||||
- Supported - implemented, with a relevant live-printer result recorded.
|
||||
- Partial - an important condition, limitation, or defect applies.
|
||||
- Unsupported - no applicable implementation, or deliberate refusal.
|
||||
- Not verified - implemented or source-inspected, but without a relevant live
|
||||
result.
|
||||
|
||||
`Base` means `MoonrakerPrinterAgent`. Qidi, Creality, and Snapmaker inherit
|
||||
from it unless a row identifies an override.
|
||||
|
||||
## Connection and status
|
||||
|
||||
| Capability | Base | Qidi | Creality | Snapmaker |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| Direct LAN connection with API key | Supported | Supported | Not verified | Not verified |
|
||||
| WebSocket status updates | Supported | Supported | Not verified | Not verified |
|
||||
| Reconnect and fresh status | Partial | Partial | Partial | Partial |
|
||||
| Discovery and cloud binding | Unsupported | Unsupported | Unsupported | Unsupported |
|
||||
| Device identity with a configured port | Partial | Partial | Partial | Partial |
|
||||
|
||||
- Reconnect completion can leave the Device tab with stale status.
|
||||
- A bare IP and `host:port` can become separate device identities.
|
||||
- The Base and Qidi Supported grades come from prior hardware sessions. They
|
||||
were carried into this rewrite and not rerun.
|
||||
|
||||
## Controls
|
||||
|
||||
| Capability | Base | Qidi | Creality | Snapmaker |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| Home and arbitrary G-code | Not verified | Not verified | Not verified | Not verified |
|
||||
| Bed and nozzle temperature | Not verified | Not verified | Not verified | Not verified |
|
||||
| Pause, resume, and cancel | Not verified | Not verified | Not verified | Not verified |
|
||||
| Configured chamber light | Partial | Partial | Partial | Partial |
|
||||
| Jog and manual extrusion | Partial | Partial | Not verified | Not verified |
|
||||
| Legacy part-fan speed control | Partial | Partial | Not verified | Not verified |
|
||||
| Structured fan, chamber, and AI controls | Unsupported | Unsupported | Unsupported | Unsupported |
|
||||
| AMS RFID, calibration, and tray control | Unsupported | Unsupported | Unsupported | Unsupported |
|
||||
|
||||
- Chamber light needs a recognised light object.
|
||||
- Base and Qidi jog works, but can leave relative positioning active. Do not
|
||||
use it as a general safe-control test until its G-code state is restored.
|
||||
- Base and Qidi part-fan control works through legacy `gcode_line` while
|
||||
`is_enable_np` is false. Adding `cfg`, `fun`, `aux`, and `stat` flips that
|
||||
flag and routes fan and extruder controls to unsupported structured commands.
|
||||
- Creality and Snapmaker inherit the source path but have no separate live
|
||||
evidence for jog or fan control.
|
||||
|
||||
## Printing
|
||||
|
||||
| Capability | Base | Qidi | Creality | Snapmaker |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| Upload G-code without starting | Not verified | Not verified | Not verified | Not verified |
|
||||
| Upload and start a local print | Not verified | Not verified | Not verified | Not verified |
|
||||
| Mapped multi-material print | Unsupported | Not verified | Unsupported | Unsupported |
|
||||
| Cloud or SD-card start variants | Unsupported | Partial | Unsupported | Unsupported |
|
||||
| Cancel during upload | Not verified | Not verified | Not verified | Not verified |
|
||||
| Send with no nozzle identity | Not applicable | Not verified | Not applicable | Not applicable |
|
||||
|
||||
- Qidi applies mapping before it routes the real local print path.
|
||||
- Some Qidi print variants can reach base success stubs after mapping.
|
||||
- Qidi tolerates missing nozzle data in source, but that Send preflight is not
|
||||
hardware-verified.
|
||||
|
||||
## Filament
|
||||
|
||||
| Capability | Base | Qidi | Creality | Snapmaker |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| Sync mode | Not verified | Not verified | Not verified | Not verified |
|
||||
| Read installed material and slots | Partial | Not verified | Not verified | Not verified |
|
||||
| Slot, material, and colour refresh | Unsupported | Not verified | Not verified | Not verified |
|
||||
| Cleanup after removed material | Not verified | Not verified | Not verified | Not verified |
|
||||
| Load, unload, or write a slot | Unsupported | Partial | Unsupported | Unsupported |
|
||||
| Auto Refill | Unsupported | Unsupported | Unsupported | Unsupported |
|
||||
|
||||
- All built-in agents use pull-mode sync.
|
||||
- Base reads Happy Hare or AFC data when present. Qidi has print-time mapping;
|
||||
Creality has CFS logic; Snapmaker reads printer arrays and NFC data.
|
||||
- The Base Device-tab slot refresh uses a proprietary AMS command and has no
|
||||
generic Moonraker translation.
|
||||
- Generic write-side macros stay unsupported until their printer contract is
|
||||
known and verified.
|
||||
|
||||
## Camera
|
||||
|
||||
| Capability | Base | Qidi | Creality | Snapmaker |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| Discover a Moonraker webcam | Not verified | Not verified | Not verified | Unsupported |
|
||||
| Provide a camera source | Not verified | Not verified | Not verified | Not verified |
|
||||
| Live camera view | Not verified | Not verified | Not verified | Not verified |
|
||||
| Snapshot-only camera start | Unsupported | Unsupported | Unsupported | Not verified |
|
||||
| Camera start renewal and teardown | Unsupported | Unsupported | Unsupported | Partial |
|
||||
| Print thumbnail | Not verified | Not verified | Not verified | Not verified |
|
||||
|
||||
- Snapmaker bypasses webcam discovery with a local snapshot-polling page.
|
||||
- Its renewal and teardown path has lifetime risks without live evidence.
|
||||
- Moonraker thumbnail endpoint responses and filename-cache behavior need live
|
||||
coverage.
|
||||
|
||||
## Python plug-ins
|
||||
|
||||
| Capability | Python plug-in agent |
|
||||
| --- | --- |
|
||||
| Registration and re-registration | Not verified - lifecycle tests cover replacement |
|
||||
| Duplicate agent ID | Not verified - conflict is rejected and reported |
|
||||
| Disable or unload | Not verified - deregistration is tested; session teardown needs coverage |
|
||||
| Capability surface | Defined by the plug-in and exposed Python API |
|
||||
|
||||
## Reading the matrix safely
|
||||
|
||||
- `Partial` is not a softer form of `Supported`. It names a condition that must
|
||||
be checked before use. `Not verified` means the code was found, but no
|
||||
relevant live result is recorded.
|
||||
- Pair each claim with the evidence grades in the testing guide. This matters
|
||||
especially for CFS, Snapmaker camera, MMU macros, Qidi Send preflight, and
|
||||
thumbnail endpoints.
|
||||
|
||||
## Background - deliberate exclusions
|
||||
|
||||
The matrix excludes Bambu-specific cloud binding, RFID, calibration, and
|
||||
camera-control features from the Moonraker family. They use different protocol
|
||||
contracts and are deliberately refused when no safe Klipper equivalent exists.
|
||||
971
docs/printer-agent/reference/device-tab-annotations.html
Normal file
971
docs/printer-agent/reference/device-tab-annotations.html
Normal file
File diff suppressed because one or more lines are too long
280
docs/printer-agent/reference/manual-checklist.html
Normal file
280
docs/printer-agent/reference/manual-checklist.html
Normal file
@@ -0,0 +1,280 @@
|
||||
<!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>
|
||||
249
docs/printer-agent/testing.md
Normal file
249
docs/printer-agent/testing.md
Normal file
@@ -0,0 +1,249 @@
|
||||
# Testing and troubleshooting printer agents
|
||||
|
||||
*Owns evidence grades, the automated and manual verification passes, and
|
||||
the open-defect register. Every "not hardware-verified" note elsewhere in
|
||||
this guide resolves to a grade defined here.*
|
||||
|
||||
This page describes how to assess a printer-agent change without treating
|
||||
source inspection as a hardware result. Use the
|
||||
[manual checklist](reference/manual-checklist.html) for a repeatable live-printer
|
||||
pass, and use the [capability matrix](reference/capability-matrix.md) to decide
|
||||
which cases apply to the agent being changed.
|
||||
|
||||
## Evidence grades
|
||||
|
||||
Keep these grades separate in reviews and release notes.
|
||||
|
||||
- Source-inspected - the current implementation was read. It establishes
|
||||
intended behavior, not printer compatibility.
|
||||
- Automated - a targeted test ran. It covers its inputs and assertions, not a
|
||||
printer, firmware version, or network failure that it does not model.
|
||||
- Hardware-verified - the stated behavior was observed on a named class of
|
||||
live printer. Record the model, firmware, configuration, and result with the
|
||||
test evidence.
|
||||
|
||||
Do not call a capability supported by hardware solely because the code compiles or
|
||||
a unit test passes. Conversely, a hardware observation should not be generalized
|
||||
to every Moonraker-family printer without checking its configuration.
|
||||
|
||||
## Carried hardware evidence
|
||||
|
||||
Prior hardware sessions verified direct Moonraker-family connection, live
|
||||
WebSocket status, and jog on both a Qidi/Moonraker printer and a generic
|
||||
Moonraker box. The jog moved the printer but could leave relative positioning
|
||||
active. This evidence establishes the Moonraker-base and Qidi grades in the
|
||||
capability matrix. It was carried into this rewrite and was not rerun here.
|
||||
|
||||
It does not establish those behaviors for Creality or Snapmaker, and it does
|
||||
not cover Qidi-specific filament discovery, box mapping, or print wrappers.
|
||||
|
||||
## Build and automated tests
|
||||
|
||||
Run the smallest relevant test target first, then broaden the run if the change
|
||||
crosses shared agent, plug-in, or Device-tab code. Set
|
||||
`<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`.
|
||||
1243
resources/orca_plugins/BBLPrinterAgentPlugin.py
Normal file
1243
resources/orca_plugins/BBLPrinterAgentPlugin.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Custom Printer",
|
||||
"version": "02.04.00.02",
|
||||
"version": "02.04.00.01",
|
||||
"force_update": "0",
|
||||
"description": "My configurations",
|
||||
"machine_model_list": [
|
||||
|
||||
@@ -24,6 +24,12 @@
|
||||
"filament_loading_speed_start": [
|
||||
"50"
|
||||
],
|
||||
"filament_multitool_ramming": [
|
||||
"1"
|
||||
],
|
||||
"filament_multitool_ramming_flow": [
|
||||
"40"
|
||||
],
|
||||
"filament_stamping_distance": [
|
||||
"45"
|
||||
],
|
||||
|
||||
@@ -24,6 +24,12 @@
|
||||
"filament_loading_speed_start": [
|
||||
"50"
|
||||
],
|
||||
"filament_multitool_ramming": [
|
||||
"1"
|
||||
],
|
||||
"filament_multitool_ramming_flow": [
|
||||
"40"
|
||||
],
|
||||
"filament_stamping_distance": [
|
||||
"45"
|
||||
],
|
||||
|
||||
@@ -24,6 +24,12 @@
|
||||
"filament_loading_speed_start": [
|
||||
"50"
|
||||
],
|
||||
"filament_multitool_ramming": [
|
||||
"1"
|
||||
],
|
||||
"filament_multitool_ramming_flow": [
|
||||
"40"
|
||||
],
|
||||
"filament_stamping_distance": [
|
||||
"45"
|
||||
],
|
||||
|
||||
@@ -24,6 +24,12 @@
|
||||
"filament_loading_speed_start": [
|
||||
"50"
|
||||
],
|
||||
"filament_multitool_ramming": [
|
||||
"1"
|
||||
],
|
||||
"filament_multitool_ramming_flow": [
|
||||
"40"
|
||||
],
|
||||
"filament_stamping_distance": [
|
||||
"45"
|
||||
],
|
||||
|
||||
@@ -25,6 +25,12 @@
|
||||
"filament_loading_speed_start": [
|
||||
"50"
|
||||
],
|
||||
"filament_multitool_ramming": [
|
||||
"1"
|
||||
],
|
||||
"filament_multitool_ramming_flow": [
|
||||
"40"
|
||||
],
|
||||
"filament_stamping_distance": [
|
||||
"45"
|
||||
],
|
||||
|
||||
@@ -25,6 +25,12 @@
|
||||
"filament_loading_speed_start": [
|
||||
"50"
|
||||
],
|
||||
"filament_multitool_ramming": [
|
||||
"1"
|
||||
],
|
||||
"filament_multitool_ramming_flow": [
|
||||
"40"
|
||||
],
|
||||
"filament_stamping_distance": [
|
||||
"45"
|
||||
],
|
||||
|
||||
@@ -25,6 +25,12 @@
|
||||
"filament_loading_speed_start": [
|
||||
"50"
|
||||
],
|
||||
"filament_multitool_ramming": [
|
||||
"1"
|
||||
],
|
||||
"filament_multitool_ramming_flow": [
|
||||
"40"
|
||||
],
|
||||
"filament_stamping_distance": [
|
||||
"45"
|
||||
],
|
||||
|
||||
@@ -25,6 +25,12 @@
|
||||
"filament_loading_speed_start": [
|
||||
"50"
|
||||
],
|
||||
"filament_multitool_ramming": [
|
||||
"1"
|
||||
],
|
||||
"filament_multitool_ramming_flow": [
|
||||
"40"
|
||||
],
|
||||
"filament_stamping_distance": [
|
||||
"45"
|
||||
],
|
||||
|
||||
@@ -25,6 +25,12 @@
|
||||
"filament_loading_speed_start": [
|
||||
"50"
|
||||
],
|
||||
"filament_multitool_ramming": [
|
||||
"1"
|
||||
],
|
||||
"filament_multitool_ramming_flow": [
|
||||
"40"
|
||||
],
|
||||
"filament_stamping_distance": [
|
||||
"45"
|
||||
],
|
||||
|
||||
@@ -116,7 +116,7 @@
|
||||
"deretraction_speed": [
|
||||
"30"
|
||||
],
|
||||
"z_hop_types": "Slope Lift",
|
||||
"z_hop_types": "Normal Lift",
|
||||
"silent_mode": "0",
|
||||
"single_extruder_multi_material": "1",
|
||||
"change_filament_gcode": "",
|
||||
|
||||
@@ -118,7 +118,7 @@
|
||||
"deretraction_speed": [
|
||||
"30"
|
||||
],
|
||||
"z_hop_types": "Slope Lift",
|
||||
"z_hop_types": "Normal Lift",
|
||||
"silent_mode": "0",
|
||||
"single_extruder_multi_material": "1",
|
||||
"change_filament_gcode": "",
|
||||
|
||||
@@ -116,7 +116,7 @@
|
||||
"deretraction_speed": [
|
||||
"30"
|
||||
],
|
||||
"z_hop_types": "Slope Lift",
|
||||
"z_hop_types": "Normal Lift",
|
||||
"silent_mode": "0",
|
||||
"single_extruder_multi_material": "1",
|
||||
"change_filament_gcode": "",
|
||||
|
||||
@@ -172,11 +172,11 @@
|
||||
"0.4"
|
||||
],
|
||||
"z_hop_types": [
|
||||
"Slope Lift",
|
||||
"Slope Lift",
|
||||
"Slope Lift",
|
||||
"Slope Lift",
|
||||
"Slope Lift"
|
||||
"Normal Lift",
|
||||
"Normal Lift",
|
||||
"Normal Lift",
|
||||
"Normal Lift",
|
||||
"Normal Lift"
|
||||
],
|
||||
"purge_in_prime_tower": "0",
|
||||
"machine_pause_gcode": "M601",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "OrcaFilamentLibrary",
|
||||
"version": "02.04.00.04",
|
||||
"version": "02.04.00.03",
|
||||
"force_update": "0",
|
||||
"description": "Orca Filament Library",
|
||||
"filament_list": [
|
||||
|
||||
@@ -36,9 +36,6 @@
|
||||
"filament_max_volumetric_speed": [
|
||||
"8"
|
||||
],
|
||||
"filament_multitool_ramming_flow": [
|
||||
"8"
|
||||
],
|
||||
"filament_type": [
|
||||
"PET-CF"
|
||||
],
|
||||
|
||||
@@ -39,9 +39,6 @@
|
||||
"filament_max_volumetric_speed": [
|
||||
"8"
|
||||
],
|
||||
"filament_multitool_ramming_flow": [
|
||||
"8"
|
||||
],
|
||||
"filament_vendor": [
|
||||
"Bambu Lab"
|
||||
],
|
||||
|
||||
@@ -39,9 +39,6 @@
|
||||
"filament_max_volumetric_speed": [
|
||||
"6"
|
||||
],
|
||||
"filament_multitool_ramming_flow": [
|
||||
"6"
|
||||
],
|
||||
"filament_vendor": [
|
||||
"Bambu Lab"
|
||||
],
|
||||
|
||||
@@ -21,9 +21,6 @@
|
||||
"filament_max_volumetric_speed": [
|
||||
"6"
|
||||
],
|
||||
"filament_multitool_ramming_flow": [
|
||||
"6"
|
||||
],
|
||||
"filament_type": [
|
||||
"PLA-AERO"
|
||||
],
|
||||
|
||||
@@ -27,9 +27,6 @@
|
||||
"filament_max_volumetric_speed": [
|
||||
"6"
|
||||
],
|
||||
"filament_multitool_ramming_flow": [
|
||||
"6"
|
||||
],
|
||||
"filament_scarf_seam_type": [
|
||||
"none"
|
||||
],
|
||||
|
||||
@@ -21,9 +21,6 @@
|
||||
"filament_max_volumetric_speed": [
|
||||
"6"
|
||||
],
|
||||
"filament_multitool_ramming_flow": [
|
||||
"6"
|
||||
],
|
||||
"filament_vendor": [
|
||||
"Bambu Lab"
|
||||
],
|
||||
|
||||
@@ -36,9 +36,6 @@
|
||||
"filament_max_volumetric_speed": [
|
||||
"1"
|
||||
],
|
||||
"filament_multitool_ramming_flow": [
|
||||
"1"
|
||||
],
|
||||
"filament_retraction_minimum_travel": [
|
||||
"3"
|
||||
],
|
||||
|
||||
@@ -72,6 +72,15 @@
|
||||
"fan_min_speed": [
|
||||
"10"
|
||||
],
|
||||
"filament_cooling_final_speed": [
|
||||
"0"
|
||||
],
|
||||
"filament_cooling_initial_speed": [
|
||||
"0"
|
||||
],
|
||||
"filament_cooling_moves": [
|
||||
"0"
|
||||
],
|
||||
"filament_cost": [
|
||||
"599"
|
||||
],
|
||||
@@ -90,12 +99,30 @@
|
||||
"filament_is_support": [
|
||||
"0"
|
||||
],
|
||||
"filament_loading_speed": [
|
||||
"0"
|
||||
],
|
||||
"filament_loading_speed_start": [
|
||||
"0"
|
||||
],
|
||||
"filament_long_retractions_when_cut": [
|
||||
"nil"
|
||||
],
|
||||
"filament_max_volumetric_speed": [
|
||||
"15"
|
||||
],
|
||||
"filament_minimal_purge_on_wipe_tower": [
|
||||
"0"
|
||||
],
|
||||
"filament_multitool_ramming": [
|
||||
"0"
|
||||
],
|
||||
"filament_multitool_ramming_flow": [
|
||||
"10"
|
||||
],
|
||||
"filament_multitool_ramming_volume": [
|
||||
"10"
|
||||
],
|
||||
"filament_notes": [
|
||||
""
|
||||
],
|
||||
@@ -138,6 +165,21 @@
|
||||
"filament_soluble": [
|
||||
"0"
|
||||
],
|
||||
"filament_stamping_distance": [
|
||||
"0"
|
||||
],
|
||||
"filament_stamping_loading_speed": [
|
||||
"0"
|
||||
],
|
||||
"filament_toolchange_delay": [
|
||||
"0"
|
||||
],
|
||||
"filament_unloading_speed": [
|
||||
"0"
|
||||
],
|
||||
"filament_unloading_speed_start": [
|
||||
"0"
|
||||
],
|
||||
"filament_vendor": [
|
||||
"Elas"
|
||||
],
|
||||
|
||||
@@ -42,6 +42,15 @@
|
||||
"fan_min_speed": [
|
||||
"20"
|
||||
],
|
||||
"filament_cooling_final_speed": [
|
||||
"0"
|
||||
],
|
||||
"filament_cooling_initial_speed": [
|
||||
"0"
|
||||
],
|
||||
"filament_cooling_moves": [
|
||||
"0"
|
||||
],
|
||||
"filament_cost": [
|
||||
"445"
|
||||
],
|
||||
@@ -60,12 +69,30 @@
|
||||
"filament_is_support": [
|
||||
"0"
|
||||
],
|
||||
"filament_loading_speed": [
|
||||
"0"
|
||||
],
|
||||
"filament_loading_speed_start": [
|
||||
"0"
|
||||
],
|
||||
"filament_long_retractions_when_cut": [
|
||||
"1"
|
||||
],
|
||||
"filament_max_volumetric_speed": [
|
||||
"18"
|
||||
],
|
||||
"filament_minimal_purge_on_wipe_tower": [
|
||||
"0"
|
||||
],
|
||||
"filament_multitool_ramming": [
|
||||
"0"
|
||||
],
|
||||
"filament_multitool_ramming_flow": [
|
||||
"10"
|
||||
],
|
||||
"filament_multitool_ramming_volume": [
|
||||
"10"
|
||||
],
|
||||
"filament_notes": [
|
||||
""
|
||||
],
|
||||
@@ -108,6 +135,21 @@
|
||||
"filament_soluble": [
|
||||
"0"
|
||||
],
|
||||
"filament_stamping_distance": [
|
||||
"0"
|
||||
],
|
||||
"filament_stamping_loading_speed": [
|
||||
"0"
|
||||
],
|
||||
"filament_toolchange_delay": [
|
||||
"0"
|
||||
],
|
||||
"filament_unloading_speed": [
|
||||
"0"
|
||||
],
|
||||
"filament_unloading_speed_start": [
|
||||
"0"
|
||||
],
|
||||
"filament_wipe": [
|
||||
"nil"
|
||||
],
|
||||
|
||||
@@ -71,6 +71,15 @@
|
||||
"fan_min_speed": [
|
||||
"60"
|
||||
],
|
||||
"filament_cooling_final_speed": [
|
||||
"0"
|
||||
],
|
||||
"filament_cooling_initial_speed": [
|
||||
"0"
|
||||
],
|
||||
"filament_cooling_moves": [
|
||||
"0"
|
||||
],
|
||||
"filament_cost": [
|
||||
"449.5"
|
||||
],
|
||||
@@ -89,12 +98,30 @@
|
||||
"filament_is_support": [
|
||||
"0"
|
||||
],
|
||||
"filament_loading_speed": [
|
||||
"0"
|
||||
],
|
||||
"filament_loading_speed_start": [
|
||||
"0"
|
||||
],
|
||||
"filament_long_retractions_when_cut": [
|
||||
"nil"
|
||||
],
|
||||
"filament_max_volumetric_speed": [
|
||||
"25"
|
||||
],
|
||||
"filament_minimal_purge_on_wipe_tower": [
|
||||
"0"
|
||||
],
|
||||
"filament_multitool_ramming": [
|
||||
"0"
|
||||
],
|
||||
"filament_multitool_ramming_flow": [
|
||||
"10"
|
||||
],
|
||||
"filament_multitool_ramming_volume": [
|
||||
"10"
|
||||
],
|
||||
"filament_notes": [
|
||||
""
|
||||
],
|
||||
@@ -137,6 +164,21 @@
|
||||
"filament_soluble": [
|
||||
"0"
|
||||
],
|
||||
"filament_stamping_distance": [
|
||||
"0"
|
||||
],
|
||||
"filament_stamping_loading_speed": [
|
||||
"0"
|
||||
],
|
||||
"filament_toolchange_delay": [
|
||||
"0"
|
||||
],
|
||||
"filament_unloading_speed": [
|
||||
"0"
|
||||
],
|
||||
"filament_unloading_speed_start": [
|
||||
"0"
|
||||
],
|
||||
"filament_vendor": [
|
||||
"Elas"
|
||||
],
|
||||
|
||||
@@ -71,6 +71,15 @@
|
||||
"fan_min_speed": [
|
||||
"100"
|
||||
],
|
||||
"filament_cooling_final_speed": [
|
||||
"0"
|
||||
],
|
||||
"filament_cooling_initial_speed": [
|
||||
"0"
|
||||
],
|
||||
"filament_cooling_moves": [
|
||||
"0"
|
||||
],
|
||||
"filament_cost": [
|
||||
"500"
|
||||
],
|
||||
@@ -89,12 +98,30 @@
|
||||
"filament_is_support": [
|
||||
"0"
|
||||
],
|
||||
"filament_loading_speed": [
|
||||
"0"
|
||||
],
|
||||
"filament_loading_speed_start": [
|
||||
"0"
|
||||
],
|
||||
"filament_long_retractions_when_cut": [
|
||||
"nil"
|
||||
],
|
||||
"filament_max_volumetric_speed": [
|
||||
"18"
|
||||
],
|
||||
"filament_minimal_purge_on_wipe_tower": [
|
||||
"0"
|
||||
],
|
||||
"filament_multitool_ramming": [
|
||||
"0"
|
||||
],
|
||||
"filament_multitool_ramming_flow": [
|
||||
"10"
|
||||
],
|
||||
"filament_multitool_ramming_volume": [
|
||||
"10"
|
||||
],
|
||||
"filament_notes": [
|
||||
""
|
||||
],
|
||||
@@ -137,6 +164,21 @@
|
||||
"filament_soluble": [
|
||||
"0"
|
||||
],
|
||||
"filament_stamping_distance": [
|
||||
"0"
|
||||
],
|
||||
"filament_stamping_loading_speed": [
|
||||
"0"
|
||||
],
|
||||
"filament_toolchange_delay": [
|
||||
"0"
|
||||
],
|
||||
"filament_unloading_speed": [
|
||||
"0"
|
||||
],
|
||||
"filament_unloading_speed_start": [
|
||||
"0"
|
||||
],
|
||||
"filament_vendor": [
|
||||
"Elas"
|
||||
],
|
||||
|
||||
@@ -24,9 +24,6 @@
|
||||
"filament_max_volumetric_speed": [
|
||||
"6"
|
||||
],
|
||||
"filament_multitool_ramming_flow": [
|
||||
"6"
|
||||
],
|
||||
"nozzle_temperature": [
|
||||
"290"
|
||||
],
|
||||
|
||||
@@ -35,9 +35,6 @@
|
||||
"filament_max_volumetric_speed": [
|
||||
"8"
|
||||
],
|
||||
"filament_multitool_ramming_flow": [
|
||||
"8"
|
||||
],
|
||||
"filament_vendor": [
|
||||
"Elegoo"
|
||||
],
|
||||
|
||||
@@ -12,9 +12,6 @@
|
||||
"filament_max_volumetric_speed": [
|
||||
"10"
|
||||
],
|
||||
"filament_multitool_ramming_flow": [
|
||||
"10"
|
||||
],
|
||||
"nozzle_temperature": [
|
||||
"220"
|
||||
],
|
||||
|
||||
@@ -44,8 +44,5 @@
|
||||
],
|
||||
"filament_max_volumetric_speed": [
|
||||
"10"
|
||||
],
|
||||
"filament_multitool_ramming_flow": [
|
||||
"10"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -44,8 +44,5 @@
|
||||
],
|
||||
"filament_max_volumetric_speed": [
|
||||
"8"
|
||||
],
|
||||
"filament_multitool_ramming_flow": [
|
||||
"8"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -10,8 +10,5 @@
|
||||
"filament_type": [
|
||||
"PA-CF"
|
||||
],
|
||||
"filament_multitool_ramming_flow": [
|
||||
"8"
|
||||
],
|
||||
"compatible_printers": []
|
||||
}
|
||||
|
||||
@@ -8,8 +8,5 @@
|
||||
"filament_max_volumetric_speed": [
|
||||
"20"
|
||||
],
|
||||
"filament_multitool_ramming_flow": [
|
||||
"20"
|
||||
],
|
||||
"compatible_printers": []
|
||||
}
|
||||
|
||||
@@ -17,9 +17,6 @@
|
||||
"filament_max_volumetric_speed": [
|
||||
"11.5"
|
||||
],
|
||||
"filament_multitool_ramming_flow": [
|
||||
"13"
|
||||
],
|
||||
"overhang_fan_speed": [
|
||||
"100"
|
||||
],
|
||||
|
||||
@@ -12,9 +12,6 @@
|
||||
"filament_max_volumetric_speed": [
|
||||
"18"
|
||||
],
|
||||
"filament_multitool_ramming_flow": [
|
||||
"25"
|
||||
],
|
||||
"slow_down_layer_time": [
|
||||
"4"
|
||||
],
|
||||
|
||||
@@ -8,9 +8,6 @@
|
||||
"filament_max_volumetric_speed": [
|
||||
"11"
|
||||
],
|
||||
"filament_multitool_ramming_flow": [
|
||||
"11"
|
||||
],
|
||||
"filament_retraction_length": [
|
||||
"0.8"
|
||||
],
|
||||
|
||||
@@ -15,9 +15,6 @@
|
||||
"filament_max_volumetric_speed": [
|
||||
"6"
|
||||
],
|
||||
"filament_multitool_ramming_flow": [
|
||||
"6"
|
||||
],
|
||||
"filament_type": [
|
||||
"PP-CF"
|
||||
],
|
||||
|
||||
@@ -15,9 +15,6 @@
|
||||
"filament_max_volumetric_speed": [
|
||||
"6"
|
||||
],
|
||||
"filament_multitool_ramming_flow": [
|
||||
"6"
|
||||
],
|
||||
"filament_type": [
|
||||
"PP-GF"
|
||||
],
|
||||
|
||||
@@ -17,9 +17,6 @@
|
||||
"filament_max_volumetric_speed": [
|
||||
"8"
|
||||
],
|
||||
"filament_multitool_ramming_flow": [
|
||||
"8"
|
||||
],
|
||||
"filament_vendor": [
|
||||
"Overture"
|
||||
],
|
||||
|
||||
@@ -23,9 +23,6 @@
|
||||
"filament_max_volumetric_speed": [
|
||||
"7.5"
|
||||
],
|
||||
"filament_multitool_ramming_flow": [
|
||||
"7.5"
|
||||
],
|
||||
"filament_vendor": [
|
||||
"Valment"
|
||||
],
|
||||
|
||||
@@ -43,12 +43,6 @@
|
||||
"filament_max_volumetric_speed": [
|
||||
"12"
|
||||
],
|
||||
"filament_multitool_ramming_flow": [
|
||||
"15"
|
||||
],
|
||||
"filament_multitool_ramming_volume": [
|
||||
"10"
|
||||
],
|
||||
"filament_type": [
|
||||
"ABS"
|
||||
],
|
||||
|
||||
@@ -43,12 +43,6 @@
|
||||
"filament_max_volumetric_speed": [
|
||||
"12"
|
||||
],
|
||||
"filament_multitool_ramming_flow": [
|
||||
"12"
|
||||
],
|
||||
"filament_multitool_ramming_volume": [
|
||||
"10"
|
||||
],
|
||||
"filament_type": [
|
||||
"ASA"
|
||||
],
|
||||
|
||||
@@ -41,15 +41,6 @@
|
||||
"filament_max_volumetric_speed": [
|
||||
"8"
|
||||
],
|
||||
"filament_multitool_ramming": [
|
||||
"0"
|
||||
],
|
||||
"filament_multitool_ramming_flow": [
|
||||
"6"
|
||||
],
|
||||
"filament_multitool_ramming_volume": [
|
||||
"0.1"
|
||||
],
|
||||
"filament_type": [
|
||||
"BVOH"
|
||||
],
|
||||
|
||||
@@ -66,12 +66,6 @@
|
||||
"filament_minimal_purge_on_wipe_tower": [
|
||||
"15"
|
||||
],
|
||||
"filament_multitool_ramming": [
|
||||
"1"
|
||||
],
|
||||
"filament_multitool_ramming_volume": [
|
||||
"5"
|
||||
],
|
||||
"filament_retract_before_wipe": [
|
||||
"nil"
|
||||
],
|
||||
|
||||
@@ -41,9 +41,6 @@
|
||||
"filament_max_volumetric_speed": [
|
||||
"16"
|
||||
],
|
||||
"filament_multitool_ramming_flow": [
|
||||
"16"
|
||||
],
|
||||
"filament_scarf_seam_type": [
|
||||
"none"
|
||||
],
|
||||
|
||||
@@ -8,9 +8,6 @@
|
||||
"filament_type": [
|
||||
"EVA"
|
||||
],
|
||||
"filament_multitool_ramming_flow": [
|
||||
"12"
|
||||
],
|
||||
"supertack_plate_temp": [
|
||||
"0"
|
||||
],
|
||||
|
||||
@@ -41,9 +41,6 @@
|
||||
"filament_max_volumetric_speed": [
|
||||
"8"
|
||||
],
|
||||
"filament_multitool_ramming_flow": [
|
||||
"8"
|
||||
],
|
||||
"filament_type": [
|
||||
"HIPS"
|
||||
],
|
||||
|
||||
@@ -44,9 +44,6 @@
|
||||
"filament_max_volumetric_speed": [
|
||||
"8"
|
||||
],
|
||||
"filament_multitool_ramming_flow": [
|
||||
"12"
|
||||
],
|
||||
"filament_type": [
|
||||
"PA"
|
||||
],
|
||||
|
||||
@@ -7,9 +7,6 @@
|
||||
"filament_max_volumetric_speed": [
|
||||
"12"
|
||||
],
|
||||
"filament_multitool_ramming_flow": [
|
||||
"12"
|
||||
],
|
||||
"filament_type": [
|
||||
"PAHT"
|
||||
],
|
||||
|
||||
@@ -86,9 +86,6 @@
|
||||
"filament_max_volumetric_speed": [
|
||||
"12"
|
||||
],
|
||||
"filament_multitool_ramming_flow": [
|
||||
"16"
|
||||
],
|
||||
"filament_flow_ratio": [
|
||||
"0.94"
|
||||
]
|
||||
|
||||
@@ -26,9 +26,6 @@
|
||||
"filament_max_volumetric_speed": [
|
||||
"12"
|
||||
],
|
||||
"filament_multitool_ramming_flow": [
|
||||
"10"
|
||||
],
|
||||
"filament_type": [
|
||||
"PCTG"
|
||||
],
|
||||
|
||||
@@ -38,9 +38,6 @@
|
||||
"filament_max_volumetric_speed": [
|
||||
"12"
|
||||
],
|
||||
"filament_multitool_ramming_flow": [
|
||||
"12"
|
||||
],
|
||||
"filament_type": [
|
||||
"PE"
|
||||
],
|
||||
|
||||
@@ -26,9 +26,6 @@
|
||||
"filament_max_volumetric_speed": [
|
||||
"10"
|
||||
],
|
||||
"filament_multitool_ramming_flow": [
|
||||
"15"
|
||||
],
|
||||
"filament_type": [
|
||||
"PETG"
|
||||
],
|
||||
|
||||
@@ -38,9 +38,6 @@
|
||||
"filament_max_volumetric_speed": [
|
||||
"6"
|
||||
],
|
||||
"filament_multitool_ramming_flow": [
|
||||
"6"
|
||||
],
|
||||
"filament_type": [
|
||||
"PHA"
|
||||
],
|
||||
|
||||
@@ -38,9 +38,6 @@
|
||||
"filament_max_volumetric_speed": [
|
||||
"12"
|
||||
],
|
||||
"filament_multitool_ramming_flow": [
|
||||
"20"
|
||||
],
|
||||
"filament_scarf_seam_type": [
|
||||
"none"
|
||||
],
|
||||
|
||||
@@ -9,9 +9,6 @@
|
||||
"filament_flow_ratio": [
|
||||
"0.98"
|
||||
],
|
||||
"filament_multitool_ramming_flow": [
|
||||
"10"
|
||||
],
|
||||
"slow_down_layer_time": [
|
||||
"8"
|
||||
]
|
||||
|
||||
@@ -38,9 +38,6 @@
|
||||
"filament_max_volumetric_speed": [
|
||||
"12"
|
||||
],
|
||||
"filament_multitool_ramming_flow": [
|
||||
"12"
|
||||
],
|
||||
"filament_type": [
|
||||
"PP"
|
||||
],
|
||||
|
||||
@@ -47,9 +47,6 @@
|
||||
"filament_max_volumetric_speed": [
|
||||
"8"
|
||||
],
|
||||
"filament_multitool_ramming_flow": [
|
||||
"8"
|
||||
],
|
||||
"filament_type": [
|
||||
"PPA-CF"
|
||||
],
|
||||
|
||||
@@ -43,9 +43,6 @@
|
||||
"filament_max_volumetric_speed": [
|
||||
"4"
|
||||
],
|
||||
"filament_multitool_ramming_flow": [
|
||||
"4"
|
||||
],
|
||||
"filament_type": [
|
||||
"PPS"
|
||||
],
|
||||
|
||||
@@ -40,15 +40,6 @@
|
||||
"filament_max_volumetric_speed": [
|
||||
"12"
|
||||
],
|
||||
"filament_multitool_ramming": [
|
||||
"0"
|
||||
],
|
||||
"filament_multitool_ramming_flow": [
|
||||
"6"
|
||||
],
|
||||
"filament_multitool_ramming_volume": [
|
||||
"0.1"
|
||||
],
|
||||
"filament_soluble": [
|
||||
"1"
|
||||
],
|
||||
|
||||
@@ -11,9 +11,6 @@
|
||||
"filament_max_volumetric_speed": [
|
||||
"12"
|
||||
],
|
||||
"filament_multitool_ramming_flow": [
|
||||
"12"
|
||||
],
|
||||
"filament_type": [
|
||||
"SBS"
|
||||
],
|
||||
|
||||
@@ -43,15 +43,6 @@
|
||||
"filament_max_volumetric_speed": [
|
||||
"3.2"
|
||||
],
|
||||
"filament_multitool_ramming": [
|
||||
"0"
|
||||
],
|
||||
"filament_multitool_ramming_flow": [
|
||||
"3.2"
|
||||
],
|
||||
"filament_multitool_ramming_volume": [
|
||||
"0.1"
|
||||
],
|
||||
"filament_retraction_length": [
|
||||
"0.4"
|
||||
],
|
||||
|
||||
@@ -17,9 +17,6 @@
|
||||
"filament_max_volumetric_speed": [
|
||||
"8"
|
||||
],
|
||||
"filament_multitool_ramming_flow": [
|
||||
"8"
|
||||
],
|
||||
"filament_vendor": [
|
||||
"eSUN"
|
||||
],
|
||||
|
||||
@@ -17,9 +17,6 @@
|
||||
"filament_max_volumetric_speed": [
|
||||
"8"
|
||||
],
|
||||
"filament_multitool_ramming_flow": [
|
||||
"8"
|
||||
],
|
||||
"filament_vendor": [
|
||||
"eSUN"
|
||||
],
|
||||
|
||||
@@ -21,9 +21,6 @@
|
||||
"filament_max_volumetric_speed": [
|
||||
"6"
|
||||
],
|
||||
"filament_multitool_ramming_flow": [
|
||||
"6"
|
||||
],
|
||||
"filament_vendor": [
|
||||
"eSUN"
|
||||
],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Snapmaker",
|
||||
"version": "02.04.00.08",
|
||||
"version": "02.04.00.07",
|
||||
"force_update": "0",
|
||||
"description": "Snapmaker configurations",
|
||||
"machine_model_list": [
|
||||
@@ -426,6 +426,10 @@
|
||||
"name": "0.16 Optimal @Snapmaker U1 (0.4 nozzle)",
|
||||
"sub_path": "process/0.16 Optimal @Snapmaker U1 (0.4 nozzle).json"
|
||||
},
|
||||
{
|
||||
"name": "0.20 Bambu Support W @Snapmaker U1 (0.4 nozzle)",
|
||||
"sub_path": "process/0.20 Bambu Support W @Snapmaker U1 (0.4 nozzle).json"
|
||||
},
|
||||
{
|
||||
"name": "0.20 Quality @Snapmaker U1 (0.4 nozzle)",
|
||||
"sub_path": "process/0.20 Quality @Snapmaker U1 (0.4 nozzle).json"
|
||||
@@ -482,66 +486,6 @@
|
||||
"name": "fdm_process_U1_0.8_common",
|
||||
"sub_path": "process/fdm_process_U1_0.8_common.json"
|
||||
},
|
||||
{
|
||||
"name": "fdm_process_U1_0.06_nozzle_0.2",
|
||||
"sub_path": "process/fdm_process_U1_0.06_nozzle_0.2.json"
|
||||
},
|
||||
{
|
||||
"name": "fdm_process_U1_0.08_nozzle_0.2",
|
||||
"sub_path": "process/fdm_process_U1_0.08_nozzle_0.2.json"
|
||||
},
|
||||
{
|
||||
"name": "fdm_process_U1_0.10_nozzle_0.2",
|
||||
"sub_path": "process/fdm_process_U1_0.10_nozzle_0.2.json"
|
||||
},
|
||||
{
|
||||
"name": "fdm_process_U1_0.12_nozzle_0.2",
|
||||
"sub_path": "process/fdm_process_U1_0.12_nozzle_0.2.json"
|
||||
},
|
||||
{
|
||||
"name": "fdm_process_U1_0.14_nozzle_0.2",
|
||||
"sub_path": "process/fdm_process_U1_0.14_nozzle_0.2.json"
|
||||
},
|
||||
{
|
||||
"name": "fdm_process_U1_0.18_nozzle_0.6",
|
||||
"sub_path": "process/fdm_process_U1_0.18_nozzle_0.6.json"
|
||||
},
|
||||
{
|
||||
"name": "fdm_process_U1_0.24_nozzle_0.6",
|
||||
"sub_path": "process/fdm_process_U1_0.24_nozzle_0.6.json"
|
||||
},
|
||||
{
|
||||
"name": "fdm_process_U1_0.24_nozzle_0.8",
|
||||
"sub_path": "process/fdm_process_U1_0.24_nozzle_0.8.json"
|
||||
},
|
||||
{
|
||||
"name": "fdm_process_U1_0.30_nozzle_0.6",
|
||||
"sub_path": "process/fdm_process_U1_0.30_nozzle_0.6.json"
|
||||
},
|
||||
{
|
||||
"name": "fdm_process_U1_0.32_nozzle_0.8",
|
||||
"sub_path": "process/fdm_process_U1_0.32_nozzle_0.8.json"
|
||||
},
|
||||
{
|
||||
"name": "fdm_process_U1_0.36_nozzle_0.6",
|
||||
"sub_path": "process/fdm_process_U1_0.36_nozzle_0.6.json"
|
||||
},
|
||||
{
|
||||
"name": "fdm_process_U1_0.40_nozzle_0.8",
|
||||
"sub_path": "process/fdm_process_U1_0.40_nozzle_0.8.json"
|
||||
},
|
||||
{
|
||||
"name": "fdm_process_U1_0.42_nozzle_0.6",
|
||||
"sub_path": "process/fdm_process_U1_0.42_nozzle_0.6.json"
|
||||
},
|
||||
{
|
||||
"name": "fdm_process_U1_0.48_nozzle_0.8",
|
||||
"sub_path": "process/fdm_process_U1_0.48_nozzle_0.8.json"
|
||||
},
|
||||
{
|
||||
"name": "fdm_process_U1_0.56_nozzle_0.8",
|
||||
"sub_path": "process/fdm_process_U1_0.56_nozzle_0.8.json"
|
||||
},
|
||||
{
|
||||
"name": "0.06 High Quality @Snapmaker U1 (0.2 nozzle)",
|
||||
"sub_path": "process/0.06 High Quality @Snapmaker U1 (0.2 nozzle).json"
|
||||
@@ -1564,6 +1508,10 @@
|
||||
"name": "Snapmaker PLA Metal @U1",
|
||||
"sub_path": "filament/Snapmaker PLA Metal @U1.json"
|
||||
},
|
||||
{
|
||||
"name": "Snapmaker PLA Silk @U1",
|
||||
"sub_path": "filament/Snapmaker PLA Silk @U1.json"
|
||||
},
|
||||
{
|
||||
"name": "Snapmaker PLA Silk",
|
||||
"sub_path": "filament/Snapmaker PLA Silk.json"
|
||||
@@ -1723,214 +1671,6 @@
|
||||
{
|
||||
"name": "Snapmaker PLA Matte @U1 base",
|
||||
"sub_path": "filament/Snapmaker PLA Matte @U1 base.json"
|
||||
},
|
||||
{
|
||||
"name": "Polymaker PLA @U1 base",
|
||||
"sub_path": "filament/Polymaker PLA @U1 base.json"
|
||||
},
|
||||
{
|
||||
"name": "Polymaker Silk PLA Family @U1",
|
||||
"sub_path": "filament/Polymaker Silk PLA Family @U1.json"
|
||||
},
|
||||
{
|
||||
"name": "Polymaker Tough PLA Family @U1",
|
||||
"sub_path": "filament/Polymaker Tough PLA Family @U1.json"
|
||||
},
|
||||
{
|
||||
"name": "Snapmaker Breakaway Support For PLA @U1 0.2 nozzle",
|
||||
"sub_path": "filament/Snapmaker Breakaway Support For PLA @U1 0.2 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "Snapmaker Breakaway Support For PLA @U1 0.6 nozzle",
|
||||
"sub_path": "filament/Snapmaker Breakaway Support For PLA @U1 0.6 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "Snapmaker Breakaway Support For PLA @U1 0.8 nozzle",
|
||||
"sub_path": "filament/Snapmaker Breakaway Support For PLA @U1 0.8 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "Snapmaker PETG HF @U1 base2",
|
||||
"sub_path": "filament/Snapmaker PETG HF @U1 base2.json"
|
||||
},
|
||||
{
|
||||
"name": "Snapmaker PETG Translucent @U1 base",
|
||||
"sub_path": "filament/Snapmaker PETG Translucent @U1 base.json"
|
||||
},
|
||||
{
|
||||
"name": "Snapmaker PLA Basic @U1 base",
|
||||
"sub_path": "filament/Snapmaker PLA Basic @U1 base.json"
|
||||
},
|
||||
{
|
||||
"name": "Snapmaker PLA Full Spectrum @U1 0.4 nozzle",
|
||||
"sub_path": "filament/Snapmaker PLA Full Spectrum @U1 0.4 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "Snapmaker PLA Glow @U1 base",
|
||||
"sub_path": "filament/Snapmaker PLA Glow @U1 base.json"
|
||||
},
|
||||
{
|
||||
"name": "Snapmaker PLA Matte @U1 base2",
|
||||
"sub_path": "filament/Snapmaker PLA Matte @U1 base2.json"
|
||||
},
|
||||
{
|
||||
"name": "Snapmaker PLA Silk @U1 0.2 nozzle",
|
||||
"sub_path": "filament/Snapmaker PLA Silk @U1 0.2 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "Snapmaker PLA Silk @U1 0.6 nozzle",
|
||||
"sub_path": "filament/Snapmaker PLA Silk @U1 0.6 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "Snapmaker PLA Silk @U1 0.8 nozzle",
|
||||
"sub_path": "filament/Snapmaker PLA Silk @U1 0.8 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "Snapmaker PLA SnapSpeed @U1 base2",
|
||||
"sub_path": "filament/Snapmaker PLA SnapSpeed @U1 base2.json"
|
||||
},
|
||||
{
|
||||
"name": "Snapmaker PLA Translucent @U1 base",
|
||||
"sub_path": "filament/Snapmaker PLA Translucent @U1 base.json"
|
||||
},
|
||||
{
|
||||
"name": "Snapmaker PLA Wood @U1 0.4 nozzle",
|
||||
"sub_path": "filament/Snapmaker PLA Wood @U1 0.4 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "Snapmaker PLA Wood @U1 0.6 nozzle",
|
||||
"sub_path": "filament/Snapmaker PLA Wood @U1 0.6 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "Snapmaker PLA Wood @U1 0.8 nozzle",
|
||||
"sub_path": "filament/Snapmaker PLA Wood @U1 0.8 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "Snapmaker PLA-CF @U1 0.4 nozzle",
|
||||
"sub_path": "filament/Snapmaker PLA-CF @U1 0.4 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "Snapmaker PLA-CF @U1 0.6 nozzle",
|
||||
"sub_path": "filament/Snapmaker PLA-CF @U1 0.6 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "Snapmaker PLA-CF @U1 0.8 nozzle",
|
||||
"sub_path": "filament/Snapmaker PLA-CF @U1 0.8 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "Snapmaker PVA @U1 0.6 nozzle",
|
||||
"sub_path": "filament/Snapmaker PVA @U1 0.6 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "Snapmaker PVA @U1 0.8 nozzle",
|
||||
"sub_path": "filament/Snapmaker PVA @U1 0.8 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "Snapmaker TPU 90A @U1",
|
||||
"sub_path": "filament/Snapmaker TPU 90A @U1.json"
|
||||
},
|
||||
{
|
||||
"name": "Snapmaker TPU 90A @U1 0.6 nozzle",
|
||||
"sub_path": "filament/Snapmaker TPU 90A @U1 0.6 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "Snapmaker TPU 90A @U1 0.8 nozzle",
|
||||
"sub_path": "filament/Snapmaker TPU 90A @U1 0.8 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "Snapmaker TPU 95A HF @U1",
|
||||
"sub_path": "filament/Snapmaker TPU 95A HF @U1.json"
|
||||
},
|
||||
{
|
||||
"name": "Snapmaker TPU 95A HF @U1 0.6 nozzle",
|
||||
"sub_path": "filament/Snapmaker TPU 95A HF @U1 0.6 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "Snapmaker TPU 95A HF @U1 0.8 nozzle",
|
||||
"sub_path": "filament/Snapmaker TPU 95A HF @U1 0.8 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "Snapmaker PLA Silk @U1",
|
||||
"sub_path": "filament/Snapmaker PLA Silk @U1.json"
|
||||
},
|
||||
{
|
||||
"name": "Polymaker General PLA Family @U1",
|
||||
"sub_path": "filament/Polymaker General PLA Family @U1.json"
|
||||
},
|
||||
{
|
||||
"name": "Snapmaker PETG HF @U1 0.2 nozzle",
|
||||
"sub_path": "filament/Snapmaker PETG HF @U1 0.2 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "Snapmaker PETG HF @U1 0.6 nozzle",
|
||||
"sub_path": "filament/Snapmaker PETG HF @U1 0.6 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "Snapmaker PETG HF @U1 0.8 nozzle",
|
||||
"sub_path": "filament/Snapmaker PETG HF @U1 0.8 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "Snapmaker PETG Translucent @U1 0.2 nozzle",
|
||||
"sub_path": "filament/Snapmaker PETG Translucent @U1 0.2 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "Snapmaker PETG Translucent @U1 0.4 nozzle",
|
||||
"sub_path": "filament/Snapmaker PETG Translucent @U1 0.4 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "Snapmaker PETG Translucent @U1 0.6 nozzle",
|
||||
"sub_path": "filament/Snapmaker PETG Translucent @U1 0.6 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "Snapmaker PETG Translucent @U1 0.8 nozzle",
|
||||
"sub_path": "filament/Snapmaker PETG Translucent @U1 0.8 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "Snapmaker PLA Basic @U1",
|
||||
"sub_path": "filament/Snapmaker PLA Basic @U1.json"
|
||||
},
|
||||
{
|
||||
"name": "Snapmaker PLA Glow @U1 0.4 nozzle",
|
||||
"sub_path": "filament/Snapmaker PLA Glow @U1 0.4 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "Snapmaker PLA Matte @U1 0.2 nozzle",
|
||||
"sub_path": "filament/Snapmaker PLA Matte @U1 0.2 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "Snapmaker PLA Matte @U1 0.6 nozzle",
|
||||
"sub_path": "filament/Snapmaker PLA Matte @U1 0.6 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "Snapmaker PLA Matte @U1 0.8 nozzle",
|
||||
"sub_path": "filament/Snapmaker PLA Matte @U1 0.8 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "Snapmaker PLA SnapSpeed @U1 0.2 nozzle",
|
||||
"sub_path": "filament/Snapmaker PLA SnapSpeed @U1 0.2 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "Snapmaker PLA SnapSpeed @U1 0.6 nozzle",
|
||||
"sub_path": "filament/Snapmaker PLA SnapSpeed @U1 0.6 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "Snapmaker PLA SnapSpeed @U1 0.8 nozzle",
|
||||
"sub_path": "filament/Snapmaker PLA SnapSpeed @U1 0.8 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "Snapmaker PLA Translucent @U1 0.2 nozzle",
|
||||
"sub_path": "filament/Snapmaker PLA Translucent @U1 0.2 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "Snapmaker PLA Translucent @U1 0.4 nozzle",
|
||||
"sub_path": "filament/Snapmaker PLA Translucent @U1 0.4 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "Snapmaker PLA Translucent @U1 0.6 nozzle",
|
||||
"sub_path": "filament/Snapmaker PLA Translucent @U1 0.6 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "Snapmaker PLA Translucent @U1 0.8 nozzle",
|
||||
"sub_path": "filament/Snapmaker PLA Translucent @U1 0.8 nozzle.json"
|
||||
}
|
||||
],
|
||||
"machine_list": [
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
{
|
||||
"type": "filament",
|
||||
"name": "Polymaker General PLA Family @U1",
|
||||
"inherits": "Polymaker PLA @U1 base",
|
||||
"from": "system",
|
||||
"setting_id": "thXfSaUkOAKJ50ey",
|
||||
"instantiation": "true",
|
||||
"compatible_printers": [
|
||||
"Snapmaker U1 (0.4 nozzle)"
|
||||
],
|
||||
"enable_pressure_advance": [
|
||||
"0"
|
||||
],
|
||||
"filament_max_volumetric_speed": [
|
||||
"15"
|
||||
],
|
||||
"filament_vendor": [
|
||||
"Polymaker"
|
||||
],
|
||||
"nozzle_temperature_range_high": [
|
||||
"230"
|
||||
],
|
||||
"temperature_vitrification": [
|
||||
"62"
|
||||
],
|
||||
"textured_plate_temp": [
|
||||
"65"
|
||||
],
|
||||
"textured_plate_temp_initial_layer": [
|
||||
"65"
|
||||
]
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
{
|
||||
"type": "filament",
|
||||
"name": "Polymaker PLA @U1 base",
|
||||
"inherits": "fdm_filament_pla",
|
||||
"from": "system",
|
||||
"filament_id": "OGFL99",
|
||||
"instantiation": "false",
|
||||
"compatible_printers": [
|
||||
"Snapmaker U1 (0.4 nozzle)"
|
||||
],
|
||||
"activate_air_filtration": [
|
||||
"0"
|
||||
],
|
||||
"chamber_temperatures": [
|
||||
"0"
|
||||
],
|
||||
"complete_print_exhaust_fan_speed": [
|
||||
"70"
|
||||
],
|
||||
"cool_plate_temp": [
|
||||
"35"
|
||||
],
|
||||
"cool_plate_temp_initial_layer": [
|
||||
"35"
|
||||
],
|
||||
"during_print_exhaust_fan_speed": [
|
||||
"70"
|
||||
],
|
||||
"filament_cost": [
|
||||
"20"
|
||||
],
|
||||
"filament_long_retractions_when_cut": [
|
||||
"nil"
|
||||
],
|
||||
"filament_max_volumetric_speed": [
|
||||
"12"
|
||||
],
|
||||
"filament_minimal_purge_on_wipe_tower": [
|
||||
"15"
|
||||
],
|
||||
"filament_retraction_distances_when_cut": [
|
||||
"nil"
|
||||
],
|
||||
"filament_retraction_length": [
|
||||
"nil"
|
||||
],
|
||||
"filament_retraction_speed": [
|
||||
"nil"
|
||||
],
|
||||
"filament_vendor": [
|
||||
"Generic"
|
||||
],
|
||||
"filament_scarf_seam_type": [
|
||||
"none"
|
||||
],
|
||||
"filament_scarf_height": [
|
||||
"10%"
|
||||
],
|
||||
"filament_scarf_gap": [
|
||||
"15%"
|
||||
],
|
||||
"filament_scarf_length": [
|
||||
"10"
|
||||
],
|
||||
"filament_shrink": [
|
||||
"100%"
|
||||
],
|
||||
"hot_plate_temp": [
|
||||
"55"
|
||||
],
|
||||
"hot_plate_temp_initial_layer": [
|
||||
"55"
|
||||
],
|
||||
"nozzle_temperature": [
|
||||
"220"
|
||||
],
|
||||
"overhang_fan_threshold": [
|
||||
"50%"
|
||||
],
|
||||
"supertack_plate_temp": [
|
||||
"45"
|
||||
],
|
||||
"supertack_plate_temp_initial_layer": [
|
||||
"45"
|
||||
],
|
||||
"slow_down_layer_time": [
|
||||
"4"
|
||||
],
|
||||
"slow_down_min_speed": [
|
||||
"10"
|
||||
],
|
||||
"temperature_vitrification": [
|
||||
"45"
|
||||
],
|
||||
"textured_plate_temp": [
|
||||
"55"
|
||||
],
|
||||
"textured_plate_temp_initial_layer": [
|
||||
"55"
|
||||
],
|
||||
"filament_start_gcode": [
|
||||
"; Filament gcode\n"
|
||||
],
|
||||
"filament_end_gcode": [
|
||||
"; filament end gcode\n"
|
||||
]
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
{
|
||||
"type": "filament",
|
||||
"name": "Polymaker Silk PLA Family @U1",
|
||||
"inherits": "Polymaker PLA @U1 base",
|
||||
"from": "system",
|
||||
"setting_id": "h8vIYpXbxFtHPV6e",
|
||||
"instantiation": "true",
|
||||
"compatible_printers": [
|
||||
"Snapmaker U1 (0.4 nozzle)"
|
||||
],
|
||||
"filament_density": [
|
||||
"1.34"
|
||||
],
|
||||
"filament_vendor": [
|
||||
"Polymaker"
|
||||
],
|
||||
"nozzle_temperature": [
|
||||
"230"
|
||||
],
|
||||
"nozzle_temperature_initial_layer": [
|
||||
"230"
|
||||
],
|
||||
"nozzle_temperature_range_high": [
|
||||
"230"
|
||||
],
|
||||
"textured_plate_temp": [
|
||||
"65"
|
||||
],
|
||||
"textured_plate_temp_initial_layer": [
|
||||
"65"
|
||||
]
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
{
|
||||
"type": "filament",
|
||||
"name": "Polymaker Tough PLA Family @U1",
|
||||
"inherits": "Polymaker PLA @U1 base",
|
||||
"from": "system",
|
||||
"setting_id": "d6CC81Es2hp46bto",
|
||||
"instantiation": "true",
|
||||
"compatible_printers": [
|
||||
"Snapmaker U1 (0.4 nozzle)"
|
||||
],
|
||||
"filament_density": [
|
||||
"1.23"
|
||||
],
|
||||
"filament_flow_ratio": [
|
||||
"0.96"
|
||||
],
|
||||
"filament_vendor": [
|
||||
"Polymaker"
|
||||
],
|
||||
"slow_down_layer_time": [
|
||||
"6"
|
||||
],
|
||||
"temperature_vitrification": [
|
||||
"55"
|
||||
],
|
||||
"textured_plate_temp": [
|
||||
"65"
|
||||
],
|
||||
"textured_plate_temp_initial_layer": [
|
||||
"65"
|
||||
]
|
||||
}
|
||||
@@ -11,6 +11,7 @@
|
||||
"filament_minimal_purge_on_wipe_tower": [
|
||||
"15"
|
||||
],
|
||||
|
||||
"pressure_advance": [
|
||||
"0.05"
|
||||
]
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"filament_z_hop": [
|
||||
"0.0"
|
||||
],
|
||||
|
||||
"enable_pressure_advance": [
|
||||
"1"
|
||||
],
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"filament_z_hop": [
|
||||
"0.0"
|
||||
],
|
||||
|
||||
"enable_pressure_advance": [
|
||||
"1"
|
||||
],
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"supertack_plate_temp_initial_layer": [
|
||||
"70"
|
||||
],
|
||||
|
||||
"pressure_advance": [
|
||||
"0.04"
|
||||
]
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"type": "filament",
|
||||
"name": "PolyLite Dual PLA @0.2 nozzle",
|
||||
"inherits": "PolyLite PLA @0.2 nozzle",
|
||||
"from": "system",
|
||||
"setting_id": "jhmy4YHi9MuL0DWu",
|
||||
"instantiation": "true",
|
||||
"name": "PolyLite Dual PLA @0.2 nozzle",
|
||||
"setting_id": "jhmy4YHi9MuL0DWu",
|
||||
"inherits": "PolyLite PLA @0.2 nozzle",
|
||||
"compatible_printers": [
|
||||
"Snapmaker A250 Dual (0.2 nozzle)",
|
||||
"Snapmaker A250 Dual BKit (0.2 nozzle)",
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"type": "filament",
|
||||
"name": "PolyLite J1 PLA @0.2 nozzle",
|
||||
"inherits": "PolyLite PLA @0.2 nozzle",
|
||||
"from": "system",
|
||||
"setting_id": "jFlRSxx0KvN3BOUu",
|
||||
"instantiation": "true",
|
||||
"name": "PolyLite J1 PLA @0.2 nozzle",
|
||||
"setting_id": "jFlRSxx0KvN3BOUu",
|
||||
"inherits": "PolyLite PLA @0.2 nozzle",
|
||||
"compatible_printers": [
|
||||
"Snapmaker J1 (0.2 nozzle)"
|
||||
]
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"type": "filament",
|
||||
"name": "PolyLite J1 PLA",
|
||||
"inherits": "PolyLite PLA @base",
|
||||
"from": "system",
|
||||
"setting_id": "wcpOTTMyZNPFKyXr",
|
||||
"instantiation": "true",
|
||||
"name": "PolyLite J1 PLA",
|
||||
"setting_id": "wcpOTTMyZNPFKyXr",
|
||||
"inherits": "PolyLite PLA @base",
|
||||
"compatible_printers": [
|
||||
"Snapmaker J1 (0.4 nozzle)",
|
||||
"Snapmaker J1 (0.6 nozzle)",
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
{
|
||||
"type": "filament",
|
||||
"name": "PolyLite PETG @Base",
|
||||
"inherits": "fdm_filament_petg",
|
||||
"from": "system",
|
||||
"instantiation": "false",
|
||||
"filament_density": [
|
||||
"1.25"
|
||||
],
|
||||
@@ -23,6 +18,11 @@
|
||||
"0"
|
||||
],
|
||||
"description": "",
|
||||
"inherits": "fdm_filament_petg",
|
||||
"name": "PolyLite PETG @Base",
|
||||
"type": "filament",
|
||||
"instantiation": "false",
|
||||
"from": "system",
|
||||
"filament_vendor": [
|
||||
"Polymaker"
|
||||
]
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"supertack_plate_temp_initial_layer": [
|
||||
"70"
|
||||
],
|
||||
|
||||
"pressure_advance": [
|
||||
"0.05"
|
||||
]
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"supertack_plate_temp_initial_layer": [
|
||||
"70"
|
||||
],
|
||||
|
||||
"pressure_advance": [
|
||||
"0.05"
|
||||
]
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"type": "filament",
|
||||
"name": "PolyLite PLA @0.2 nozzle",
|
||||
"inherits": "PolyLite PLA @base",
|
||||
"from": "system",
|
||||
"setting_id": "s9mdMaFca8SHfji9",
|
||||
"instantiation": "true",
|
||||
"name": "PolyLite PLA @0.2 nozzle",
|
||||
"setting_id": "s9mdMaFca8SHfji9",
|
||||
"inherits": "PolyLite PLA @base",
|
||||
"compatible_printers": [
|
||||
"Snapmaker A250 (0.2 nozzle)",
|
||||
"Snapmaker A250 BKit (0.2 nozzle)",
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"type": "filament",
|
||||
"name": "PolyLite PLA @base",
|
||||
"inherits": "fdm_filament_pla",
|
||||
"from": "system",
|
||||
"filament_id": "1393866034",
|
||||
"instantiation": "false",
|
||||
"name": "PolyLite PLA @base",
|
||||
"filament_id": "1393866034",
|
||||
"inherits": "fdm_filament_pla",
|
||||
"filament_flow_ratio": [
|
||||
"0.95"
|
||||
],
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"type": "filament",
|
||||
"name": "PolyTerra Dual PLA @0.2 nozzle",
|
||||
"inherits": "PolyTerra PLA @0.2 nozzle",
|
||||
"from": "system",
|
||||
"setting_id": "UQYgjH1uVxf3d2Nv",
|
||||
"instantiation": "true",
|
||||
"name": "PolyTerra Dual PLA @0.2 nozzle",
|
||||
"setting_id": "UQYgjH1uVxf3d2Nv",
|
||||
"inherits": "PolyTerra PLA @0.2 nozzle",
|
||||
"compatible_printers": [
|
||||
"Snapmaker A250 Dual (0.2 nozzle)",
|
||||
"Snapmaker A250 Dual BKit (0.2 nozzle)",
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"type": "filament",
|
||||
"name": "PolyTerra J1 PLA @0.2 nozzle",
|
||||
"inherits": "PolyTerra PLA @0.2 nozzle",
|
||||
"from": "system",
|
||||
"setting_id": "mxKHxGmBZzPLS82O",
|
||||
"instantiation": "true",
|
||||
"name": "PolyTerra J1 PLA @0.2 nozzle",
|
||||
"setting_id": "mxKHxGmBZzPLS82O",
|
||||
"inherits": "PolyTerra PLA @0.2 nozzle",
|
||||
"compatible_printers": [
|
||||
"Snapmaker J1 (0.2 nozzle)"
|
||||
]
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"type": "filament",
|
||||
"name": "PolyTerra J1 PLA",
|
||||
"inherits": "PolyTerra PLA @base",
|
||||
"from": "system",
|
||||
"setting_id": "4MZWZX7QPBzxdEGj",
|
||||
"instantiation": "true",
|
||||
"name": "PolyTerra J1 PLA",
|
||||
"setting_id": "4MZWZX7QPBzxdEGj",
|
||||
"inherits": "PolyTerra PLA @base",
|
||||
"compatible_printers": [
|
||||
"Snapmaker J1 (0.4 nozzle)",
|
||||
"Snapmaker J1 (0.6 nozzle)",
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"type": "filament",
|
||||
"name": "PolyTerra PLA @0.2 nozzle",
|
||||
"inherits": "PolyTerra PLA @base",
|
||||
"from": "system",
|
||||
"setting_id": "6W1ygT08NordBdIW",
|
||||
"instantiation": "true",
|
||||
"name": "PolyTerra PLA @0.2 nozzle",
|
||||
"setting_id": "6W1ygT08NordBdIW",
|
||||
"inherits": "PolyTerra PLA @base",
|
||||
"compatible_printers": [
|
||||
"Snapmaker A250 (0.2 nozzle)",
|
||||
"Snapmaker A250 BKit (0.2 nozzle)",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user