Early docs These pages are very early. Structure and tone are still being worked out, so anything here may change drastically.
Skip to content
Rendered from src/printer/README.md in the repository — edit it there.

Printer — The Slicer → Printer Link ​

This module is how a finished .gcode file reaches a real machine, how the app answers "is that printer awake?", and how a printer describes itself well enough that the user barely has to set one up.

It is split across that seam, because the two halves have different reach:

Probe and upload from the process that has the LAN, never from the browser — but interpret what comes back in exactly one place, everywhere.

FileOwnsCompiles for
transport.rsThe HTTP: probing, status, uploadnative only
klipper.rsTurning Moonraker's JSON into a detectioneverywhere, wasm included
detection.rsPrinterDetection and its questionseverywhere

The pure half is what lets the web build reach the same conclusions about a printer as the server and the desktop app. It fetches with its own fetch and then calls straight into klipper through wasm, rather than keeping a second copy of these rules in TypeScript.

Why not just fetch from the UI ​

Moonraker (Klipper) ships no permissive Access-Control-* headers, so a direct browser request from the Angular app fails for most users. Routing the request through the native process sidesteps CORS entirely — the request never leaves the same trust boundary as the printer's own network.

Both native runtimes reach the same three functions in-process:

FunctionCloud (WS message)Native (Tauri command)
check_statusCheckPrinterprinter_check
detect_printerDetectPrinterprinter_detect
send_gcodeSendToPrinterprinter_send

The result types are serialized to the same field shape as the matching WS payloads (minus the envelope), so the UI reuses one set of fromServer* mappers for both transports:

Native typeWS payload
PrinterStatusReportPrinterStatus
PrinterDetectionPrinterDetected
SendOutcomePrinterSendResult

Keep them in step — a field added to one without the other splits the UI's single mapper in two.

The contract ​

  • Never gate the transport on environment.runtimeMode alone. The desktop build ships the cloud environment and only becomes native by detecting Tauri at runtime, so a build-time constant sends the desktop app down the CORS-prone browser path. That is the specific bug this contract exists to prevent; printer-connection.ts picks at runtime — cloud WS when connected, Tauri commands when isTauriHost(), browser fetch only in web.
  • The web fallback distinguishes unreachable from reachable-but-blocked. A no-cors follow-up probe separates the two, and the UI surfaces a distinct cors status rather than a misleading green or offline dot.
  • Never put reqwest in profiles. That module compiles to wasm; the transport stays here, behind the native cfg.
  • An unimplemented printer kind reports unsupported, not an error. Only PrinterConnectionKind::Moonraker is implemented today, and an honest status beats a misleading dot.

Reading a printer out of its own config ​

Moonraker will hand over a parsed copy of the user's printer.cfg, and nearly everything a setup wizard would ask for is in it. One rule governs what we do with that:

Facts apply, preferences ask.

A value the config states — build volume, nozzle, filament diameter, the machine's own velocity and acceleration limits, whether object exclusion or firmware retraction is configured — is applied without asking, and recorded as a DetectionFinding naming the section it came from so the wizard can show its work. A value that is a choice — what a [fan_generic] is for, whether to re-probe the bed every print — becomes a DetectionQuestion.

Every question carries a suggested answer, and the wizard applies all of them up front. No question ever blocks a profile: the user can add the printer from the first screen and the questions only refine it. A question the config answered outright is marked certain — still reported, because its effect (the start G-code a macro convention implies) is UI-owned copy the engine has no business carrying, but never put to the user.

A question carries its provenance, like a finding does. sources names the config sections behind the lean, spelled the way they appear in printer.cfg, and the wizard prints them under the reason. Applying a dozen settings silently is only fair if the user can check them; asking someone to decide something is worse without it, because "what is this fan for?" cannot be answered until they know which [fan_generic] is meant.

One question per thing decided. A machine with two [fan_generic] sections raises two questions — aux_fan:rscs, aux_fan:exhaust — each naming its fan in subject. A single list over every fan could only ever mark one of them as cooling, which is the wrong answer for the common case of a part cooler beside an exhaust.

Derived values ride in a sparse params bag rather than a field per reading. Adding a field would mean editing this struct, the WS message, the TypeScript model and the wizard — four places to learn one number.

Only state the machine's ceiling once. max_velocity already caps every move, in the firmware and in our own time estimate, so no role speed is derived from it; doing that would restate the cap and override the user's own profile.

Probing in stages, cheapest first ​

configfile is a copy of the whole printer.cfg and runs to megabytes on a macro-heavy setup. So it is asked for last, after the two cheap calls that carry the load:

Only the first is required. A host that times out on configfile still yields the build volume, and still settles the start-macro convention — because the object list alone says which macros are defined.

Vendor is the machine's maker, never the firmware ​

Klipper runs on hundreds of printers. Writing it into the vendor field makes every detected profile claim the same manufacturer, so an unrecognised machine leaves vendor and model empty and the user names it. Machines with a distinctive configuration are fingerprinted on their levelling hardware and build volume — the two things that separate otherwise identical CoreXY designs — and offered as a question to confirm, never assumed.

The data model lives elsewhere ​

PrinterConnection — kind, host (may embed a scheme or :port), port, api_key, plus the legacy UI-owned connected flag. That flag is no longer trusted for the status dot, which reflects the live probe instead:

DotMeaning
neutrallocal or unknown
greenonline
amberchecking · cors · error · unsupported
redoffline

What this module deliberately does not do ​

  • No slicing. It moves finished bytes; geometry belongs in core::.
  • No printer state machine. It reports point-in-time snapshots. Job orchestration and queueing are the printer firmware's business.
  • No credential storage. api_key lives on the profile, and the profile exporter strips it — see profiles.
  • No G-code copy. Detection reports which start macros a printer defines; the commands that implies are templates in gcode-templates.ts, and the wording of every question is in detection-questions.ts.
  • No profile writing. It describes a printer; the wizard decides what to build from that description.
  • No browser transport. The wasm build has none by construction; the UI owns that fallback.

See also ​