pycentauri 0.5.0__tar.gz → 0.5.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 (30) hide show
  1. {pycentauri-0.5.0 → pycentauri-0.5.1}/CHANGELOG.md +21 -0
  2. {pycentauri-0.5.0 → pycentauri-0.5.1}/PKG-INFO +25 -3
  3. {pycentauri-0.5.0 → pycentauri-0.5.1}/README.md +24 -2
  4. {pycentauri-0.5.0 → pycentauri-0.5.1}/pyproject.toml +1 -1
  5. {pycentauri-0.5.0 → pycentauri-0.5.1}/src/pycentauri/__init__.py +1 -1
  6. {pycentauri-0.5.0 → pycentauri-0.5.1}/src/pycentauri/cli.py +113 -0
  7. {pycentauri-0.5.0 → pycentauri-0.5.1}/src/pycentauri/mcp/server.py +47 -0
  8. {pycentauri-0.5.0 → pycentauri-0.5.1}/.gitignore +0 -0
  9. {pycentauri-0.5.0 → pycentauri-0.5.1}/LICENSE +0 -0
  10. {pycentauri-0.5.0 → pycentauri-0.5.1}/src/pycentauri/camera.py +0 -0
  11. {pycentauri-0.5.0 → pycentauri-0.5.1}/src/pycentauri/client.py +0 -0
  12. {pycentauri-0.5.0 → pycentauri-0.5.1}/src/pycentauri/discovery.py +0 -0
  13. {pycentauri-0.5.0 → pycentauri-0.5.1}/src/pycentauri/mcp/__init__.py +0 -0
  14. {pycentauri-0.5.0 → pycentauri-0.5.1}/src/pycentauri/mcp/__main__.py +0 -0
  15. {pycentauri-0.5.0 → pycentauri-0.5.1}/src/pycentauri/models.py +0 -0
  16. {pycentauri-0.5.0 → pycentauri-0.5.1}/src/pycentauri/py.typed +0 -0
  17. {pycentauri-0.5.0 → pycentauri-0.5.1}/src/pycentauri/rtsp.py +0 -0
  18. {pycentauri-0.5.0 → pycentauri-0.5.1}/src/pycentauri/sdcp.py +0 -0
  19. {pycentauri-0.5.0 → pycentauri-0.5.1}/src/pycentauri/server.py +0 -0
  20. {pycentauri-0.5.0 → pycentauri-0.5.1}/src/pycentauri/web/__init__.py +0 -0
  21. {pycentauri-0.5.0 → pycentauri-0.5.1}/src/pycentauri/web/app.js +0 -0
  22. {pycentauri-0.5.0 → pycentauri-0.5.1}/src/pycentauri/web/index.html +0 -0
  23. {pycentauri-0.5.0 → pycentauri-0.5.1}/src/pycentauri/web/styles.css +0 -0
  24. {pycentauri-0.5.0 → pycentauri-0.5.1}/tests/__init__.py +0 -0
  25. {pycentauri-0.5.0 → pycentauri-0.5.1}/tests/integration/__init__.py +0 -0
  26. {pycentauri-0.5.0 → pycentauri-0.5.1}/tests/test_client.py +0 -0
  27. {pycentauri-0.5.0 → pycentauri-0.5.1}/tests/test_discovery.py +0 -0
  28. {pycentauri-0.5.0 → pycentauri-0.5.1}/tests/test_rtsp.py +0 -0
  29. {pycentauri-0.5.0 → pycentauri-0.5.1}/tests/test_sdcp.py +0 -0
  30. {pycentauri-0.5.0 → pycentauri-0.5.1}/tests/test_server.py +0 -0
