pycentauri 0.5.1__tar.gz → 0.6.1__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.1}/CHANGELOG.md +143 -0
  2. pycentauri-0.6.1/PKG-INFO +474 -0
  3. pycentauri-0.6.1/README.md +424 -0
  4. {pycentauri-0.5.1 → pycentauri-0.6.1}/pyproject.toml +3 -1
  5. {pycentauri-0.5.1 → pycentauri-0.6.1}/src/pycentauri/__init__.py +16 -2
  6. {pycentauri-0.5.1 → pycentauri-0.6.1}/src/pycentauri/camera.py +1 -0
  7. pycentauri-0.6.1/src/pycentauri/cc2.py +796 -0
  8. {pycentauri-0.5.1 → pycentauri-0.6.1}/src/pycentauri/cli.py +149 -31
  9. {pycentauri-0.5.1 → pycentauri-0.6.1}/src/pycentauri/client.py +58 -5
  10. pycentauri-0.6.1/src/pycentauri/connect.py +80 -0
  11. {pycentauri-0.5.1 → pycentauri-0.6.1}/src/pycentauri/mcp/server.py +51 -22
  12. {pycentauri-0.5.1 → pycentauri-0.6.1}/src/pycentauri/models.py +81 -0
  13. {pycentauri-0.5.1 → pycentauri-0.6.1}/src/pycentauri/server.py +158 -50
  14. {pycentauri-0.5.1 → pycentauri-0.6.1}/src/pycentauri/web/app.js +149 -7
  15. pycentauri-0.6.1/src/pycentauri/web/index.html +233 -0
  16. pycentauri-0.6.1/src/pycentauri/web/styles.css +637 -0
  17. pycentauri-0.6.1/tests/test_cc2_mapping.py +361 -0
  18. {pycentauri-0.5.1 → pycentauri-0.6.1}/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.1}/.gitignore +0 -0
  25. {pycentauri-0.5.1 → pycentauri-0.6.1}/LICENSE +0 -0
  26. {pycentauri-0.5.1 → pycentauri-0.6.1}/src/pycentauri/discovery.py +0 -0
  27. {pycentauri-0.5.1 → pycentauri-0.6.1}/src/pycentauri/mcp/__init__.py +0 -0
  28. {pycentauri-0.5.1 → pycentauri-0.6.1}/src/pycentauri/mcp/__main__.py +0 -0
  29. {pycentauri-0.5.1 → pycentauri-0.6.1}/src/pycentauri/py.typed +0 -0
  30. {pycentauri-0.5.1 → pycentauri-0.6.1}/src/pycentauri/rtsp.py +0 -0
  31. {pycentauri-0.5.1 → pycentauri-0.6.1}/src/pycentauri/sdcp.py +0 -0
  32. {pycentauri-0.5.1 → pycentauri-0.6.1}/src/pycentauri/web/__init__.py +0 -0
  33. {pycentauri-0.5.1 → pycentauri-0.6.1}/tests/__init__.py +0 -0
  34. {pycentauri-0.5.1 → pycentauri-0.6.1}/tests/test_client.py +0 -0
  35. {pycentauri-0.5.1 → pycentauri-0.6.1}/tests/test_discovery.py +0 -0
  36. {pycentauri-0.5.1 → pycentauri-0.6.1}/tests/test_rtsp.py +0 -0
  37. {pycentauri-0.5.1 → pycentauri-0.6.1}/tests/test_sdcp.py +0 -0
