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.
- {pycentauri-0.5.1 → pycentauri-0.6.0}/CHANGELOG.md +124 -0
- pycentauri-0.6.0/PKG-INFO +470 -0
- pycentauri-0.6.0/README.md +420 -0
- {pycentauri-0.5.1 → pycentauri-0.6.0}/pyproject.toml +3 -1
- {pycentauri-0.5.1 → pycentauri-0.6.0}/src/pycentauri/__init__.py +16 -2
- {pycentauri-0.5.1 → pycentauri-0.6.0}/src/pycentauri/camera.py +1 -0
- pycentauri-0.6.0/src/pycentauri/cc2.py +703 -0
- {pycentauri-0.5.1 → pycentauri-0.6.0}/src/pycentauri/cli.py +149 -31
- {pycentauri-0.5.1 → pycentauri-0.6.0}/src/pycentauri/client.py +58 -5
- pycentauri-0.6.0/src/pycentauri/connect.py +80 -0
- {pycentauri-0.5.1 → pycentauri-0.6.0}/src/pycentauri/mcp/server.py +51 -22
- {pycentauri-0.5.1 → pycentauri-0.6.0}/src/pycentauri/models.py +81 -0
- {pycentauri-0.5.1 → pycentauri-0.6.0}/src/pycentauri/server.py +153 -50
- {pycentauri-0.5.1 → pycentauri-0.6.0}/src/pycentauri/web/app.js +149 -7
- pycentauri-0.6.0/src/pycentauri/web/index.html +233 -0
- pycentauri-0.6.0/src/pycentauri/web/styles.css +637 -0
- pycentauri-0.6.0/tests/test_cc2_mapping.py +272 -0
- {pycentauri-0.5.1 → pycentauri-0.6.0}/tests/test_server.py +27 -0
- pycentauri-0.5.1/PKG-INFO +0 -290
- pycentauri-0.5.1/README.md +0 -242
- pycentauri-0.5.1/src/pycentauri/web/index.html +0 -310
- pycentauri-0.5.1/src/pycentauri/web/styles.css +0 -955
- pycentauri-0.5.1/tests/integration/__init__.py +0 -0
- {pycentauri-0.5.1 → pycentauri-0.6.0}/.gitignore +0 -0
- {pycentauri-0.5.1 → pycentauri-0.6.0}/LICENSE +0 -0
- {pycentauri-0.5.1 → pycentauri-0.6.0}/src/pycentauri/discovery.py +0 -0
- {pycentauri-0.5.1 → pycentauri-0.6.0}/src/pycentauri/mcp/__init__.py +0 -0
- {pycentauri-0.5.1 → pycentauri-0.6.0}/src/pycentauri/mcp/__main__.py +0 -0
- {pycentauri-0.5.1 → pycentauri-0.6.0}/src/pycentauri/py.typed +0 -0
- {pycentauri-0.5.1 → pycentauri-0.6.0}/src/pycentauri/rtsp.py +0 -0
- {pycentauri-0.5.1 → pycentauri-0.6.0}/src/pycentauri/sdcp.py +0 -0
- {pycentauri-0.5.1 → pycentauri-0.6.0}/src/pycentauri/web/__init__.py +0 -0
- {pycentauri-0.5.1 → pycentauri-0.6.0}/tests/__init__.py +0 -0
- {pycentauri-0.5.1 → pycentauri-0.6.0}/tests/test_client.py +0 -0
- {pycentauri-0.5.1 → pycentauri-0.6.0}/tests/test_discovery.py +0 -0
- {pycentauri-0.5.1 → pycentauri-0.6.0}/tests/test_rtsp.py +0 -0
- {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.
|