pycentauri 0.5.1__tar.gz → 0.6.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. {pycentauri-0.5.1 → pycentauri-0.6.0}/CHANGELOG.md +124 -0
  2. pycentauri-0.6.0/PKG-INFO +470 -0
  3. pycentauri-0.6.0/README.md +420 -0
  4. {pycentauri-0.5.1 → pycentauri-0.6.0}/pyproject.toml +3 -1
  5. {pycentauri-0.5.1 → pycentauri-0.6.0}/src/pycentauri/__init__.py +16 -2
  6. {pycentauri-0.5.1 → pycentauri-0.6.0}/src/pycentauri/camera.py +1 -0
  7. pycentauri-0.6.0/src/pycentauri/cc2.py +703 -0
  8. {pycentauri-0.5.1 → pycentauri-0.6.0}/src/pycentauri/cli.py +149 -31
  9. {pycentauri-0.5.1 → pycentauri-0.6.0}/src/pycentauri/client.py +58 -5
  10. pycentauri-0.6.0/src/pycentauri/connect.py +80 -0
  11. {pycentauri-0.5.1 → pycentauri-0.6.0}/src/pycentauri/mcp/server.py +51 -22
  12. {pycentauri-0.5.1 → pycentauri-0.6.0}/src/pycentauri/models.py +81 -0
  13. {pycentauri-0.5.1 → pycentauri-0.6.0}/src/pycentauri/server.py +153 -50
  14. {pycentauri-0.5.1 → pycentauri-0.6.0}/src/pycentauri/web/app.js +149 -7
  15. pycentauri-0.6.0/src/pycentauri/web/index.html +233 -0
  16. pycentauri-0.6.0/src/pycentauri/web/styles.css +637 -0
  17. pycentauri-0.6.0/tests/test_cc2_mapping.py +272 -0
  18. {pycentauri-0.5.1 → pycentauri-0.6.0}/tests/test_server.py +27 -0
  19. pycentauri-0.5.1/PKG-INFO +0 -290
  20. pycentauri-0.5.1/README.md +0 -242
  21. pycentauri-0.5.1/src/pycentauri/web/index.html +0 -310
  22. pycentauri-0.5.1/src/pycentauri/web/styles.css +0 -955
  23. pycentauri-0.5.1/tests/integration/__init__.py +0 -0
  24. {pycentauri-0.5.1 → pycentauri-0.6.0}/.gitignore +0 -0
  25. {pycentauri-0.5.1 → pycentauri-0.6.0}/LICENSE +0 -0
  26. {pycentauri-0.5.1 → pycentauri-0.6.0}/src/pycentauri/discovery.py +0 -0
  27. {pycentauri-0.5.1 → pycentauri-0.6.0}/src/pycentauri/mcp/__init__.py +0 -0
  28. {pycentauri-0.5.1 → pycentauri-0.6.0}/src/pycentauri/mcp/__main__.py +0 -0
  29. {pycentauri-0.5.1 → pycentauri-0.6.0}/src/pycentauri/py.typed +0 -0
  30. {pycentauri-0.5.1 → pycentauri-0.6.0}/src/pycentauri/rtsp.py +0 -0
  31. {pycentauri-0.5.1 → pycentauri-0.6.0}/src/pycentauri/sdcp.py +0 -0
  32. {pycentauri-0.5.1 → pycentauri-0.6.0}/src/pycentauri/web/__init__.py +0 -0
  33. {pycentauri-0.5.1 → pycentauri-0.6.0}/tests/__init__.py +0 -0
  34. {pycentauri-0.5.1 → pycentauri-0.6.0}/tests/test_client.py +0 -0
  35. {pycentauri-0.5.1 → pycentauri-0.6.0}/tests/test_discovery.py +0 -0
  36. {pycentauri-0.5.1 → pycentauri-0.6.0}/tests/test_rtsp.py +0 -0
  37. {pycentauri-0.5.1 → pycentauri-0.6.0}/tests/test_sdcp.py +0 -0
