Elegoo (Neptune / OrangeStorm / Centauri) — LAN Protocol¶
Status: 🟡 source-read (grounded in Elegoo's own first-party LAN SDK; SDCP additionally 🔵 community firmware-validated) · Firmware: Klipper/Moonraker (Neptune, OrangeStorm) · SDCP (Centauri Carbon 1) · proprietary MQTT (Centauri Carbon 2) · Models: Neptune 4 family, OrangeStorm Giga, Centauri Carbon 1 / 2 / 2 Combo
Elegoo is not one protocol — it is a brand covering three unrelated LAN stacks, plus one shared multi-material unit (CANVAS). Pick the backend by model first; everything else follows from that choice.
At a glance¶
Elegoo's line splits into three wire protocols. The one thing to know: there is no common Elegoo protocol — an implementer routes each model to the right stack.
| Backend | Models | Transport | Notes |
|---|---|---|---|
| Moonraker / Klipper | Neptune 4 / 4 Pro / 4 Plus / 4 Max, OrangeStorm Giga | HTTP + WebSocket JSON-RPC :7125 |
Stock, plain Moonraker — see klipper-moonraker.md. Only per-model deltas here. |
| SDCP | Centauri Carbon 1 (+ CANVAS) | JSON-over-WebSocket :3030 + UDP :3000 discovery |
Topic-carrying WS, not JSON-RPC; a genuinely separate stack. |
| Proprietary MQTT | Centauri Carbon 2 / 2 Combo | MQTT :1883 + HTTP :80 upload + UDP :52700 discovery |
Printer runs its own broker; access-code auth. |
- Discovery: manual IP is first-class on every path. Auto: SSDP/mDNS (Moonraker), UDP
M99999→:3000(SDCP), UDP{"id":0,"method":7000}→:52700(CC2). - Auth / credential: Moonraker — none per-device (optional oneshot-token handshake) · SDCP — none at all (LAN-trust) · CC2 — an access code the owner reads off the printer screen (see Credentials).
- Read / status: Moonraker — subscribe · SDCP — passive push (no subscribe command) + a
Cmd 0poll floor · CC2 — push + a1002poll. - Print launch: all three are upload-then-start over a separate channel from the control channel.
- Feeders: CANVAS — a 4-slot RFID feeder that reports material/color/temp but no remaining quantity and no RFID UID; fetched by a separate query, not in the status stream.
- ⚠️ The load-bearing gotcha: on SDCP (CC1), sending an unrecognized command — or an incomplete start command — can crash the printer daemon and kill an active print. The whole SDCP control path is shaped around avoiding that (idle-gating, always-complete start payload, a mandatory post-upload settle).
Transport & connection¶
Moonraker path (Neptune 4 family, OrangeStorm Giga)¶
Stock, unmodified Moonraker HTTP + WebSocket JSON-RPC on :7125 — the standard stack documented in
klipper-moonraker.md. Elegoo's own SDK ships a model-agnostic Moonraker client (no
per-model logic), which is first-party confirmation that these printers need no bespoke code. 🟡 Elegoo-specific
transport notes:
- The firmware is a pinned, older Moonraker fork on a locked appliance image (the stock config carries an explicit warning against updating Klipper/Fluidd/Moonraker yourself, stating the machine will otherwise stop working). Treat any newer endpoint/field as maybe-absent and degrade rather than assume. 🟡
- The web UIs are on non-standard ports (Fluidd
4408, Mainsail4409), but Moonraker itself is still:7125. 🔵 - If the target Moonraker enforces auth, open with a
GET /access/oneshot_tokenhandshake before the socket — the SDK's client does this. 🟡
SDCP path (Centauri Carbon 1)¶
One WebSocket server, two roles. The printer listens on ws://<ip>:3030/websocket (plain, no TLS); the same
:3030 server also answers the HTTP file-upload endpoint. 🟡
- Not JSON-RPC 2.0. Each frame is a nested JSON object — an outer routing layer wrapping an inner command object:
🟡
{ "Id": "<machine UUID>", "Topic": "sdcp/request/<MainboardID>", "Data": { "Cmd": 128, "Data": { /* command params */ }, "RequestID": "<client hex>", "MainboardID": "<id>", "TimeStamp": 0, "From": 1 } } - Correlation is by an echoed
RequestID(a client-generated hex string), not a JSON-RPCid. Requests go onsdcp/request/<MainboardID>; the ack/result returns onsdcp/response/<MainboardID>echoing thatRequestID. 🟡 - Topic grammar (the frame carries its own
Topic, MQTT-like):sdcp/request(client→device),sdcp/response(acks, echoesRequestID),sdcp/status(pushed state),sdcp/attributes(static identity/caps),sdcp/error,sdcp/notice— each suffixed/<MainboardID>. 🟡 Fromis a source enum; a LAN client sendsFrom:1(the SDK does the same). Full set:0=PC/LAN,1=WEB_PC,2=WEB,3=APP,4=SERVER. 🟡- Connection limits: 5 concurrent WS connections (a 6th connect returns HTTP 500), and a 60 s idle timeout
closes the socket. Reuse one long-lived, auto-reconnecting socket per printer and keep it warm with a periodic
Cmd 0(there is no subscribe command to lean on). 🔵 - FDM-on-a-resin-envelope caveat: the published SDCP v3.0 spec is written for resin (MSLA) machines; the Centauri
Carbon is FDM and reuses the same envelope, discovery, and
Cmdcodes but substitutes FDM field names and re-purposes the status enum (see Reading state). 🟡
CC2 path (Centauri Carbon 2 / 2 Combo)¶
The printer runs its own MQTT broker; a client connects to it. Control rides MQTT; file bytes ride a separate HTTP server on the same box. 🟡
- Transport is
tcp://<ip>:1883only — MQTT 3.1.1, clean session, keepalive 60 s, QoS 1. There is no WebSocket / no:9001(a common community claim; the first-party SDK builds only the TCP URL). 🟡 - Serial-scoped topics:
elegoo/<sn>/api_register(pub),.../<requestId>/register_response(sub),.../api_status(pushed status, sub),.../<clientId>/api_request(commands, pub),.../<clientId>/api_response(sub). The serial number<sn>is a literal segment of every topic. 🟡 - Envelope is JSON-RPC-ish but not real JSON-RPC: commands are
{"id":<int>, "method":<int>, "params":{…}}; replies are{"id":<same>, "result":{…}}, correlated onid. Unsolicited status pushes arrive via push method6000(attributes via6008). 🟡 clientIdformat is"1_PC_"+ a random 4-digit number, reused as therequestId. 🟡- Mandatory registration handshake, per connection: subscribe the three topics above, then publish to
api_register; success is aregister_responsewitherror=="ok"and a matchingclient_id. The printer keeps no session state across TCP drops, so re-register on every reconnect. 🟡 - Heartbeat is exactly
{"type":"PING"}every ~10 s on the command topic (.../api_request), accepted on a JSONtype=="PONG"; timeout ~65 s. (There is no dedicated heartbeat topic.) 🟡 - Connection cap is printer-enforced with no numeric constant — an over-limit register attempt returns the string
"too many clients". Don't hardcode a number. 🟡
Discovery & identity¶
Manual IP entry works on all three paths; auto-discovery is a convenience. See
../patterns/discovery-and-credentials.md.
| Path | Mechanism | Identity key returned |
|---|---|---|
| Moonraker | SSDP / mDNS (Moonraker answers standard discovery); or manual IP | no stable vendor model id — supply a friendly name yourself |
| SDCP (CC1) | UDP broadcast of the 6-ASCII-byte string M99999 to :3000; each board replies with one JSON datagram |
MainboardID — the stable identity and the routing key in every topic |
| CC2 | UDP broadcast of {"id":0,"method":7000} to :52700; reply is {"id":…,"result":{…}} |
serial (sn) — the topic identity |
- SDCP discovery datagram carries
Name,MachineName(e.g. the model string),MainboardID,MainboardIP(used as the WS host),ProtocolVersion,FirmwareVersion(stored with a leadingVstripped). 🟡 A wrongMainboardIDmeans you publish/subscribe to topics the printer never touches — a silent no-op, not an error. - CC2 discovery reply carries
host_name,machine_model,sn,token_status(1⇒ an access code is required),lan_status, andauthMode:"accessCode". 🟡 A wrongsnis likewise a silent no-op. - Moonraker exposes no stable model id, so a "model" here is a user-assisted display/geometry overlay (name +
build volume), never a dispatch key. Advertised build volumes: Neptune 4 / 4 Pro 225×225×265, 4 Plus 320×320×385,
4 Max 420×420×480, OrangeStorm Giga 800×800×1000 mm. 🟡 Prefer the live
toolhead.axis_maximumwhen present.
Credentials / auth¶
Three postures — one per path — and this is where they diverge most:
- Moonraker (Neptune / Giga): no per-device credential in the common case. If the unit enforces Moonraker
auth, obtain a session via the standard
GET /access/oneshot_tokenhandshake before opening the socket. Seeklipper-moonraker.md. 🟡 - SDCP (CC1): no authentication at all — no handshake, no password, no token. The WS is open on the LAN; the only gates are the 5-connection cap and the 60 s idle timeout. There is no credential to collect or store, and no TLS. 🟡🔵 (LAN-trust; an implementer should keep the client cloud-out.)
- CC2: a single access code is both the MQTT password (with a fixed username
elegoo) and the HTTP uploadX-Token. The owner reads it from the printer's network-settings screen, and LAN mode must be enabled. Document the mechanism, never a value — an integration prompts the owner for their own code and, since it's a per-printer secret, should store it encrypted at rest. An empty/unset code falls back to a documented default, so a freshly-provisioned printer may accept the default until the owner sets one. 🟡 See../patterns/discovery-and-credentials.md.
Reading state¶
Moonraker path¶
Standard Klipper object-model reads (temps, print_stats, virtual_sdcard, fans, etc.) — see
klipper-moonraker.md. Two Elegoo deltas worth flagging:
- OrangeStorm Giga has a segmented, multi-zone bed exposed as
heater_bedplusheater_generic heater_bed1/2/3. These are bed zones, not a chamber — an implementer that foldsheater_generic *into "chamber" will mislabel them; excludeheater_generic heater_bed*from any chamber mapping. 🔵 - The Giga's optional multi-nozzle upgrade is IDEX (
[dual_carriage]+extruderN), not a docking toolchanger — it surfaces correctly as multiple extruders, and IDEX mode (copy/mirror) over the wire is uncaptured. 🟡 - Progress is file-byte position (Moonraker
virtual_sdcard.progress), so ETA-from-progress is unreliable near the end; times are seconds. See../patterns/timing-normalization.md.
SDCP path (CC1)¶
Status is a passive push on sdcp/status/<MainboardID>; a client can force a refresh with Cmd 0 (which replies
on the same topic). There is no subscribe command — state simply arrives. The frame carries a top-level machine
status, a nested PrintInfo job block, and temperature/fan/coordinate blocks. 🟡
Two enums, both load-bearing (keep them distinct):
- Top-level
CurrentStatus(0–11) — the machine state, and the idle gate keys offCurrentStatus == 0(IDLE).1=PRINTING,2=FILE_TRANSFERRING,8=FILE_CHECKING,9=HOMING, others busy/transient. 🟡 PrintInfo.Status(0–26) — the job sub-status, the real lifecycle signal:
| Native → normalized | Codes |
|---|---|
| standby | 0 IDLE |
| preheating / startup | 1 HOMING, 15 AUTOLEVELING, 16 PREHEATING, 18 PRINT_START, 19/21/22 *_COMPLETED |
| printing | 13 PRINTING (FDM active), 10/11 checking |
| paused | 5 PAUSING, 6 PAUSED |
| resuming | 12 RESUMING |
| cancelled | 7 STOPPING, 8 STOPED (sic) |
| complete | 9 COMPLETE |
| error | 14 ERROR |
| (never fire on FDM) | 2 DROPPING, 3 EXPOSURING, 4 LIFTING — resin-only leftovers |
Feed phases 23–26 (AUTO_FEEDING / FEEDOUT…) relate to material handling. A code outside 0–26 (newer firmware)
should normalize to unknown while preserving the raw value — never to error. 🟡
- A separate file/launch error field
ErrorNumber(0–5):0none,1MD5-check failed,2file-I/O failed,3invalid resolution,4unknown format,5unknown model. Any non-zero is an error. 🟡 - Temperatures (°C): nozzle
TempOfNozzle/TempTargetNozzle, bedTempOfHotbed/TempTargetHotbed, chamber/boxTempOfBox/TempTargetBox. 🟡 - Fans: a
CurrentFanSpeedblock{ModelFan, BoxFan, AuxiliaryFan}, each 0–100 — read-only (no fan-set command exists). 🟡
Timing (SDCP). ⚠️ These units are the classic trap — get them right:
CurrentTicks/TotalTicksare SECONDS (int64) — do not divide by 1000. 🟡- There is no
RemainTimefield. Remaining =TotalTicks − CurrentTicks(clamped ≥ 0). 🟡 Progressis a 0–100 int read verbatim from theProgresskey — not derived from ticks, and normalized as a time fraction. Whether the percent tracks time or layer at the very end of a print is an open runtime observation — treat it cautiously (as on CC2). 🟡- The envelope
TimeStampfields are Unix milliseconds — a different unit from the tick fields on the same frame. Don't apply one field's scaling to the other. See../patterns/timing-normalization.md.
CC2 path¶
Poll method 1002 (GET_STATUS) or consume the unsolicited api_status pushes — both carry the same result
object. Attributes come from method 1001 (sn, machine_model, firmware versions). 🟡
machine_status.status(0–15) → normalized:1Idle→standby,2Printing→printing (refine viasub_status), transient phases0/3–13(Initializing, Filament-Operating, Auto-Leveling, PID-Calibrating, Resonance-Testing, Self-Checking, Updating, Homing, File-Transferring, Video-Composing, Extruder-Operating) → printing/busy,14Emergency-Stop → error,15Power-Loss-Recovery → paused, and a-1sentinel → offline. 🟡machine_status.sub_statusis the finer lifecycle signal (0= none). Key codes:2075printing,2077print-completed → complete,2401/2402resuming,2501pausing /2502/2505paused,2503stopping /2504stopped → cancelled,1045/1096/1405/1906preheating,1081/1082/1086downloading. Unknown ints fall through to unknown. 🟡exception_statusis an array of raw ints with no fixed device enum — the SDK passes them through with no lookup. Treat any present code as error and surface the raw ints; per-int meanings are community lore only. 🔵- Temperatures (°C):
extruder{temperature,target,filament_detected},heater_bed{temperature,target},chamber_sensor{temperature,measured_max/min_temperature}. A multi-fanfansblock reports part/aux/box/heater/ controller each as{speed, rpm?}. 🟡
Timing (CC2). All time fields are SECONDS (int64): print_status.remaining_time_sec (firmware-reported →
prefer it), print_duration (extruding time), total_duration (wall clock). progress is a plain 0–100 int with
no scaling (no float, no ÷100). Whether that percent tracks time or file-position at the very end of a print is an
open runtime observation — treat it cautiously. See
../patterns/timing-normalization.md. 🟡
Writing / control¶
⚠️ Every command below drives a hot, moving machine — validate against your own device and gate writes behind an explicit opt-in. On SDCP especially, a malformed command can crash the printer (see Quirks).
SDCP (CC1) — integer Cmd codes¶
Active command set: 0 status, 1 attributes, 128 START, 129 PAUSE, 130 STOP, 131 RESUME,
324 canvas-read. CC1 does have a live resume (131). 🟡 There is no live temperature / fan / light / motion /
speed write — those codes exist only as commented-out stubs in the SDK; don't emit them. 🟡
Print-launch sequence (upload → settle → start):
- Upload —
POST http://<ip>:3030/uploadFile/upload,multipart/form-data, fieldFile= a ≤1 MB chunk. Per-chunk fields:S-File-MD5(whole-file MD5 hex, same on every chunk),Check(1= verify),Offset,Uuid(one value for the whole transfer),TotalSize. Each chunk must returncode=="000000"; abort on-1/-2/-3/-4. The remote filename is the basename of the uploaded path. 🟡 - Settle ~1 s — a required delay after the last chunk so the firmware can close the file handle. Skipping it risks a "file not found / read failed" ack. 🟡
- Start —
Cmd 128onsdcp/request/…carrying all six inner fields:Filename(must match the uploaded name),StartLayer(0= from the start; enables resume-from-layer),Calibration_switch,PrintPlatformType,Tlp_Switch(time-lapse),slot_map([]for single-material). The result arrives inline at the response'sAck(0OK;1busy,2file-not-found,3MD5-failed,4file-read-failed,5invalid-format,6model-mismatch). Confirm launch by state (sdcp/status→ PRINTING), not by the ack alone. 🟡
Job control: Cmd 129/130/131 (pause/stop/resume) carry an empty inner Data. Cmd 130 (stop) is
destructive — treat it as a dangerous, hold-to-confirm action. 🟡 Upload-and-hold is native — just skip the
Cmd 128.
CC2 — integer method codes¶
Active set: 1001 attributes, 1002 status, 1020 START, 1021 PAUSE, 1022 STOP, 1043
update-name, 2004 set-auto-refill, 2005 canvas-read, 1057/1058 remote-download start/cancel. 🟡
⚠️ There is no LAN resume on CC2. The resume method (
1023) is commented out in the SDK — the official client never sends it over LAN. Ship pause + cancel only; treat resume as unavailable (it exists on the device panel, just not over the wire). Home/move/set-temperature (1026/1027/1028) are likewise disabled over LAN. 🟡
Print-launch sequence (upload → start):
- Upload — chunked
PUT http://<ip>:80/uploadover a single keep-alive TCP connection for all chunks, 1 MB each. Per-chunk headers:Content-Type: application/octet-stream,Content-Range: bytes <offset>-<endInclusive>/<total>(note the space afterbytes; the end index is the last byte, inclusive),X-File-Name,X-File-MD5(whole-file MD5 hex),X-Token(the access code). Each chunk returns{"error_code":0};1000= bad token. 🟡 - Start — MQTT
method 1020onapi_requestwithparams.storage_media("local"/"u-disk"/"sd-card"),params.filename(must matchX-File-Name), andparams.config={delay_video, printer_check, print_layout ("A"/"B"), bedlevel_force, slot_map}. Success isresult.error_code == 0. 🟡
Job control: 1021 pause / 1022 stop (destructive) with empty params. Upload-and-hold is native — skip the
1020.
Moonraker path¶
Standard printer.print.start / pause / resume / cancel plus Moonraker file upload — see
klipper-moonraker.md. Note this is the only Elegoo path with a working resume across the
board.
Multi-material / feeders — CANVAS¶
CANVAS is Elegoo's 4-slot RFID feeder, available on the Centauri Carbon 1 (over SDCP) and the CC2 Combo (over MQTT).
It maps onto the cross-vendor feeder model in
../patterns/multi-material-feeders.md, where it is the sparse outlier
that proves per-slot "remaining %" must be nullable.
- Same shape on both transports. The feeder is read by a separate on-demand query —
Cmd 324on SDCP,method 2005on CC2 — with an empty request body; both return the identical structure. It is not in the continuous status push. A cheap presence flag (canvasConnected/ an external-device block) does ride the status stream, so an implementer polls the feeder query on connect, on a presence edge, and before a multicolor print. 🟡 - The report (snake_case wire keys): a top level of
active_canvas_id,active_tray_id(0= none), andauto_refill, thencanvas_list[]of units — each unit hascanvas_id(0–4; multi-unit is a first-class concept),connected, andtray_list[]. 🟡 - Per-slot record (
tray_list), exactly 9 fields:tray_id(0 = empty, 1–4),brand,filament_type,filament_name,filament_code(a filament-catalog code, e.g."0x00000"— not an RFID tag UID),filament_color(#RRGGBB),min_nozzle_temp,max_nozzle_temp(°C), andstatus(0empty /1pre-loaded /2loaded). 🟡 - What CANVAS does NOT expose (confirmed absent in the 9-field record, not merely unread): no per-slot remaining quantity/grams/weight, and no RFID tag UID (RFID drives auto-detect internally but the tag identity never reaches the wire). Both must degrade to null. There is also no per-slot dryer/humidity. 🟡
- Auto-refill (continue from an identical spool when one runs out) is toggled by
method 2004on CC2 and read back asauto_refill; runout surfaces as an entry in the CC2exception_statusarray, not a per-tray flag. 🔵 - Print-time slot map — the color→slot assignment, sent inside the start command as
slot_map, an array of{t, canvas_id, tray_id}integers (snake_case).tis the sliced tool/color index; the pair identifies the physical (unit, tray). Empty for single-color prints. ⚠️ The vendor SDK also contains a camelCase{t, trayId, canvasId}form — that is the SDK's internal API JSON, not the printer wire; emit the snake_case form. The file's own color plan (which color eachtwants) comes from a file-detail query (method 1046on CC2), joined to the slot map on the sharedtindex. 🟡 - Per-material consumption must come from the sliced file (there is no per-slot gram field on the wire) — see
../patterns/timing-normalization.md§4.
Quirks & gotchas¶
- Three protocols, one brand. Neptune/Giga = Moonraker; Centauri Carbon 1 = SDCP/WebSocket; Centauri Carbon 2 = MQTT. They share only the CANVAS report shape — route by model first.
- SDCP: unrecognized or incomplete commands can crash the printer. Sending a
Cmdthe firmware doesn't know can be silently dropped and then crash the daemon, killing an active print. Only probe speculative commands when the machine is idle (CurrentStatus == 0). And always send the full 6-field start payload — a partial start (some third-party clients send only{Filename, StartLayer}) crashes the firmware. Honor the ~1 s post-upload settle. 🟡🔵 - SDCP is not JSON-RPC — no
jsonrpc/method/id. Correlate on the echoedRequestID, and expect status to arrive unsolicited (there is no subscribe command; a periodicCmd 0is both poll and keepalive). - SDCP identity is the
MainboardID; CC2 identity is the serial. A wrong id/serial silently addresses topics the printer never touches — no error, just nothing happens. - CC2 has no LAN resume (see Writing). Panel-side resume exists; the wire command does not.
- CC2 slot-map is snake_case on the wire — the camelCase form in the SDK is internal-only.
- CC2 re-registration is per-connection — the printer keeps no session across TCP drops; re-run the handshake on
every reconnect, and keep the heartbeat exactly
{"type":"PING"}on the command topic. - CC2
:9001/WebSocket does not exist — transport is TCP:1883only, despite community claims otherwise. - CC2
Content-Rangeis standard-but-strict — a literal space afterbytesand an inclusive end index; the printer validates it. - Moonraker is a pinned, older fork on a locked image — don't update firmware (bricks the machine), assume older
API surface, and don't mislabel the Giga's
heater_bed1/2/3bed zones as a chamber. - Timing units differ by path and even by field — SDCP ticks are seconds (not ms) while its
TimeStampis ms; CC2 times are seconds; Moonraker times are seconds but its progress is file-byte position.
Confidence & validation¶
- Source basis (🟡): every wire fact — ports, envelopes, discovery payloads, method/
Cmdtables, status enums, tick units, upload headers, CANVAS structure and slot-map form — is read from Elegoo's own first-party LAN SDK (Apache-2.0), which ships all three paths, plus the official (resin-oriented) SDCP v3.0 spec for the SDCP envelope andFrom/error spans. This makes the shapes first-party-correct, but the orchard author has not hardware- validated the Centauri (CC1/CC2) or CANVAS paths — there is no owner-side bench for them. - Community hardware corroboration (🔵): the SDCP path is independently firmware-validated by more than one community client (tested against Centauri Carbon firmware in the ~1.1.x range) — the 5-connection cap, 60 s idle timeout, no-auth posture, and the crash-on-partial-start behavior come from real devices. The Moonraker path is routinely reached by third-party clients (Fluidd/Mainsail/others) on stock Neptune 4 hardware.
- Not yet confirmed / open gaps (a capture would close each):
- CC2 push cadence — how often
api_statusfires unsolicited (to decide whether a poll backstop is needed). - CC2 progress semantics near 100% — whether the 0–100 percent tracks time or file position at the end.
- Accepted file container/extension on CC1 and CC2 (raw g-code vs an Elegoo sliced container) — uncaptured on both.
- CANVAS live behavior — which slot
statusvalues fire mid-multicolor-print, real multi-unit daisy-chaining, and the exact runout/tangle exception codes (the CANVAS data shape is first-party; its runtime behavior is not hardware-checked, since the author has no CANVAS unit). - OrangeStorm Giga — a stock (not community-image) config capture, and the IDEX upgrade's wire shape.
- A single live CC1 lifecycle trace to confirm which of the 0–26 status codes actually emit in the wild, plus the exact coordinate/position field names.
Sources¶
Clean-room, facts-only. The authoritative source for all three paths is Elegoo's own first-party LAN SDK
(elegoo-link, Apache-2.0), which contains the CC1 (SDCP), CC2 (MQTT), and model-agnostic Moonraker adapters plus the
shared CANVAS type definitions — consumed here as uncopyrightable interface facts (ports, topic strings, JSON
field names, method/Cmd integers, discovery payloads, upload headers). Supporting sources: the official cbd-tech
SDCP v3.0 protocol spec (publisher-stated MIT) for the SDCP envelope, From enum, and error spans; Moonraker's
documented HTTP/WS API for the Neptune/OrangeStorm path; and MIT-licensed community clients (a Python SDCP
library, the cassini CLI, and a Home Assistant integration) as firmware-validated behavioral cross-checks. No SDK
or client code was copied; struct/field shapes are described, not reproduced. No Elegoo firmware source
(GPL-3.0) was vendored, and no secrets are included — the CC2 access code is documented as a mechanism (read from
the printer screen), never a value. Passed ../CLEANROOM-CHECKLIST.md.
Machine-readable (SDCP):
../schemas/elegoo/sdcp-envelope.json(frame + topics +From),sdcp-cmd-catalog.json(Cmd/Ack/ErrorNumber+ upload),sdcp-state-enum.json(the two status enums + timing).