Skip to content

Inova API Plugin

Peter Pak edited this page Jul 26, 2026 · 2 revisions

Inova API Plugin

The deployed .NET plugin that loads into the SLS4All Compact firmware's process and serves an HTTP + WebSocket API on port 5001 of the printer. It is the canonical integration surface for everything else in this project — recorder, MCP tools, dashboard. Lives at sls4all/Inova-API-Plugin/ (submodule); its README is the source of truth for layout and the build/install workflow.

Why a plugin, not a fork

The plugin loads via the firmware's official [Application.PluginAssemblies] mechanism and reuses the firmware's DI singletons (IMovementClient, ITemperatureClient, IPowerClient, …) by forwarding them into a child Kestrel host — endpoint handlers see the same live state the firmware uses internally. It runs on its own port (5001) precisely so the firmware's Kestrel on port 80 never needs patching. Zero firmware fork.

It deliberately does not proxy what the firmware already serves well (camera, thermal, galvo images on port 80 — see Hardware Reference), and cannot reference SLS4All.Compact.AppCore (private NuGet deps). Everything in Core/Printing/Processing is fair game via refs/.

Critical build rule

The csproj references deployed firmware DLLs copied into refs/, not the sibling SLS4All.Compact submodule. The submodule can lag the deployed firmware by months, and ABI breaks aren't caught at compile time (learned via a MissingMethodException incident: AsyncEvent<T>.AddHandler changed return type between snapshots). When the printer firmware updates, refresh refs/ from the printer before rebuilding.

API surface (summary)

  • Timed<T> envelope on all data endpoints: { respondedAt, data } for sub-second clock reconstruction.
  • WS /state/stream — the live numeric telemetry channel (the recorder's main feed).
  • WS /movement/position/stream — machine-axis positions, event-driven (bursty by design: axes only move during recoat).
  • Typed clients: TypeScript at client/typescript/ (used by the recorder via @inova/client), Python at client/python/.
  • WebSocket gotcha (fixed, don't reintroduce): always accept with new WebSocketAcceptContext { SubProtocol = null } — the parameterless overload silently sets a non-RFC-compliant Sec-WebSocket-Protocol: default.

Runtime override surface (deployed & verified 2026-07-08)

The mid-print control endpoints the MCP control tools sit on:

Endpoint What it does
GET/POST /printing/recoater-passes Firmware's staged multi-pass (fractional powder staging — see Hardware Reference). Writes both IOptions.Value and IOptionsMonitor.CurrentValue (distinct instances; LayerClient reads the monitor).
GET/POST /printing/recoater-passes-full FullRecoatLayerClient (a PluginReplacements substitution) expands one BeginLayer into one normal recoat + N−1 repeat sweeps at the same bed height; powder: true manually feeds a full Z1 dose before each repeat, false = dry sweeps. GET reports replacementActive — if false, the override is a no-op.
GET/POST /printing/layer-overrides Generic registry of all 11 nullable LayerClientOptions knobs (speed factors, shake, ZMoveForce, volume factors, delays; TimeSpans as seconds). Batch-validated, all-or-nothing; saved overrides re-apply on config hot-reload.
GET/POST /printing/setup-overrides The firmware's native mid-print tune channel (IPrintingService.SetupOverrides): phase temperature targets, totalEnergyDensityPercent, laserFillEnergyDensity, outline energy densities. Highest-value surface — bed temp + laser energy through battle-tested firmware logic. Firmware resets it to empty at every print start, so POSTs are per-print. SetupOverridesDefaults gives the running profile's baselines.

Substitution notes: PluginReplacements subclassing works (LoggingCodePlotter, FullRecoatLayerClient active); LoggingMovementClient stays disabled — it hung boot with a suspected DI cycle. Watch the first boot after enabling any new replacement.

Jobs & profiles CRUD (deployed 2026-07-18)

  • /jobs/* — list/get/patch/delete stored .s4a jobs, /jobs/{id}/instances (persisted nesting) + /jobs/{id}/meshes/{hash}, and POST /jobs/{id}/clone (firmware IJobStorage.CloneJob; pass suggestedId since IPrintJob has no Id; optional printProfileId re-point). CloneJob quirk: the returned object keeps the raw requested name while the filename is sanitized (/ → _) — re-fetch via TryGetJob before any follow-up UpsertJob or it throws FileNotFound.
  • /profiles/* — list (with createdAt + modifiedAt — file mtime of ~/SLS4All/PrintProfiles/<name>.<guid>.json; the storage interface has no timestamps), get (?merged=true = effective values), create (stamps CreatedAt), PUT partial update, delete. Bounds validation lives here (_bounds table) because the firmware PrintProfile model has none of its own (0/97 properties) — temps cap 200 °C; thickness fields are micrometers (layer 100, bedPrep 13000). A raw profile JSON carries a ~250 KB stats blob.
  • Both surfaces are mechanically generic; policy ([TEMPLATE] job convention, system-Default-profile protection) is enforced in the MCP layer — see Agent Plane.

Dev → printer loop

Edit .cs → ./build.sh (stages DLL in dist/) → commit → on the printer git pull && ./install.sh → restart firmware (sudo reboot; the user runs install and restart — the agent recommends and verifies). Post-deploy checks: boot log line FullRecoatLayerClient installed as ILayerClient; replacementActive: true; with powder repeats, watch Z1 telemetry for the extra ~105 µm feed steps.

Known limitations

  • No CORS headers, no auth — trusted-LAN-only operating assumption (matches the firmware's effective stance on this unit).
  • No proxy for firmware port-80 image endpoints (by design).
  • General motion/lights control endpoints unwritten; safety guards should land alongside.
  • The deployed firmware is obfuscated — whole-assembly decompiles fail, but per-type/per-method decompilation with ICSharpCode.Decompiler recovers options classes and signatures (how the recoater semantics were reverse-engineered).

Clone this wiki locally