@@ -6,6 +6,130 @@ Changelog](https://keepachangelog.com/en/1.1.0/).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.6.0] - 2026-07-05
10
+
11
+ ### Added
12
+ - **Centauri Carbon 2 support.** The CC2 uses MQTT on port 1883
13
+ (instead of the CC1's WebSocket SDCP on 3030) with a JSON-RPC
14
+ envelope, X-Token auth, and a completely different command set
15
+ (1001/1002/1020–1031 vs 0/1/128–512). This release adds:
16
+ - `CC2Printer` — async MQTT client that subclasses `Printer`,
17
+ translating CC2 payloads into the same `Status` / `Attributes`
18
+ models so every surface (CLI, HTTP, MCP, web UI) works unchanged.
19
+ - `connect_auto(host, access_code=...)` — factory that detects the
20
+ model (probes `:1883` only — a CC1 answers with a harmless kernel
21
+ RST, and the CC1's real WebSocket connect doubles as its own probe)
22
+ and returns the right `Printer` subclass. Callers never need to
23
+ know which model they're talking to.
24
+ - `--access-code` / `PYCENTAURI_ACCESS_CODE` on every CLI command
25
+ and MCP launch (required for CC2, ignored for CC1).
26
+ - `paho-mqtt>=2.0` added to core dependencies.
27
+ - CC2 status exposes `gcode_move.speed` — live head speed as the
28
+ commanded speed of the current move, in mm/min (÷60 matches the
29
+ printer screen's mm/s readout)
30
+ and `speed_mode` (integer 0–3) — fields the CC1 lacks entirely. Both
31
+ are preserved in `Status.raw["_cc2"]` alongside `remaining_time_sec`,
32
+ `filament_detected`, `external_device`, and `exception_status`.
33
+ - CC2 methods 1028 (SET_TEMPERATURE), 1029 (SET_LIGHT), 1030
34
+ (SET_FAN_SPEED), 1031 (SET_PRINT_SPEED), 1023 (RESUME), and 1036
35
+ (PRINT_TASK_LIST) all confirmed working. 1042 (VIDEO_STREAM) and
36
+ 1044 (GET_FILE_LIST) are non-responsive on firmware 01.03.02.51.
37
+ - **Canvas multi-filament system integration (CC2 only):**
38
+ - `Printer.canvas_status()` — returns `CanvasStatus` with connected
39
+ units, tray list (filament name/type/color/brand/temp range/loaded
40
+ status), active tray, and auto-refill state. Method 2005.
41
+ - `Printer.set_auto_refill(enabled)` — toggles auto-refill. Method
42
+ 2004. Gated by `enable_control`.
43
+ - CLI: `centauri canvas` (read) + `centauri refill --on/--off`
44
+ (write, requires `--enable-control`).
45
+ - HTTP: `GET /canvas` (always) + `POST /canvas/refill` (control-gated).
46
+ - MCP: `get_canvas_status` (always) + `set_auto_refill` (control-gated).
47
+ - `CanvasStatus`, `CanvasUnit`, `CanvasTray` model classes exported
48
+ from the package for typed access.
49
+
50
+ ### Fixed (pre-release hardening — none of these shipped in an earlier release)
51
+ - **CC1 `status()`/`watch()` self-heal through a wedged push
52
+ scheduler.** Discovered live 2026-07-04 on V0.3.0-o: the firmware can
53
+ enter a state where `Cmd 512` subscribes are accepted but no status
54
+ frames are ever pushed, while the request path stays healthy — and a
55
+ reboot does not clear it (verified 2026-07-05). Both methods now fall
56
+ back to explicit `Cmd 0` status requests (documented to emit a
57
+ one-shot status frame) whenever a subscribe produces no push within
58
+ one period, instead of hanging forever. See `docs/PROTOCOL.md`
59
+ "Operational quirks".
60
+ - `CC2Printer.watch()` now polls method 1002 inline whenever pushes go
61
+ quiet instead of silently terminating after one push period — CC2
62
+ `centauri watch` and the SSE stream no longer die on an idle printer.
63
+ - MQTT responses are correlated by the client's exact `api_response`
64
+ topic and non-push method. Previously any client's response — or the
65
+ printer's own 6000/6008 broadcasts, which reuse small auto-increment
66
+ ids — could resolve a pending request future with the wrong payload
67
+ (including reporting success for a control command).
68
+ - Registration is re-published from `_on_connect`, so paho's automatic
69
+ MQTT reconnect re-registers with the printer and pushes survive a
70
+ printer reboot without restarting `centauri server`.
71
+ - All MQTT-callback work that touches futures, events, queues, or the
72
+ merged status dict is marshalled onto the asyncio loop (fixes the
73
+ cross-thread delta merge and late `set_result` races).
74
+ - `GET /stream` uses a bounded connect timeout (read stays unbounded
75
+ for MJPEG) instead of hanging forever against a wedged camera port.
76
+ - `GET /canvas` returns 504 for printer timeouts, reserving 501 for
77
+ "no Canvas on this printer"; the web UI now retries transient Canvas
78
+ failures instead of hiding the panel until a page reload.
79
+ - A wrong CC2 access code now raises a clear auth error (MQTT reason
80
+ code / HTTP 401 hint) instead of a generic timeout or raw traceback.
81
+ - `connect_auto()` and `centauri rtsp` no longer probe :3030 — the CC1
82
+ WebSocket connect doubles as the probe, avoiding the connect/close
83
+ churn that port is sensitive to. `centauri rtsp` and the HTTP server
84
+ both pick the right MJPEG port (CC1 :3031 / CC2 :8080) automatically,
85
+ including reconnects after starting with the printer offline.
86
+ - Unknown CC2 `machine_status` values map to sentinel code 99 instead
87
+ of leaking raw values into the CC1 status-code space; `PrintStatus`
88
+ gains constants for the CC2 codes 27/28/29.
89
+ - `CanvasStatus.from_payload` survives malformed/absent `canvas_info`
90
+ payloads; `CanvasUnit`/`CanvasTray` are exported from the package
91
+ root; `typing_extensions` declared as a direct dependency.
92
+ - **CC2 sessions send the app-level `{"type": "PING"}` keepalive every
93
+ 30 s.** The firmware expires a client's registration after several
94
+ quiet minutes and then silently stops answering that session's
95
+ requests — the MQTT connection stays up, responses just never come
96
+ (verified 2026-07-05: a dashboard went request-deaf after ~6 minutes
97
+ while a fresh session answered instantly). MQTT-level keepalive does
98
+ not prevent this; only the app-level PING does.
99
+ - `centauri server` bounds uvicorn's graceful shutdown at 5 s. Open
100
+ SSE/MJPEG streams never close on their own, and without the bound a
101
+ stopped server lingered forever as a zombie still holding a printer
102
+ connection (observed 2026-06-23 and 2026-07-05).
103
+ - CC2 lifecycle commands (start/pause/stop/resume) use a 90 s timeout —
104
+ the firmware answers only after the mechanical sequence completes
105
+ (observed 2026-07-05: a resume succeeded but its confirmation
106
+ arrived after the old 15 s window, surfacing a false error). When a
107
+ confirmation still doesn't arrive, `POST /print/*` returns 504
108
+ ("sent, unconfirmed — check printer status") instead of 502, and the
109
+ web UI shows it as a warning rather than a failure.
110
+
111
+ ### Tests
112
+ - New `tests/test_cc2_mapping.py`: pure dict-in/dict-out coverage of
113
+ the CC2→CC1 translation layer — machine_status/sub_status mapping
114
+ (including purge-zone filament-switch detection and its progress
115
+ guard), partial-delta deep merge, full 1002 payload round-trip, and
116
+ Canvas parsing. Plus a server test pinning `GET /canvas` → 501 on
117
+ CC1.
118
+
119
+ ### Documentation
120
+ - `docs/PROTOCOL.md` gains a full CC2 section: MQTT transport, topic
121
+ structure, connection/registration dance, the confirmed method table
122
+ (12 probed, 10 working), error codes, the complete status payload,
123
+ and a CC1-vs-CC2 comparison table. Probed on 2026-06-30 through
124
+ 2026-07-04 against firmware 01.03.02.51 (Canvas methods 2004/2005
125
+ verified live with a 4-slot Canvas attached; CC2 webcam confirmed
126
+ unauthenticated MJPEG on :8080).
127
+ - `docs/ARCHITECTURE.md` rewritten for the two-transport reality:
128
+ module map with `cc2`/`connect`, per-model ports, CC1-vs-CC2
129
+ `/status` and SSE flow differences, and an honest test-architecture
130
+ section (no automated live suite exists; live verification is manual
131
+ CLI runs).
132
+
9
133
  ## [0.5.1] - 2026-06-17
10
134
 
11
135
  ### Added
@@ -0,0 +1,470 @@
1
+ Metadata-Version: 2.4
2
+ Name: pycentauri
3
+ Version: 0.6.0
4
+ Summary: Local-network toolkit for Elegoo Centauri Carbon 3D printers: async Python client, CLI, MCP server, REST/SSE HTTP server, web UI, and RTSP bridge
5
+ Project-URL: Homepage, https://github.com/bjan/pycentauri
6
+ Project-URL: Repository, https://github.com/bjan/pycentauri
7
+ Project-URL: Issues, https://github.com/bjan/pycentauri/issues
8
+ Project-URL: Changelog, https://github.com/bjan/pycentauri/blob/main/CHANGELOG.md
9
+ Author: Brandon
10
+ License-Expression: Apache-2.0
11
+ License-File: LICENSE
12
+ Keywords: 3d-printing,centauri,centauri-carbon,elegoo,mcp,sdcp
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Intended Audience :: End Users/Desktop
16
+ Classifier: License :: OSI Approved :: Apache Software License
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Topic :: Printing
24
+ Classifier: Typing :: Typed
25
+ Requires-Python: >=3.10
26
+ Requires-Dist: httpx>=0.27
27
+ Requires-Dist: paho-mqtt>=2.0
28
+ Requires-Dist: pydantic>=2.7
29
+ Requires-Dist: typer>=0.12
30
+ Requires-Dist: typing-extensions>=4.6
31
+ Requires-Dist: websockets>=12.0
32
+ Provides-Extra: dev
33
+ Requires-Dist: fastapi>=0.110; extra == 'dev'
34
+ Requires-Dist: httpx>=0.27; extra == 'dev'
35
+ Requires-Dist: mcp>=1.2; extra == 'dev'
36
+ Requires-Dist: mypy>=1.10; extra == 'dev'
37
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
38
+ Requires-Dist: pytest-cov>=5.0; extra == 'dev'
39
+ Requires-Dist: pytest>=8.0; extra == 'dev'
40
+ Requires-Dist: ruff>=0.5; extra == 'dev'
41
+ Requires-Dist: sse-starlette>=2.0; extra == 'dev'
42
+ Requires-Dist: uvicorn[standard]>=0.29; extra == 'dev'
43
+ Provides-Extra: mcp
44
+ Requires-Dist: mcp>=1.2; extra == 'mcp'
45
+ Provides-Extra: server
46
+ Requires-Dist: fastapi>=0.110; extra == 'server'
47
+ Requires-Dist: sse-starlette>=2.0; extra == 'server'
48
+ Requires-Dist: uvicorn[standard]>=0.29; extra == 'server'
49
+ Description-Content-Type: text/markdown
50
+
51
+ # pycentauri
52
+
53
+ Local-network toolkit for [Elegoo Centauri Carbon](https://www.elegoo.com/)
54
+ 3D printers — the **original Centauri Carbon (CC1)** and the **Centauri
55
+ Carbon 2 (CC2)**. One async client, six surfaces: Python library, CLI,
56
+ MCP server for AI agents, REST/SSE HTTP server, built-in web dashboard,
57
+ and an RTSP bridge for your NVR.
58
+
59
+ No cloud account, no Elegoo servers — everything talks directly to the
60
+ printer on your LAN. `pycentauri` auto-detects which model it's talking
61
+ to and speaks the right protocol:
62
+
63
+ | | CC1 | CC2 |
64
+ |---|---|---|
65
+ | Transport | SDCP v3 over WebSocket (`:3030`) | JSON-RPC over MQTT (`:1883`) |
66
+ | Auth | none | access code (printer screen) |
67
+ | Discovery | UDP broadcast | direct IP + HTTP bootstrap |
68
+ | Webcam | MJPEG `:3031` | MJPEG `:8080` |
69
+ | Live head speed | — | ✓ (`gcode_move.speed` ÷ 60 = the screen's mm/s readout) |
70
+ | Fan channels | 3 | 5 |
71
+ | Canvas multi-filament | — | ✓ (status + auto-refill) |
72
+ | Filament-switch detection | — | ✓ (position-based) |
73
+
74
+ > **Status:** alpha, but used daily against real printers. Protocols were
75
+ > reverse-engineered from Elegoo's official
76
+ > [`elegoo-link`](https://github.com/ELEGOO-3D/elegoo-link) C++ SDK, the
77
+ > [`CentauriLink`](https://github.com/CentauriLink/Centauri-Link) project,
78
+ > and live wire captures. CC1 tested on firmware V1.1.46 and OpenCentauri
79
+ > V0.3.0-o; CC2 tested on firmware 01.03.02.51. Full wire-protocol notes
80
+ > live in [`docs/PROTOCOL.md`](docs/PROTOCOL.md).
81
+
82
+ ## Install
83
+
84
+ ```sh
85
+ pip install pycentauri # library + CLI
86
+ pip install "pycentauri[mcp]" # + MCP server
87
+ pip install "pycentauri[server]" # + HTTP REST/SSE server + web UI
88
+ pip install "pycentauri[mcp,server]" # all Python surfaces
89
+ ```
90
+
91
+ The RTSP bridge additionally requires
92
+ [MediaMTX](https://github.com/bluenviron/mediamtx/releases) and `ffmpeg`
93
+ on `$PATH`.
94
+
95
+ Python 3.10+. Core dependencies: `websockets`, `paho-mqtt`, `httpx`,
96
+ `typer`, `pydantic`.
97
+
98
+ ## Connecting to your printer
99
+
100
+ **CC1** needs only its IP (or nothing at all — it answers UDP discovery).
101
+
102
+ **CC2** needs its IP *and* its access code, found on the printer's
103
+ touchscreen under network/connectivity settings. Pass it as
104
+ `--access-code` / `access_code=` / `PYCENTAURI_ACCESS_CODE`. The examples
105
+ below use `Ab3dEf` as a stand-in — substitute your own.
106
+
107
+ Every CLI command accepts `--host` (env: `PYCENTAURI_HOST`). With no host
108
+ given, commands try UDP discovery, which only finds CC1s.
109
+
110
+ ## CLI
111
+
112
+ ```sh
113
+ # Discovery (CC1 only — CC2 doesn't answer broadcasts)
114
+ centauri discover
115
+
116
+ # Status, attributes, live watch, snapshot
117
+ centauri status --host 192.168.1.209 # CC1
118
+ centauri status --host 192.168.1.189 --access-code Ab3dEf # CC2
119
+ centauri status --host 192.168.1.209 --json
120
+ centauri attributes --host 192.168.1.209
121
+ centauri watch --host 192.168.1.209
122
+ centauri snapshot --host 192.168.1.209 shot.jpg
123
+
124
+ # Print control — all writes require --enable-control
125
+ centauri print start cube.gcode --host 192.168.1.209 --enable-control
126
+ centauri print pause --host 192.168.1.209 --enable-control
127
+ centauri print resume --host 192.168.1.209 --enable-control
128
+ centauri print stop --host 192.168.1.209 --enable-control
129
+
130
+ # Live adjust while printing
131
+ centauri speed sport --host 192.168.1.209 --enable-control
132
+ centauri fan --model 100 --aux 60 --chamber 30 --host 192.168.1.209 --enable-control
133
+ centauri temp --nozzle 215 --bed 60 --host 192.168.1.209 --enable-control
134
+
135
+ # Canvas multi-filament (CC2 only)
136
+ centauri canvas --host 192.168.1.189 --access-code Ab3dEf
137
+ centauri refill --on --host 192.168.1.189 --access-code Ab3dEf --enable-control
138
+ ```
139
+
140
+ `centauri canvas` prints each tray's filament, color, temperature range,
141
+ and loaded state:
142
+
143
+ ```
144
+ auto_refill : OFF
145
+ active_tray : none
146
+ connected : yes
147
+
148
+ canvas #0:
149
+ ● tray 0: PLA Wood (PLA) #F72221 [190-230°C]
150
+ ● tray 1: PLA Wood (PLA) #AF7832 [190-230°C]
151
+ ● tray 2: PETG (PETG) #A03BF7 [230-260°C]
152
+ ● tray 3: PLA Wood (PLA) #D2C5A3 [190-230°C]
153
+ ```
154
+
155
+ ### Speed modes
156
+
157
+ Both printers accept exactly four speed settings — arbitrary percentages
158
+ are silently ignored by the firmware:
159
+
160
+ | Mode | CC1 wire value (`PrintSpeedPct`) | CC2 wire value (`speed_mode`) |
161
+ |---|---|---|
162
+ | `silent` | 50 | 0 |
163
+ | `balanced` | 100 | 1 |
164
+ | `sport` | 130 | 2 |
165
+ | `ludicrous` | 160 | 3 |
166
+
167
+ Speed changes only take effect while a print is actively running.
168
+
169
+ ## Python library
170
+
171
+ ```python
172
+ import asyncio
173
+ from pycentauri import Printer, CC2Printer, connect_auto
174
+
175
+ async def main():
176
+ # Explicit CC1
177
+ async with await Printer.connect("192.168.1.209") as printer:
178
+ st = await printer.status()
179
+ print(st.print_status, st.progress, st.temp_nozzle)
180
+
181
+ # Explicit CC2
182
+ async with await CC2Printer.connect("192.168.1.189", access_code="Ab3dEf") as printer:
183
+ st = await printer.status()
184
+ print(st.temp_nozzle, st.raw["_cc2"]["gcode_move_speed"]) # mm/min; ÷60 = screen's mm/s
185
+
186
+ canvas = await printer.canvas_status()
187
+ for unit in canvas.canvas_list:
188
+ for tray in unit.tray_list:
189
+ print(tray.tray_id, tray.filament_name, tray.filament_color)
190
+
191
+ # Auto-detect — port-probes :3030 vs :1883 and returns the right class
192
+ async with await connect_auto("192.168.1.189", access_code="Ab3dEf") as printer:
193
+ attrs = await printer.attributes()
194
+ print(attrs.machine_name, attrs.firmware_version)
195
+
196
+ asyncio.run(main())
197
+ ```
198
+
199
+ Both classes expose the same API: `status()`, `attributes()`, `watch()`
200
+ (async iterator of live status), `snapshot()`, `start_print()`, `pause()`,
201
+ `resume()`, `stop()`, `set_print_speed()`, `set_fan_speed()`,
202
+ `set_temperatures()`, `canvas_status()`, `set_auto_refill()`. Write
203
+ methods require `enable_control=True` at connect time and raise
204
+ `ControlDisabledError` otherwise. Canvas methods raise `PrinterError` on
205
+ CC1 (no Canvas support over SDCP).
206
+
207
+ CC2-only telemetry rides along in `Status.raw["_cc2"]`: the live head
208
+ speed (`gcode_move_speed` — the commanded speed of the current move in
209
+ **mm/min**; divide by 60 for the mm/s figure the printer's screen
210
+ shows), `speed_mode`, filament runout sensor state,
211
+ firmware-computed `remaining_time_sec`, `machine_status`/`sub_status`
212
+ raw codes, and `external_device` (camera / U-disk presence).
213
+
214
+ ## HTTP server + web UI
215
+
216
+ ```sh
217
+ # Read-only, loopback only
218
+ centauri server --host 192.168.1.209
219
+
220
+ # Read + write + RTSP, on the LAN (put an authenticating proxy in front)
221
+ centauri server --host 192.168.1.209 --bind 0.0.0.0 --port 8787 \
222
+ --enable-control --rtsp
223
+
224
+ # CC2
225
+ centauri server --host 192.168.1.189 --access-code Ab3dEf \
226
+ --bind 0.0.0.0 --port 8787 --enable-control
227
+ ```
228
+
229
+ The server holds a single long-lived connection to the printer
230
+ (WebSocket for CC1, MQTT for CC2) with automatic reconnect and
231
+ exponential backoff — it will never exhaust CC1's 5-connection limit.
232
+
233
+ The **web UI** at `/ui/` is a clean dark dashboard, mobile-friendly and
234
+ dependency-free (no CDN assets — works on an air-gapped LAN): live
235
+ webcam, job progress with layer/ETA, printer state, thermals, kinematics
236
+ (with live head speed on CC2), pause/resume/stop, speed-mode selector,
237
+ fan and heater sliders that hydrate from live values, a Canvas panel
238
+ with per-tray color swatches and an auto-refill toggle (CC2), and RTSP
239
+ bridge controls. Control panels only render when the server was started
240
+ with `--enable-control`.
241
+
242
+ ### Endpoints
243
+
244
+ | Method | Path | Notes |
245
+ |---|---|---|
246
+ | `GET` | `/` | Redirects to `/ui/` |
247
+ | `GET` | `/ui/` | Web dashboard |
248
+ | `GET` | `/api/info` | Health + version + connection state |
249
+ | `GET` | `/status` | Latest status (typed summary + full `raw` payload) |
250
+ | `GET` | `/attributes` | Model, firmware, mainboard ID |
251
+ | `GET` | `/snapshot` | Single JPEG frame |
252
+ | `GET` | `/stream` | MJPEG proxy (drop into an `<img>` tag) |
253
+ | `GET` | `/events/status` | Server-Sent Events stream of status pushes |
254
+ | `GET` | `/discover` | UDP LAN scan (finds CC1s) |
255
+ | `GET` | `/canvas` | Canvas state (CC2; `501` on CC1) |
256
+ | `GET` | `/docs`, `/redoc` | OpenAPI documentation |
257
+ | `POST` | `/print/start` | `{"filename": "cube.gcode", "storage": "local"}` † |
258
+ | `POST` | `/print/pause` · `/print/resume` · `/print/stop` | † |
259
+ | `POST` | `/print/speed` | `{"mode": "sport"}` or `{"mode": 130}` † |
260
+ | `POST` | `/print/fan` | `{"model": 50, "auxiliary": 30, "chamber": 0}` — any subset, 0–100 † |
261
+ | `POST` | `/print/temperature` | `{"nozzle": 215, "bed": 60}` — any subset, °C, 0 = off † |
262
+ | `POST` | `/canvas/refill` | `{"enabled": true}` (CC2) † |
263
+ | `GET` | `/api/rtsp` | RTSP bridge state (when `--rtsp`) |
264
+ | `POST` | `/api/rtsp/start` · `/api/rtsp/stop` | Toggle the bridge (when `--rtsp`) |
265
+
266
+ † requires the server to be launched with `--enable-control`; otherwise
267
+ the route isn't registered at all.
268
+
269
+ Temperature writes are bounds-checked server-side: nozzle 0–300 °C,
270
+ bed 0–110 °C, chamber 0–60 °C.
271
+
272
+ ## MCP server (AI agents)
273
+
274
+ Give Claude Code, Claude Desktop, Cursor, or any MCP client eyes and
275
+ hands on your printer:
276
+
277
+ ```sh
278
+ # Read-only (status, snapshot, attributes, discovery, canvas)
279
+ claude mcp add pycentauri --env PYCENTAURI_HOST=192.168.1.209 \
280
+ -- python -m pycentauri.mcp
281
+
282
+ # With control tools
283
+ claude mcp add pycentauri-cc2 \
284
+ --env PYCENTAURI_HOST=192.168.1.189 \
285
+ --env PYCENTAURI_ACCESS_CODE=Ab3dEf \
286
+ -- python -m pycentauri.mcp --enable-control
287
+ ```
288
+
289
+ The target host is pinned in the server's environment at spawn time — a
290
+ prompt-injected agent cannot redirect commands to an arbitrary IP,
291
+ because no tool takes a host parameter.
292
+
293
+ | Tool | Availability | Description |
294
+ |---|---|---|
295
+ | `get_status` | always | State, temps, progress, layer, position, fans |
296
+ | `get_attributes` | always | Model, firmware, mainboard ID |
297
+ | `get_snapshot` | always | Webcam frame as MCP image — the model *sees* the print |
298
+ | `discover_printers` | always | UDP LAN scan |
299
+ | `get_canvas_status` | always | Canvas trays, colors, auto-refill (CC2) |
300
+ | `start_print` | `--enable-control` | Start a file already on the printer |
301
+ | `pause_print` / `resume_print` / `stop_print` | `--enable-control` | Job control |
302
+ | `set_print_speed` | `--enable-control` | `silent`/`balanced`/`sport`/`ludicrous` |
303
+ | `set_fan_speed` | `--enable-control` | Any subset of model/aux/chamber, 0–100% |
304
+ | `set_temperatures` | `--enable-control` | Any subset of nozzle/bed/chamber, °C |
305
+ | `set_auto_refill` | `--enable-control` | Canvas auto-refill toggle (CC2) |
306
+
307
+ Control tools aren't merely gated — without the flag they are never
308
+ registered, so they don't appear in the model's tool list at all.
309
+
310
+ ## RTSP bridge
311
+
312
+ Re-streams the printer's MJPEG webcam as H.264/RTSP for clients that
313
+ don't speak MJPEG — Home Assistant, Frigate, Jellyfin, Synology
314
+ Surveillance, VLC:
315
+
316
+ ```sh
317
+ # Standalone (foreground, Ctrl-C to stop)
318
+ centauri rtsp --host 192.168.1.209
319
+ # → rtsp://<this-host>:8554/printer
320
+
321
+ # Integrated with the HTTP server — adds a STREAM panel to the web UI
322
+ centauri server --host 192.168.1.209 --rtsp --bind 0.0.0.0
323
+ ```
324
+
325
+ MediaMTX only runs the ffmpeg transcode while a client is connected, so
326
+ idle cost is zero. Tunables: `--fps`, `--bitrate`, `--preset`, `--path`,
327
+ `--port` (standalone) or the `--rtsp-*` variants on `centauri server`.
328
+ The bridge picks the correct camera port for CC1 vs CC2 automatically.
329
+
330
+ ## Print status codes
331
+
332
+ `print_status` in the API and library uses the CC1 firmware's code
333
+ space, extended with three codes for CC2 Canvas operations:
334
+
335
+ | Code | Meaning | | Code | Meaning |
336
+ |---|---|---|---|---|
337
+ | 0 | Idle | | 13 | Printing |
338
+ | 1 | Homing | | 14 | Error |
339
+ | 5 | Pausing | | 15 | Leveling |
340
+ | 6 | Paused | | 16 | Preheating |
341
+ | 7 | Stopping | | **27** | **Switching filament** (CC2) |
342
+ | 8 | Stopped | | **28** | **Filament load complete** (CC2) |
343
+ | 9 | Completed | | **29** | **Unloading filament** (CC2) |
344
+
345
+ The full table (including CC1's resin-inherited codes) is in
346
+ [`docs/PROTOCOL.md`](docs/PROTOCOL.md).
347
+
348
+ On the CC2, mid-print Canvas filament switches are detected by head
349
+ position: the firmware never fully leaves its "printing" state during a
350
+ switch, but the head parks at the purge chute behind the bed (y ≥ 258 mm,
351
+ physically outside the printable area) for the duration. pycentauri
352
+ reports code 27 the entire time the head is parked there mid-print.
353
+
354
+ ## Safety model
355
+
356
+ - **Explicit opt-in for writes.** Every surface requires
357
+ `enable_control=True` / `--enable-control` before any state-changing
358
+ command is possible. Read-only is the default everywhere.
359
+ - **Bounds-checked heaters.** The library refuses temperature targets
360
+ outside nozzle 0–300 °C / bed 0–110 °C / chamber 0–60 °C even though
361
+ the firmware might accept them.
362
+ - **Unauthenticated HTTP surface.** The REST server has no auth of its
363
+ own. Bind it to loopback (the default) or put an authenticating
364
+ reverse proxy in front before exposing it beyond localhost.
365
+ - **You own unattended printing.** An agent with control tools can pause,
366
+ stop, or heat your printer. Leaving one unattended is your call.
367
+
368
+ ## Known firmware quirks
369
+
370
+ ### CC1 (original Centauri Carbon)
371
+
372
+ - **5 concurrent WebSocket slots, hard.** The 6th connection gets HTTP
373
+ 500 `"too many client"`. Slots free on close. The CLI opens one per
374
+ invocation; the HTTP/MCP servers hold exactly one long-lived slot.
375
+ - **Paused/errored states don't push Attributes.** Every SDCP command
376
+ needs the printer's `MainboardID`, which normally arrives in an
377
+ Attributes push — but not while paused or errored. pycentauri
378
+ pre-seeds it from UDP discovery on every connect. If you call
379
+ `Printer.connect()` on a paused printer without discovery, pass
380
+ `mainboard_id=` yourself.
381
+ - **Unknown commands crash the firmware.** A few unrecognised SDCP
382
+ commands in quick succession kill the printer's `app` daemon — and any
383
+ active print with it. Don't probe undocumented command codes against a
384
+ printer that's doing something you care about.
385
+ - **The push scheduler goes dormant at idle (and a reboot doesn't wake
386
+ it).** The firmware can enter a state where `Cmd 512` subscribes are
387
+ acknowledged but no status frame is ever pushed while the printer
388
+ sits idle — persisting across reboots (verified 2026-07-05 on
389
+ V0.3.0-o). Starting a print revives pushes at full rate. One-shot
390
+ `Cmd 0` requests always work, so pycentauri automatically falls back
391
+ to polling when a subscribe goes quiet (~7 s updates at idle,
392
+ full-rate pushes while printing). Clients that rely purely on
393
+ subscribe pushes will hang forever on an idle printer in this state.
394
+
395
+ ### CC2 (Centauri Carbon 2)
396
+
397
+ - **No UDP discovery.** The CC2 ignores broadcast probes; specify its IP
398
+ explicitly. Give it a static DHCP lease — it doesn't register a
399
+ hostname with most routers, so its address drifts otherwise.
400
+ - **Access code required for MQTT**, passed as the password with
401
+ username `elegoo`. The HTTP bootstrap (`/system/info`) wants the same
402
+ code as an `X-Token` *query parameter* — it ignores the header form.
403
+ - **The webcam is unauthenticated.** MJPEG on `:8080` (any path) is open
404
+ to anyone on your LAN, access code or not. That's the firmware's
405
+ choice, not ours.
406
+ - **Rate limiting.** Rapid-fire MQTT requests (3+ back-to-back) trip a
407
+ cooldown of a few seconds during which the broker silently drops
408
+ responses. pycentauri's polling cadence stays under it; your scripts
409
+ should too.
410
+ - **Registrations expire without an app-level PING.** The printer
411
+ forgets a registered client after several quiet minutes and silently
412
+ stops answering that session's requests — the MQTT connection itself
413
+ stays up, so there's no error to catch. pycentauri sends the SDK's
414
+ `{"type": "PING"}` keepalive every 30 s to hold the registration; if
415
+ you write your own client, you must too.
416
+ - **File list (method 1044) and video stream (1042) don't respond** on
417
+ firmware 01.03.02.51, so remote print-start on CC2 requires knowing
418
+ the filename in advance.
419
+
420
+ ## Project layout & docs
421
+
422
+ ```
423
+ src/pycentauri/
424
+ ├── client.py # CC1: async SDCP-over-WebSocket client
425
+ ├── cc2.py # CC2: async JSON-RPC-over-MQTT client (same API)
426
+ ├── connect.py # connect_auto() — port-probe model detection
427
+ ├── sdcp.py # SDCP v3 envelope build/parse
428
+ ├── discovery.py # UDP broadcast discovery
429
+ ├── camera.py # MJPEG frame grabber
430
+ ├── models.py # Status / Attributes / CanvasStatus / PrintInfo
431
+ ├── cli.py # Typer CLI
432
+ ├── server.py # FastAPI app + connection supervisor
433
+ ├── rtsp.py # MediaMTX/ffmpeg bridge
434
+ ├── mcp/ # FastMCP stdio server
435
+ └── web/ # Static dashboard (no build step, no CDN)
436
+ ```
437
+
438
+ - [`docs/PROTOCOL.md`](docs/PROTOCOL.md) — both wire protocols in
439
+ detail: envelopes, command/method tables with tested-on dates, status
440
+ payloads, error codes, failure modes, and a CC1-vs-CC2 comparison.
441
+ - [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) — module map and
442
+ request flow.
443
+
444
+ ## Development
445
+
446
+ ```sh
447
+ git clone https://github.com/bjan/pycentauri && cd pycentauri
448
+ python -m venv .venv && .venv/bin/pip install -e ".[mcp,server,dev]"
449
+
450
+ .venv/bin/ruff check . && .venv/bin/ruff format --check .
451
+ .venv/bin/mypy src # strict mode
452
+ .venv/bin/pytest -q # no printer required — tests use in-process fakes
453
+ ```
454
+
455
+ Tests run against an in-process fake SDCP WebSocket server plus pure
456
+ translation-layer tests for CC2; nothing in CI touches real hardware.
457
+ Live verification against a physical printer is manual — `centauri
458
+ status`, `centauri canvas`, and a fan write are the standard smoke
459
+ test after protocol-layer changes.
460
+
461
+ ## Credits & license
462
+
463
+ - Protocol references: Elegoo's
464
+ [`elegoo-link`](https://github.com/ELEGOO-3D/elegoo-link) SDK
465
+ (Apache-2.0) and
466
+ [`CentauriLink`](https://github.com/CentauriLink/Centauri-Link).
467
+ - Licensed under Apache-2.0 — see [LICENSE](LICENSE).
468
+ - Not affiliated with or endorsed by Elegoo. Reverse-engineered
469
+ protocols can break with any firmware update; nothing here is
470
+ warranted to keep your prints alive.