@@ -6,6 +6,149 @@ Changelog](https://keepachangelog.com/en/1.1.0/).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.6.1] - 2026-07-05
10
+
11
+ ### Fixed
12
+ - **CC2: the user's speed mode now survives Canvas filament switches.**
13
+ The firmware resets `speed_mode` to balanced on every mid-print
14
+ switch and leaves it there (verified 2026-07-05: mode stayed reset
15
+ after the switch completed). `CC2Printer` now snapshots the mode in
16
+ effect when a switch begins and re-applies it once printing resumes —
17
+ regardless of whether the mode was set via pycentauri or the
18
+ touchscreen. Requires `enable_control`; read-only sessions log the
19
+ reset instead. The restore baseline is debounced (a mode must hold
20
+ 15 s to count) because the firmware fires its reset several seconds
21
+ *before* the head parks — without the debounce, that transient
22
+ poisoned the snapshot. Verified across two hands-free switch cycles
23
+ with journal-logged restores ~3 s after each resume.
24
+ - `centauri server` now surfaces pycentauri's own log lines (speed-mode
25
+ restores, reconnects) in its output — previously only uvicorn's
26
+ loggers were configured and library INFO logs were invisible.
27
+
28
+ ## [0.6.0] - 2026-07-05
29
+
30
+ ### Added
31
+ - **Centauri Carbon 2 support.** The CC2 uses MQTT on port 1883
32
+ (instead of the CC1's WebSocket SDCP on 3030) with a JSON-RPC
33
+ envelope, X-Token auth, and a completely different command set
34
+ (1001/1002/1020–1031 vs 0/1/128–512). This release adds:
35
+ - `CC2Printer` — async MQTT client that subclasses `Printer`,
36
+ translating CC2 payloads into the same `Status` / `Attributes`
37
+ models so every surface (CLI, HTTP, MCP, web UI) works unchanged.
38
+ - `connect_auto(host, access_code=...)` — factory that detects the
39
+ model (probes `:1883` only — a CC1 answers with a harmless kernel
40
+ RST, and the CC1's real WebSocket connect doubles as its own probe)
41
+ and returns the right `Printer` subclass. Callers never need to
42
+ know which model they're talking to.
43
+ - `--access-code` / `PYCENTAURI_ACCESS_CODE` on every CLI command
44
+ and MCP launch (required for CC2, ignored for CC1).
45
+ - `paho-mqtt>=2.0` added to core dependencies.
46
+ - CC2 status exposes `gcode_move.speed` — live head speed as the
47
+ commanded speed of the current move, in mm/min (÷60 matches the
48
+ printer screen's mm/s readout)
49
+ and `speed_mode` (integer 0–3) — fields the CC1 lacks entirely. Both
50
+ are preserved in `Status.raw["_cc2"]` alongside `remaining_time_sec`,
51
+ `filament_detected`, `external_device`, and `exception_status`.
52
+ - CC2 methods 1028 (SET_TEMPERATURE), 1029 (SET_LIGHT), 1030
53
+ (SET_FAN_SPEED), 1031 (SET_PRINT_SPEED), 1023 (RESUME), and 1036
54
+ (PRINT_TASK_LIST) all confirmed working. 1042 (VIDEO_STREAM) and
55
+ 1044 (GET_FILE_LIST) are non-responsive on firmware 01.03.02.51.
56
+ - **Canvas multi-filament system integration (CC2 only):**
57
+ - `Printer.canvas_status()` — returns `CanvasStatus` with connected
58
+ units, tray list (filament name/type/color/brand/temp range/loaded
59
+ status), active tray, and auto-refill state. Method 2005.
60
+ - `Printer.set_auto_refill(enabled)` — toggles auto-refill. Method
61
+ 2004. Gated by `enable_control`.
62
+ - CLI: `centauri canvas` (read) + `centauri refill --on/--off`
63
+ (write, requires `--enable-control`).
64
+ - HTTP: `GET /canvas` (always) + `POST /canvas/refill` (control-gated).
65
+ - MCP: `get_canvas_status` (always) + `set_auto_refill` (control-gated).
66
+ - `CanvasStatus`, `CanvasUnit`, `CanvasTray` model classes exported
67
+ from the package for typed access.
68
+
69
+ ### Fixed (pre-release hardening — none of these shipped in an earlier release)
70
+ - **CC1 `status()`/`watch()` self-heal through a wedged push
71
+ scheduler.** Discovered live 2026-07-04 on V0.3.0-o: the firmware can
72
+ enter a state where `Cmd 512` subscribes are accepted but no status
73
+ frames are ever pushed, while the request path stays healthy — and a
74
+ reboot does not clear it (verified 2026-07-05). Both methods now fall
75
+ back to explicit `Cmd 0` status requests (documented to emit a
76
+ one-shot status frame) whenever a subscribe produces no push within
77
+ one period, instead of hanging forever. See `docs/PROTOCOL.md`
78
+ "Operational quirks".
79
+ - `CC2Printer.watch()` now polls method 1002 inline whenever pushes go
80
+ quiet instead of silently terminating after one push period — CC2
81
+ `centauri watch` and the SSE stream no longer die on an idle printer.
82
+ - MQTT responses are correlated by the client's exact `api_response`
83
+ topic and non-push method. Previously any client's response — or the
84
+ printer's own 6000/6008 broadcasts, which reuse small auto-increment
85
+ ids — could resolve a pending request future with the wrong payload
86
+ (including reporting success for a control command).
87
+ - Registration is re-published from `_on_connect`, so paho's automatic
88
+ MQTT reconnect re-registers with the printer and pushes survive a
89
+ printer reboot without restarting `centauri server`.
90
+ - All MQTT-callback work that touches futures, events, queues, or the
91
+ merged status dict is marshalled onto the asyncio loop (fixes the
92
+ cross-thread delta merge and late `set_result` races).
93
+ - `GET /stream` uses a bounded connect timeout (read stays unbounded
94
+ for MJPEG) instead of hanging forever against a wedged camera port.
95
+ - `GET /canvas` returns 504 for printer timeouts, reserving 501 for
96
+ "no Canvas on this printer"; the web UI now retries transient Canvas
97
+ failures instead of hiding the panel until a page reload.
98
+ - A wrong CC2 access code now raises a clear auth error (MQTT reason
99
+ code / HTTP 401 hint) instead of a generic timeout or raw traceback.
100
+ - `connect_auto()` and `centauri rtsp` no longer probe :3030 — the CC1
101
+ WebSocket connect doubles as the probe, avoiding the connect/close
102
+ churn that port is sensitive to. `centauri rtsp` and the HTTP server
103
+ both pick the right MJPEG port (CC1 :3031 / CC2 :8080) automatically,
104
+ including reconnects after starting with the printer offline.
105
+ - Unknown CC2 `machine_status` values map to sentinel code 99 instead
106
+ of leaking raw values into the CC1 status-code space; `PrintStatus`
107
+ gains constants for the CC2 codes 27/28/29.
108
+ - `CanvasStatus.from_payload` survives malformed/absent `canvas_info`
109
+ payloads; `CanvasUnit`/`CanvasTray` are exported from the package
110
+ root; `typing_extensions` declared as a direct dependency.
111
+ - **CC2 sessions send the app-level `{"type": "PING"}` keepalive every
112
+ 30 s.** The firmware expires a client's registration after several
113
+ quiet minutes and then silently stops answering that session's
114
+ requests — the MQTT connection stays up, responses just never come
115
+ (verified 2026-07-05: a dashboard went request-deaf after ~6 minutes
116
+ while a fresh session answered instantly). MQTT-level keepalive does
117
+ not prevent this; only the app-level PING does.
118
+ - `centauri server` bounds uvicorn's graceful shutdown at 5 s. Open
119
+ SSE/MJPEG streams never close on their own, and without the bound a
120
+ stopped server lingered forever as a zombie still holding a printer
121
+ connection (observed 2026-06-23 and 2026-07-05).
122
+ - CC2 lifecycle commands (start/pause/stop/resume) use a 90 s timeout —
123
+ the firmware answers only after the mechanical sequence completes
124
+ (observed 2026-07-05: a resume succeeded but its confirmation
125
+ arrived after the old 15 s window, surfacing a false error). When a
126
+ confirmation still doesn't arrive, `POST /print/*` returns 504
127
+ ("sent, unconfirmed — check printer status") instead of 502, and the
128
+ web UI shows it as a warning rather than a failure.
129
+
130
+ ### Tests
131
+ - New `tests/test_cc2_mapping.py`: pure dict-in/dict-out coverage of
132
+ the CC2→CC1 translation layer — machine_status/sub_status mapping
133
+ (including purge-zone filament-switch detection and its progress
134
+ guard), partial-delta deep merge, full 1002 payload round-trip, and
135
+ Canvas parsing. Plus a server test pinning `GET /canvas` → 501 on
136
+ CC1.
137
+
138
+ ### Documentation
139
+ - `docs/PROTOCOL.md` gains a full CC2 section: MQTT transport, topic
140
+ structure, connection/registration dance, the confirmed method table
141
+ (12 probed, 10 working), error codes, the complete status payload,
142
+ and a CC1-vs-CC2 comparison table. Probed on 2026-06-30 through
143
+ 2026-07-04 against firmware 01.03.02.51 (Canvas methods 2004/2005
144
+ verified live with a 4-slot Canvas attached; CC2 webcam confirmed
145
+ unauthenticated MJPEG on :8080).
146
+ - `docs/ARCHITECTURE.md` rewritten for the two-transport reality:
147
+ module map with `cc2`/`connect`, per-model ports, CC1-vs-CC2
148
+ `/status` and SSE flow differences, and an honest test-architecture
149
+ section (no automated live suite exists; live verification is manual
150
+ CLI runs).
151
+
9
152
  ## [0.5.1] - 2026-06-17
10
153
 
11
154
  ### Added
@@ -0,0 +1,474 @@
1
+ Metadata-Version: 2.4
2
+ Name: pycentauri
3
+ Version: 0.6.1
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
+ - **Canvas filament switches reset the speed mode.** Every mid-print
411
+ switch silently drops the printer back to balanced. pycentauri
412
+ detects the switch ending and re-applies whatever mode was active
413
+ before it (needs `--enable-control`).
414
+ - **Registrations expire without an app-level PING.** The printer
415
+ forgets a registered client after several quiet minutes and silently
416
+ stops answering that session's requests — the MQTT connection itself
417
+ stays up, so there's no error to catch. pycentauri sends the SDK's
418
+ `{"type": "PING"}` keepalive every 30 s to hold the registration; if
419
+ you write your own client, you must too.
420
+ - **File list (method 1044) and video stream (1042) don't respond** on
421
+ firmware 01.03.02.51, so remote print-start on CC2 requires knowing
422
+ the filename in advance.
423
+
424
+ ## Project layout & docs
425
+
426
+ ```
427
+ src/pycentauri/
428
+ ├── client.py # CC1: async SDCP-over-WebSocket client
429
+ ├── cc2.py # CC2: async JSON-RPC-over-MQTT client (same API)
430
+ ├── connect.py # connect_auto() — port-probe model detection
431
+ ├── sdcp.py # SDCP v3 envelope build/parse
432
+ ├── discovery.py # UDP broadcast discovery
433
+ ├── camera.py # MJPEG frame grabber
434
+ ├── models.py # Status / Attributes / CanvasStatus / PrintInfo
435
+ ├── cli.py # Typer CLI
436
+ ├── server.py # FastAPI app + connection supervisor
437
+ ├── rtsp.py # MediaMTX/ffmpeg bridge
438
+ ├── mcp/ # FastMCP stdio server
439
+ └── web/ # Static dashboard (no build step, no CDN)
440
+ ```
441
+
442
+ - [`docs/PROTOCOL.md`](docs/PROTOCOL.md) — both wire protocols in
443
+ detail: envelopes, command/method tables with tested-on dates, status
444
+ payloads, error codes, failure modes, and a CC1-vs-CC2 comparison.
445
+ - [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) — module map and
446
+ request flow.
447
+
448
+ ## Development
449
+
450
+ ```sh
451
+ git clone https://github.com/bjan/pycentauri && cd pycentauri
452
+ python -m venv .venv && .venv/bin/pip install -e ".[mcp,server,dev]"
453
+
454
+ .venv/bin/ruff check . && .venv/bin/ruff format --check .
455
+ .venv/bin/mypy src # strict mode
456
+ .venv/bin/pytest -q # no printer required — tests use in-process fakes
457
+ ```
458
+
459
+ Tests run against an in-process fake SDCP WebSocket server plus pure
460
+ translation-layer tests for CC2; nothing in CI touches real hardware.
461
+ Live verification against a physical printer is manual — `centauri
462
+ status`, `centauri canvas`, and a fan write are the standard smoke
463
+ test after protocol-layer changes.
464
+
465
+ ## Credits & license
466
+
467
+ - Protocol references: Elegoo's
468
+ [`elegoo-link`](https://github.com/ELEGOO-3D/elegoo-link) SDK
469
+ (Apache-2.0) and
470
+ [`CentauriLink`](https://github.com/CentauriLink/Centauri-Link).
471
+ - Licensed under Apache-2.0 — see [LICENSE](LICENSE).
472
+ - Not affiliated with or endorsed by Elegoo. Reverse-engineered
473
+ protocols can break with any firmware update; nothing here is
474
+ warranted to keep your prints alive.