Architecture
Tauri 2 (Rust core) + SvelteKit + Svelte 5 + TypeScript. The frontend talks to FluidNC over WebSocket and HTTP; the Rust side owns the connection, job streaming and the calibration state model.
The split is deliberate: the Rust side owns the live connection and all machine-state truth; the frontend is a thin reactive view that listens for Tauri events and mirrors them into Svelte stores. Frontend code does not replicate state-machine or reconciliation logic.
Rust core (src-tauri/src/)
lib.rs- registers every#[tauri::command]in oneinvoke_handler.connection.rs- the heart. Aconnection_loopsupervisor task owns the WebSocket (ws://<host>:81/, i.e. web port + 1, subprotocolarduino) and survives automatic reconnects. Binary frames carry the GRBL line stream; text frames carrykey:valuecontrol messages. It polls?(status),$MINFOand$GSTATEon tunable intervals (fast while moving, slow at idle), and burst-polls$GSTATEafter any user action so the UI reflects state changes within milliseconds instead of waiting for the 1.5 second tick. All polling is suspended whileupload_activeis set (an SD/flash write stalls both ESP32 cores). The active streamingJobis owned here so it outlives reconnects.grbl.rs- parses<...>realtime status reports intoMachineStatus. FluidNC may append[GC:...]after the report; the parser extracts the<...>substring. Maslow reports 5 axes (X, Y, Z, A, B).maslow.rs- Maslow telemetry (MINFO:JSON blob) and the calibration state machine (states 0-9, mirrors firmwareMaslowEnums.h).policy_for(state)is the single source of truth for which user actions are allowed in each state. State (Current state: N) arrives separately from MINFO, via[MSG:INFO:...]or$GSTATE.streaming.rs- G-code job streaming via the GRBL character-counting protocol (127-byte RX budget). Only theackedcursor matters for resume; it is persisted to disk (every 25 acked lines plus on state change) so a job survives an app restart, not just a reconnect.calibration.rs- client-side anchor solver behind theAnchorSolvertrait (a Levenberg-Marquardt implementation, a faithful port of the firmware's own solver). Exposed assolve_calibrationfor what-if waypoint-exclusion analysis without touching the machine.http_api.rs- HTTP client for/command,/files,/upload(file ops, ping, firmware version).$/$/commands return an empty HTTP body: their output is routed to the WebSocket instead, so config reads ($CD) must go over the socket, not HTTP.toolpath.rs- parses G-code into a polyline for the preview canvas.grpc/,http/,mcp/- the three control API transports. See Using the API.
Frontend (src/)
- SPA mode:
+layout.tssetsssr = false(no Node server under Tauri);adapter-static. src/lib/stores/- one store module per domain (connection,machine,maslow,job,ui,viewport). Each exposes aninit*Listeners()registered once in+page.svelte'sonMount. These listeners are the only place Tauri events become Svelte state.src/routes/+page.svelte- desktop shell (CSS-grid: topbar + persistent right control rail + 3-tab workspace + bottom console dock).MobileShell.svelteis the touch layout;$layoutfromviewport.tsswitches between them.- Tabs are kept mounted and hidden with
display:none(not conditionally rendered) so the waypoint canvas and config panels keep their state and don't refetch on tab switch.
Event contract (Rust app.emit to frontend listen)
Key events: ws-state, grbl-line, machine-status, maslow-info, maslow-state, maslow-waypoint,
action-policy, stream-progress, config-dump / maslow-config / maslow-anchors (all filled by one $CD
dump), maslow-discord (logged when firmware state reports disagree with the app's reconciliation).
action-policy is the reconciled allow-list (FluidNC state + calibration state + job state) computed in Rust and
emitted only on change; the UI gates buttons on it rather than re-deriving it.
Conventions
- E-Stop is
0x18(Ctrl-X soft reset), sent as a realtime byte; it stays reachable from app chrome at all times, never gated behind a tab. - HTTP vs WebSocket for a machine interaction:
$/$/commands go over WebSocket;[ESP...]//files//uploadgo over HTTP. - Firmware-version-gated features (e.g. apply-tension limiting, v1.22+) hide or no-op on older firmware rather than error.