@@ -6,6 +6,27 @@ Changelog](https://keepachangelog.com/en/1.1.0/).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.5.1] - 2026-06-17
10
+
11
+ ### Added
12
+ - **CLI parity with the new live-adjust API.** Three new top-level
13
+ commands matching the library and HTTP surfaces shipped in 0.5.0:
14
+ - `centauri speed <silent|balanced|sport|ludicrous|50|100|130|160>`
15
+ - `centauri fan [--model N] [--aux N] [--chamber N]`
16
+ - `centauri temp [--nozzle N] [--bed N] [--chamber N]`
17
+ All require `--enable-control` and accept any subset of fan/heater
18
+ flags (omitted axes are left untouched).
19
+ - **MCP tool parity:** three new tools (only registered with
20
+ `--enable-control`) — `set_print_speed`, `set_fan_speed`,
21
+ `set_temperatures`. Same shapes as the HTTP endpoints; not marked
22
+ "DESTRUCTIVE" because live runtime adjustment is their entire
23
+ purpose.
24
+
25
+ ### Fixed
26
+ - 0.5.0 shipped the live-adjust API to the library, HTTP server, and
27
+ web UI but missed the CLI and MCP server. This release closes that
28
+ parity gap — every surface now exposes the same control set.
29
+
9
30
  ## [0.5.0] - 2026-06-17
10
31
 
11
32
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pycentauri
3
- Version: 0.5.0
3
+ Version: 0.5.1
4
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
5
  Project-URL: Homepage, https://github.com/bjan/pycentauri
6
6
  Project-URL: Repository, https://github.com/bjan/pycentauri
@@ -109,8 +109,17 @@ centauri print start cube.gcode --host 192.168.1.209 --enable-control
109
109
  centauri print pause --host 192.168.1.209 --enable-control
110
110
  centauri print resume --host 192.168.1.209 --enable-control
111
111
  centauri print stop --host 192.168.1.209 --enable-control
112
+
113
+ # Live adjust — speed mode, fans, heaters (only effective mid-print for speed)
114
+ centauri speed sport --host 192.168.1.209 --enable-control
115
+ centauri fan --model 100 --aux 60 --chamber 30 --host 192.168.1.209 --enable-control
116
+ centauri temp --nozzle 215 --bed 60 --host 192.168.1.209 --enable-control
112
117
  ```
113
118
 
119
+ The four speed modes (`silent`, `balanced`, `sport`, `ludicrous`) map to
120
+ the firmware's only-accepted `PrintSpeedPct` values (50, 100, 130, 160) —
121
+ arbitrary intermediate percentages are silently dropped by the firmware.
122
+
114
123
  The host can also come from the `PYCENTAURI_HOST` environment variable.
115
124
  If neither is set, every command auto-discovers via a 2.5 s UDP broadcast
116
125
  and bails out if it finds zero or more than one printer.
@@ -136,8 +145,11 @@ async def main():
136
145
  asyncio.run(main())
137
146
  ```
138
147
 
139
- Control actions (`start_print`, `pause`, `resume`, `stop`) require
140
- `Printer.connect(..., enable_control=True)`.
148
+ Control actions (`start_print`, `pause`, `resume`, `stop`,
149
+ `set_print_speed`, `set_fan_speed`, `set_temperatures`) require
150
+ `Printer.connect(..., enable_control=True)`. The mode-to-value map for
151
+ print speed is also exposed as `Printer.PRINT_SPEED_MODES` for callers
152
+ that want to render their own picker.
141
153
 
142
154
  ## Quick start — HTTP server
143
155
 
@@ -166,6 +178,13 @@ centauri server --host 192.168.1.209 --bind 0.0.0.0 --port 8787 \
166
178
  | `GET` | `/docs` / `/redoc` | Auto-generated OpenAPI docs |
167
179
  | `POST` | `/print/start` | Body: `{"filename": "cube.gcode"}`. Requires `--enable-control`. |
168
180
  | `POST` | `/print/{pause,resume,stop}` | Requires `--enable-control`. |
181
+ | `POST` | `/print/speed` | Body: `{"mode": "silent\|balanced\|sport\|ludicrous"}` (or `50\|100\|130\|160`). |
182
+ | `POST` | `/print/fan` | Body: `{"model": 50, "auxiliary": 30, "chamber": 0}` (any subset; 0..100). |
183
+ | `POST` | `/print/temperature` | Body: `{"nozzle": 215, "bed": 60, "chamber": 0}` (any subset; °C). |
184
+
185
+ The web UI's **ADJUST** panel exposes all three of the above as a
186
+ 4-button speed-mode selector and per-fan/heater rows with auto-hydration
187
+ from live status.
169
188
 
170
189
  The server holds a single long-lived WebSocket to the printer and reuses
171
190
  it for every request — no per-request reconnect, and it won't bump into
@@ -226,6 +245,9 @@ the host is pinned at spawn time. Tools exposed:
226
245
  | `pause_print` | only with `--enable-control` | Pauses the current print |
227
246
  | `resume_print` | only with `--enable-control` | Resumes a paused print |
228
247
  | `stop_print` | only with `--enable-control` | Stops the current print |
248
+ | `set_print_speed` | only with `--enable-control` | Sets speed mode (`silent`/`balanced`/`sport`/`ludicrous`) — only takes effect mid-print |
249
+ | `set_fan_speed` | only with `--enable-control` | Sets any subset of model/aux/chamber fan speeds (0..100%) |
250
+ | `set_temperatures` | only with `--enable-control` | Sets any subset of nozzle/bed/chamber heater targets (°C, 0 = off) |
229
251
 
230
252
  Control tools aren't just gated — they're not *registered* without the
231
253
  flag, so an LLM that wasn't given the `--enable-control` launch can't see
@@ -61,8 +61,17 @@ centauri print start cube.gcode --host 192.168.1.209 --enable-control
61
61
  centauri print pause --host 192.168.1.209 --enable-control
62
62
  centauri print resume --host 192.168.1.209 --enable-control
63
63
  centauri print stop --host 192.168.1.209 --enable-control
64
+
65
+ # Live adjust — speed mode, fans, heaters (only effective mid-print for speed)
66
+ centauri speed sport --host 192.168.1.209 --enable-control
67
+ centauri fan --model 100 --aux 60 --chamber 30 --host 192.168.1.209 --enable-control
68
+ centauri temp --nozzle 215 --bed 60 --host 192.168.1.209 --enable-control
64
69
  ```
65
70
 
71
+ The four speed modes (`silent`, `balanced`, `sport`, `ludicrous`) map to
72
+ the firmware's only-accepted `PrintSpeedPct` values (50, 100, 130, 160) —
73
+ arbitrary intermediate percentages are silently dropped by the firmware.
74
+
66
75
  The host can also come from the `PYCENTAURI_HOST` environment variable.
67
76
  If neither is set, every command auto-discovers via a 2.5 s UDP broadcast
68
77
  and bails out if it finds zero or more than one printer.
@@ -88,8 +97,11 @@ async def main():
88
97
  asyncio.run(main())
89
98
  ```
90
99
 
91
- Control actions (`start_print`, `pause`, `resume`, `stop`) require
92
- `Printer.connect(..., enable_control=True)`.
100
+ Control actions (`start_print`, `pause`, `resume`, `stop`,
101
+ `set_print_speed`, `set_fan_speed`, `set_temperatures`) require
102
+ `Printer.connect(..., enable_control=True)`. The mode-to-value map for
103
+ print speed is also exposed as `Printer.PRINT_SPEED_MODES` for callers
104
+ that want to render their own picker.
93
105
 
94
106
  ## Quick start — HTTP server
95
107
 
@@ -118,6 +130,13 @@ centauri server --host 192.168.1.209 --bind 0.0.0.0 --port 8787 \
118
130
  | `GET` | `/docs` / `/redoc` | Auto-generated OpenAPI docs |
119
131
  | `POST` | `/print/start` | Body: `{"filename": "cube.gcode"}`. Requires `--enable-control`. |
120
132
  | `POST` | `/print/{pause,resume,stop}` | Requires `--enable-control`. |
133
+ | `POST` | `/print/speed` | Body: `{"mode": "silent\|balanced\|sport\|ludicrous"}` (or `50\|100\|130\|160`). |
134
+ | `POST` | `/print/fan` | Body: `{"model": 50, "auxiliary": 30, "chamber": 0}` (any subset; 0..100). |
135
+ | `POST` | `/print/temperature` | Body: `{"nozzle": 215, "bed": 60, "chamber": 0}` (any subset; °C). |
136
+
137
+ The web UI's **ADJUST** panel exposes all three of the above as a
138
+ 4-button speed-mode selector and per-fan/heater rows with auto-hydration
139
+ from live status.
121
140
 
122
141
  The server holds a single long-lived WebSocket to the printer and reuses
123
142
  it for every request — no per-request reconnect, and it won't bump into
@@ -178,6 +197,9 @@ the host is pinned at spawn time. Tools exposed:
178
197
  | `pause_print` | only with `--enable-control` | Pauses the current print |
179
198
  | `resume_print` | only with `--enable-control` | Resumes a paused print |
180
199
  | `stop_print` | only with `--enable-control` | Stops the current print |
200
+ | `set_print_speed` | only with `--enable-control` | Sets speed mode (`silent`/`balanced`/`sport`/`ludicrous`) — only takes effect mid-print |
201
+ | `set_fan_speed` | only with `--enable-control` | Sets any subset of model/aux/chamber fan speeds (0..100%) |
202
+ | `set_temperatures` | only with `--enable-control` | Sets any subset of nozzle/bed/chamber heater targets (°C, 0 = off) |
181
203
 
182
204
  Control tools aren't just gated — they're not *registered* without the
183
205
  flag, so an LLM that wasn't given the `--enable-control` launch can't see
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "pycentauri"
7
- version = "0.5.0"
7
+ version = "0.5.1"
8
8
  description = "Local-network toolkit for Elegoo Centauri Carbon 3D printers: async Python client, CLI, MCP server, REST/SSE HTTP server, web UI, and RTSP bridge"
9
9
  readme = "README.md"
10
10
  license = "Apache-2.0"
@@ -25,4 +25,4 @@ __all__ = [
25
25
  "discover",
26
26
  ]
27
27
 
28
- __version__ = "0.5.0"
28
+ __version__ = "0.5.1"
@@ -323,6 +323,119 @@ def cmd_print_stop(host: HostOpt = None, enable_control: ControlOpt = False) ->
323
323
  _run(run())
324
324
 
325
325
 
326
+ @app.command("speed")
327
+ def cmd_speed(
328
+ mode: Annotated[
329
+ str,
330
+ typer.Argument(
331
+ help="Speed mode: silent | balanced | sport | ludicrous "
332
+ "(or the integer 50 | 100 | 130 | 160).",
333
+ ),
334
+ ],
335
+ host: HostOpt = None,
336
+ enable_control: ControlOpt = False,
337
+ ) -> None:
338
+ """Set the print-speed mode. Only effective while a print is running."""
339
+ if not enable_control:
340
+ _echo_err("Refusing to send a write action without --enable-control.")
341
+ raise typer.Exit(code=2)
342
+
343
+ parsed: str | int = int(mode) if mode.lstrip("-").isdigit() else mode
344
+
345
+ async def run() -> None:
346
+ h, mid = await _resolve_target(host)
347
+ async with await Printer.connect(h, enable_control=True, mainboard_id=mid) as printer:
348
+ try:
349
+ await printer.set_print_speed(parsed)
350
+ except ValueError as err:
351
+ _echo_err(str(err))
352
+ raise typer.Exit(code=2) from err
353
+ typer.echo(f"speed mode set: {mode}")
354
+
355
+ _run(run())
356
+
357
+
358
+ @app.command("fan")
359
+ def cmd_fan(
360
+ model: Annotated[
361
+ int | None,
362
+ typer.Option("--model", help="Model (part-cooling) fan 0..100%."),
363
+ ] = None,
364
+ auxiliary: Annotated[
365
+ int | None,
366
+ typer.Option("--aux", "--auxiliary", help="Auxiliary fan 0..100%."),
367
+ ] = None,
368
+ chamber: Annotated[
369
+ int | None,
370
+ typer.Option("--chamber", help="Chamber/box fan 0..100%."),
371
+ ] = None,
372
+ host: HostOpt = None,
373
+ enable_control: ControlOpt = False,
374
+ ) -> None:
375
+ """Set fan speeds. Pass any subset; omitted fans are left untouched."""
376
+ if not enable_control:
377
+ _echo_err("Refusing to send a write action without --enable-control.")
378
+ raise typer.Exit(code=2)
379
+ if model is None and auxiliary is None and chamber is None:
380
+ _echo_err("Specify at least one of --model, --aux, --chamber.")
381
+ raise typer.Exit(code=2)
382
+
383
+ async def run() -> None:
384
+ h, mid = await _resolve_target(host)
385
+ async with await Printer.connect(h, enable_control=True, mainboard_id=mid) as printer:
386
+ try:
387
+ await printer.set_fan_speed(model=model, auxiliary=auxiliary, chamber=chamber)
388
+ except ValueError as err:
389
+ _echo_err(str(err))
390
+ raise typer.Exit(code=2) from err
391
+ parts = [
392
+ f"{k}={v}%"
393
+ for k, v in (("model", model), ("aux", auxiliary), ("chamber", chamber))
394
+ if v is not None
395
+ ]
396
+ typer.echo("fans set: " + ", ".join(parts))
397
+
398
+ _run(run())
399
+
400
+
401
+ @app.command("temp")
402
+ def cmd_temp(
403
+ nozzle: Annotated[
404
+ float | None, typer.Option("--nozzle", help="Nozzle target °C (0 = off).")
405
+ ] = None,
406
+ bed: Annotated[float | None, typer.Option("--bed", help="Bed target °C (0 = off).")] = None,
407
+ chamber: Annotated[
408
+ float | None, typer.Option("--chamber", help="Chamber target °C (0 = off).")
409
+ ] = None,
410
+ host: HostOpt = None,
411
+ enable_control: ControlOpt = False,
412
+ ) -> None:
413
+ """Set heater target temperatures. Pass any subset."""
414
+ if not enable_control:
415
+ _echo_err("Refusing to send a write action without --enable-control.")
416
+ raise typer.Exit(code=2)
417
+ if nozzle is None and bed is None and chamber is None:
418
+ _echo_err("Specify at least one of --nozzle, --bed, --chamber.")
419
+ raise typer.Exit(code=2)
420
+
421
+ async def run() -> None:
422
+ h, mid = await _resolve_target(host)
423
+ async with await Printer.connect(h, enable_control=True, mainboard_id=mid) as printer:
424
+ try:
425
+ await printer.set_temperatures(nozzle=nozzle, bed=bed, chamber=chamber)
426
+ except ValueError as err:
427
+ _echo_err(str(err))
428
+ raise typer.Exit(code=2) from err
429
+ parts = [
430
+ f"{k}={v}°C"
431
+ for k, v in (("nozzle", nozzle), ("bed", bed), ("chamber", chamber))
432
+ if v is not None
433
+ ]
434
+ typer.echo("targets set: " + ", ".join(parts))
435
+
436
+ _run(run())
437
+
438
+
326
439
  @app.command("server")
327
440
  def cmd_server(
328
441
  host: HostOpt = None,
@@ -196,6 +196,53 @@ def build_server(*, enable_control: bool = False) -> FastMCP:
196
196
  result = await printer.stop()
197
197
  return {"ok": True, "response": result.inner}
198
198
 
199
+ @mcp.tool()
200
+ async def set_print_speed(mode: str | int) -> dict[str, Any]:
201
+ """Set the print-speed mode. Only effective while a print is running.
202
+
203
+ ``mode`` is one of ``"silent"``, ``"balanced"``, ``"sport"``,
204
+ ``"ludicrous"``, or the corresponding ``PrintSpeedPct`` value
205
+ (``50``, ``100``, ``130``, ``160``). Arbitrary intermediate
206
+ values are rejected by the firmware.
207
+ """
208
+ host, mid = await _resolve_target()
209
+ async with await Printer.connect(host, enable_control=True, mainboard_id=mid) as printer:
210
+ result = await printer.set_print_speed(mode)
211
+ return {"ok": True, "response": result.inner}
212
+
213
+ @mcp.tool()
214
+ async def set_fan_speed(
215
+ model: int | None = None,
216
+ auxiliary: int | None = None,
217
+ chamber: int | None = None,
218
+ ) -> dict[str, Any]:
219
+ """Set fan speeds (0..100% each). Pass any subset; omitted fans are untouched.
220
+
221
+ ``chamber`` is the chamber/box fan. At least one of the three must
222
+ be provided.
223
+ """
224
+ host, mid = await _resolve_target()
225
+ async with await Printer.connect(host, enable_control=True, mainboard_id=mid) as printer:
226
+ result = await printer.set_fan_speed(model=model, auxiliary=auxiliary, chamber=chamber)
227
+ return {"ok": True, "response": result.inner}
228
+
229
+ @mcp.tool()
230
+ async def set_temperatures(
231
+ nozzle: float | None = None,
232
+ bed: float | None = None,
233
+ chamber: float | None = None,
234
+ ) -> dict[str, Any]:
235
+ """Set heater target temperatures in °C. Pass any subset.
236
+
237
+ ``0`` turns the corresponding heater off. Safety caps applied:
238
+ nozzle 0..300, bed 0..110, chamber 0..60. Setting all heaters to
239
+ ``0`` mid-print effectively kills the print.
240
+ """
241
+ host, mid = await _resolve_target()
242
+ async with await Printer.connect(host, enable_control=True, mainboard_id=mid) as printer:
243
+ result = await printer.set_temperatures(nozzle=nozzle, bed=bed, chamber=chamber)
244
+ return {"ok": True, "response": result.inner}
245
+
199
246
  return mcp
200
247
 
201
248
 
File without changes
File without changes
File without changes