Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
49d3fae
feat(slicing): thumbnails are added after every slice, in the slicer …
alex-mextner Nov 4, 2026
9e543ad
feat(printer): upload and start add missing thumbnails; print-preflig…
alex-mextner Nov 4, 2026
5a639fc
feat(bed): `3d printer bed check`, `reference --best-view`, and a ver…
alex-mextner Nov 4, 2026
ff6088f
feat(printer): `3d printer watch NAME --job` aborts a print whose par…
alex-mextner Nov 4, 2026
bf0da29
feat(jobwatch): follow the plate on a printer without a camera calibr…
alex-mextner Nov 4, 2026
630ce51
feat(bed): the printer's light for camera frames: detection, session,…
alex-mextner Nov 4, 2026
9d42cd3
feat(bed): every camera frame of the bed check, reference capture and…
alex-mextner Nov 4, 2026
46fbccc
feat(preflight): the `bed` line runs the full bed check and FAILs onl…
alex-mextner Nov 4, 2026
620b9d6
feat(bed): lighting-normalised verdict — an empty bed in another ligh…
alex-mextner Nov 5, 2026
9bdeefb
feat(print-preflight): slot colour by CIEDE2000 ΔE, --fix rewrites th…
alex-mextner Nov 5, 2026
d657c9d
fix(printer): retry a start --force approval Telegram could not post,…
alex-mextner Nov 5, 2026
48ad3ca
feat(printer): start --fix-colour and `printer slot set|show` align f…
alex-mextner Nov 5, 2026
612148c
test(printer): e2e for a start --force approval that Telegram could n…
alex-mextner Nov 5, 2026
79f4c90
feat(bed): references per plate type and light state with an age, `be…
alex-mextner Nov 5, 2026
36f710b
feat(bed): `3d printer bed calibrate NAME` learns the plate's visible…
alex-mextner Nov 5, 2026
5ccdf51
feat(bed): `bed check --assume-empty "why"` — the owner's auditable w…
alex-mextner Nov 5, 2026
9c69300
refactor(vision): split the over-complex functions of vision_detect.p…
alex-mextner Nov 5, 2026
403be12
feat(printer): print-quality measures and judge — stringing, pattern …
alex-mextner Nov 5, 2026
b39c2d6
feat(printer): job watch runs the print-quality tier and writes the m…
alex-mextner Nov 5, 2026
f77bb3f
feat(vision): printer vision runs the print-quality tier (Refs #733)
alex-mextner Nov 5, 2026
ea27db3
fix(printer): the first-layer floor is on only for a calibrated camer…
alex-mextner Nov 5, 2026
d35e2cf
fix(printer): per-model quality defaults come from a data file, not a…
alex-mextner Nov 5, 2026
47f2d61
feat(printer): checked macro rules and Moonraker macro discovery
alex-mextner Nov 4, 2026
bbfc609
feat(printer): per-adapter, per-model, per-printer alias table
alex-mextner Nov 4, 2026
a343c21
feat(printer): `3d printer macro list|run`, `aliases` and the per-mod…
alex-mextner Nov 4, 2026
3402218
fix(printer): the U1 has no quad_gantry_level / z_tilt: qgl and z-til…
alex-mextner Nov 4, 2026
a1d0e98
refactor(printer): split list/start/run of lib/commands/printer.py in…
alex-mextner Nov 5, 2026
bac07c1
merge: origin/main into feat/gcode-thumbnails-everywhere (Refs #722)
alex-mextner Nov 5, 2026
dcb63fa
merge: feat/gcode-thumbnails-everywhere into feat/slot-colour-fix; on…
alex-mextner Nov 5, 2026
4f501dd
test(thumbnails): user-workflow docstrings for the three e2e stories …
alex-mextner Nov 5, 2026
d34af67
Merge branch 'feat/gcode-thumbnails-everywhere' into feat/slot-colour…
alex-mextner Nov 5, 2026
659302f
test(printer): user-workflow docstrings for two colour e2e stories (R…
alex-mextner Nov 5, 2026
c7ac1ff
merge: feat/slot-colour-fix (with thumbnails) into feat/bed-watch-too…
alex-mextner Nov 5, 2026
4d53168
Merge branch 'feat/slot-colour-fix' into feat/bed-watch-tool
alex-mextner Nov 5, 2026
8aec241
Merge branch 'feat/bed-watch-tool' into feat/print-quality-monitor
alex-mextner Nov 5, 2026
f258b03
merge: print-quality monitor, bed watch, slot colour and thumbnails i…
alex-mextner Nov 5, 2026
652af64
merge: origin/main (squashed #725) into feat/bed-watch-tool; tree unc…
alex-mextner Nov 5, 2026
b26a036
merge: feat/bed-watch-tool into feat/print-quality-monitor (Refs #733)
alex-mextner Nov 5, 2026
7053b47
merge: feat/print-quality-monitor into feature/printer-macros-aliases…
alex-mextner Nov 5, 2026
62bbdac
merge: origin/main (squashed #734) into feature/printer-macros-aliase…
alex-mextner Nov 5, 2026
282392c
fix(test): printops maint e2e used fixed dates that aged 'ok' into 's…
alex-mextner Nov 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 95 additions & 0 deletions docs/commands/printer.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ Epic: [#90](https://github.com/alex-mextner/3d-cli/issues/90).
3d printer todo dismiss ID --reason TEXT | snooze ID [--until DATE] [--reason TEXT] | reopen ID
3d printer todo import [FILE...] [--dry-run] [--json]
3d printer -n PRINTER issues|todo list|add|done|dismiss|snooze|reopen|import …
3d printer -n PRINTER macro list [--offline | --dry-run] [--all] [--json]
3d printer -n PRINTER macro run MACRO [KEY=VALUE ...] [--yes] [--dry-run] [--offline] [--timeout S]
3d printer -n PRINTER aliases [--json]
3d printer -n PRINTER ALIAS [args] [--yes] [--dry-run] [--offline] [--timeout S] # home, level, load, preheat, ...
```

| Subcommand | What |
Expand All @@ -60,6 +64,8 @@ Epic: [#90](https://github.com/alex-mextner/3d-cli/issues/90).
| `subscribe` / `unsubscribe` / `subscriptions` | Route printer alerts to an AI agent first: it looks at the frame and tells the user only if the failure is real — see [printer-subscribe.md](printer-subscribe.md). |
| `agent list` / `stop` / `handoff` | The temporary per-printer omp agent (tmux) that gets the warning+ alerts no live subscriber took — see [printer-subscribe.md#the-temporary-printer-agent](printer-subscribe.md#the-temporary-printer-agent). |
| `doctor [NAME\|--all]` | Diagnose the printer's setup (Klippy, Moonraker, Spoolman link and CORS, active spool, camera, watch services, ...) and with `--fix` repair what is safe — the same command as `3d printers doctor`, see [printers-doctor.md](printers-doctor.md). |
| `macro list\|run` | The macros a Klipper printer defines, and ONE macro run with typed `KEY=VALUE` parameters — see [Macros and aliases](#macros-and-aliases-3d-printer-n-name-macro-home-load-). |
| `aliases`, `home`, `level`, `load`, `preheat`, ... | Short names for the usual operations, mapped per adapter, model and printer to the real macros — see [Macros and aliases](#macros-and-aliases-3d-printer-n-name-macro-home-load-). |
| `todo …` (alias `issues`) | The to-do list of each printer: needed calibrations, due maintenance, repairs of open faults, manual tasks — see [To-do list](#to-do-list-3d-printer-todo). A printer's problems are tasks of this list, not GitHub tickets. |

Options:
Expand Down Expand Up @@ -393,6 +399,95 @@ Telegram approval). With `--printer A --axis-limits B` only A's list counts.
print). The preflight overhangs gate counts uncarried runs up to it as bridges; `--bridge-max`
overrides it, and without either the limit is 15 mm. The gate's line names where the limit came from.

## Macros and aliases (`3d printer -n NAME macro`, `home`, `load`, ...)

Issue [#718](https://github.com/alex-mextner/3d-cli/issues/718). Two ways to run what the printer's own
firmware knows, both through the adapter's G-code channel (`moonraker`: `POST /printer/gcode/script`).
The `creality-stock` adapter has no G-code channel (ws:9999 only starts and stops a file): every command
below refuses it with "does not support macros and aliases". The printer is named with `-n NAME`, like `todo`.

### `macro list` and `macro run`

```
3d printer -n u1 macro list # what the printer defines; * = in its trusted_macros
3d printer -n u1 macro list --offline --json # registry data only (trusted_macros + alias targets); nothing contacted
3d printer -n u1 macro list --dry-run # print the two GET requests, send none
3d printer -n u1 macro run SM_PRINT_AUTO_FEED EXTRUDER=1 --yes --dry-run
3d printer -n k1se macro run PAUSE
```

`macro list` reads `GET /printer/objects/list` (the `gcode_macro NAME` objects) and `GET /printer/gcode/help`
(every extended command with its description); `_private` macros need `--all`. `macro run` prints the exact
line that is sent (`gcode:` block) and the request on `--dry-run`. Gates, in this order, nothing is sent before
all passed:

1. **Typed tokens.** The name is an identifier (`[A-Za-z_][A-Za-z0-9_]*`, upper-cased); each value is one token of
`A-Za-z0-9_.,:+/-` (at most 64 characters). A newline, `;`, space, quote, `{`, `%`, `#` or `*` is refused as an
invalid argument, so a value can never append a second command.
2. **No emergency stop.** `M112` and `EMERGENCY_STOP`, as a name or inside a value, are refused whatever the flags:
the printer's own stop control is the way to stop it.
3. **Access.** Only `access: full` printers; `read-only` and `offline` (never contacted) refuse, also with `--dry-run`.
4. **Defined on the printer.** The name must appear in the printer's `gcode/help`. A standard code (`G28`, `M104`, `T0`)
is run only if the printer defines a macro of that name (the K1's `G29`) or through an alias.
5. **`--yes`** for a macro outside the printer's `trusted_macros` and for dangerous commands (`FIRMWARE_RESTART`, `RESTART`,
`SAVE_CONFIG`, `FORCE_MOVE`, `M84`, calibrations that write config, ...), trusted or not.
6. **An idle printer** (state re-read now; printing, paused, busy and unreadable all refuse) for motion, heating and
unknown macros. `PAUSE`, `RESUME`, `CANCEL_PRINT`, `M600`, `M220`, `M221` and the `SET_PAUSE_*` pair are allowed during a job.

A macro body is the printer's own config and is not checked, so any macro that is not known to be harmless is treated
as motion. `--offline` prints the script from registry data without contacting the printer (it cannot check 4 and 6).

**There is no raw `gcode` passthrough.** `3d printer gcode SCRIPT` does not exist: `/printer/gcode/script` runs any text, newlines
and `;` included, the model of this tool is that an agent never sends a free-text line to hardware, and a stock G-code
(`G1`, `M104`) cannot be tied to a trusted list. A command the table lacks is added as a named, typed alias in the
printer's own alias file (below) or run as a macro the printer defines.

### Aliases

`3d printer -n NAME ALIAS [args] [--yes] [--dry-run] [--offline]`. An alias is a table entry (`kind`, a `summary`, typed
`args`, `gcode` lines); its lines are vetted, so only `dangerous` ones and entries marked `yes: true` ask for `--yes`; the
idle-printer rule (6) and the definition check (4) still apply. The table is
[`lib/data/printer-aliases.yaml`](../../lib/data/printer-aliases.yaml) (schema in its header), layered per adapter, then
every model whose name contains the entry's `contains` text, then the printer's own entries in
`~/.config/3d-cli/printer-aliases.yaml` and `./printer-aliases.yaml` (the project file wins per alias):

```yaml
# ~/.config/3d-cli/printer-aliases.yaml or ./printer-aliases.yaml: printer name -> its aliases
u1:
home: {summary: home X and Y only, kind: motion, gcode: ["G28 X Y"]} # replaces the table's home
nozzle-wipe: {summary: wipe the nozzle, kind: motion, gcode: [ROUGHLY_CLEAN_NOZZLE]} # a new alias
park: null # removed for this printer
cooldown: {unsupported: "close the chamber door first"} # removed, with the reason shown
```

An alias that a model does not have says so (`alias park is not defined for k1se ...`, with the aliases it does have);
it is never guessed. `3d printer -n NAME aliases` lists what each alias sends on that printer and which are unsupported;
`3d printer ALIAS --help` shows one.

| Alias | Sends | Where |
|---|---|---|
| `home [XYZ]` | `G28` / `G28 X Y` | every `moonraker` printer |
| `level` | `BED_MESH_CALIBRATE`; Creality K1: the stock `G29` (heats, probes) | `moonraker`; K1 family |
| `qgl`, `z-tilt` | `QUAD_GANTRY_LEVEL`, `Z_TILT_ADJUST` | `moonraker`; refused with a reason on the U1 and the K1 SE (their stock `printer.cfg` define neither) and, on any printer, when the firmware does not define the command |
| `pause`, `resume` | `PAUSE`, `RESUME` (allowed during a job) | `moonraker` |
| `preheat MATERIAL` / `--nozzle C` / `--bed C` / `--head T2` | `SET_HEATER_TEMPERATURE` for the nozzle of the head (fleet `heads:`, default T1) and `heater_bed`; PLA 210/60, PETG 235/80, ABS 250/100, ASA 255/100, TPU 225/50 (nozzle/bed, typical values), nozzle 0-300, bed 0-120 | `moonraker` |
| `cooldown` | `TURN_OFF_HEATERS` | `moonraker` |
| `load`, `unload` | generic: `LOAD_FILAMENT`, `UNLOAD_FILAMENT` (Fluidd/Mainsail convention, refused when the printer has no such macro); K1: `LOAD_MATERIAL`, `QUIT_MATERIAL`; U1: `load T1..T4` = `AUTO_FEEDING EXTRUDER=n LOAD=1`, `unload T1..T4` = `AUTO_FEEDING EXTRUDER=n UNLOAD=1 STAGE=prepare` then `STAGE=doing` | per adapter/model |
| `level-heated [--bed C]` | `AUTO_BED_MESH_CALIBRATE TEMP=C` (heats the bed to 55 by default, soaks 2 min, meshes) | Snapmaker U1 |
| `clean-nozzle` | `ROUGHLY_CLEAN_NOZZLE` | Snapmaker U1 |
| `park [T1..T4]`, `idle-position [T1..T4]` | `PARK_EXTRUDER[n]`, `MOVE_TO_XY_IDLE_POSITION_EXTRUDER[n]` | Snapmaker U1 |
| `purifier idle\|cool\|preheat\|hot` | `SET_PURIFIER_MODE MODE=0..3` | Snapmaker U1 |

U1 facts come from the firmware config of [Snapmaker/u1-klipper](https://github.com/Snapmaker/u1-klipper)
(`lava/fluidd.cfg`, `lava/printer.cfg`, `klippy/extras/filament_feed.py`, `purifier.py`, `kinematics/extruder.py`); the
K1 SE ones from [CrealityOfficial/K1_Series_Klipper](https://github.com/CrealityOfficial/K1_Series_Klipper)
`config/K1_SE_CR4CU220812S12/gcode_macro.cfg`. None of them was run on a real printer by the author: the tests use
the loopback fake Moonraker only. Head arguments are the fleet labels (`T1`..), never a bare number, and an unusable
head (fleet `usable: false`) is refused with its reason.

Models without an adapter here (Bambu Lab, Prusa) have no aliases: they are `offline` in the fleet and every command above
refuses them; a Bambu/Prusa adapter would bring its own `adapters:` layer in the table.

## Adapters

| Adapter | Printers | Transport |
Expand Down
75 changes: 67 additions & 8 deletions lib/commands/printer.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""3d printer — drive configured physical printers: list, status, upload, start, cancel, watch, vision, bed, subscribe, agent, todo.
"""3d printer — drive configured physical printers: list, status, upload, start, slot, cancel, macro, aliases, watch, vision, bed, subscribe, agent, todo.

WHAT: talks to the printers named in printer-devices.yaml through a protocol adapter —
`moonraker` (Klipper/Moonraker, Snapmaker U1) or `creality-stock` (Creality K1 family on
Expand All @@ -20,6 +20,9 @@
never reaches a printer screen without its preview;
- a failed/timed-out start is reported as UNKNOWN and never resent automatically.

MACROS AND ALIASES (`-n NAME macro list|run`, `aliases`, `home`, `load`, ...): printer_ops/macro_cli.py and alias_cli.py;
a macro is a checked passthrough, an alias a per-adapter/model/printer table entry (issue #718).

INVARIANTS: stdlib-only at import time (registry contract); printer_ops is imported lazily.
"""
from __future__ import annotations
Expand Down Expand Up @@ -137,8 +140,35 @@
maintenance, repairs, manual tasks (3d-api; offline a
JSONL buffer — see `3d printer todo --help`). A printer's
problems are tasks of this list, not GitHub tickets.
-n, --name NAME the printer for `todo`/`issues`:
`3d printer -n u1 issues list`
macro list|run MACRO [KEY=VALUE ...] [--yes] [--dry-run] [--offline]
the macros the printer defines / run ONE macro (Klipper: any
gcode_macro or extended command) with typed KEY=VALUE params;
the exact line sent is printed. Names are identifiers, values
one safe token; M112/EMERGENCY_STOP never; read-only/offline
printers refuse; a macro outside the printer's trusted_macros
and dangerous ones need --yes; motion/heating ones need an
idle printer (PAUSE/RESUME/CANCEL_PRINT style do not).
There is no raw `gcode` passthrough (see docs/commands/printer.md)
(see `3d printer macro --help`)
aliases [--json] what each alias below sends on this printer (-n NAME) or per
adapter/model; `3d printer ALIAS --help` for one
-n, --name NAME the printer for `todo`/`issues`, `macro`, `aliases` and the aliases:
`3d printer -n u1 issues list`, `3d printer -n u1 home`
(own aliases per printer: ~/.config/3d-cli/printer-aliases.yaml, ./printer-aliases.yaml)

Aliases (thin subcommands, need -n NAME; each maps per adapter/model to real macros and says so
when a model has none: `3d printer -n NAME ALIAS [args] [--yes] [--dry-run] [--offline]`):
home [XYZ] home all axes, or only X/Y/Z (G28)
level bed mesh (BED_MESH_CALIBRATE; Creality K1: stock G29)
qgl | z-tilt quad gantry level / Z tilt adjust (printers that define them)
pause | resume PAUSE / RESUME (allowed while a job runs)
preheat MATERIAL | --nozzle C --bed C [--head T2]
PLA|PETG|ABS|ASA|TPU typical temperatures (SET_HEATER_TEMPERATURE)
cooldown TURN_OFF_HEATERS
load T1..T4 | unload T1..T4 filament in/out: U1 AUTO_FEEDING per head; K1 LOAD_MATERIAL /
QUIT_MATERIAL (no head); other Klipper LOAD_FILAMENT / UNLOAD_FILAMENT
Snapmaker U1 only: level-heated [--bed C], clean-nozzle, park [T1..T4], idle-position [T1..T4],
purifier idle|cool|preheat|hot

Options:
--json list/status as JSON (status: normalized state + raw firmware payload)
Expand Down Expand Up @@ -203,6 +233,11 @@
3d printer todo list u1
3d printer -n k1se issues add --title "X belt squeaks" --type inspect"""

# Subcommands that take the printer as a positional NAME: `-n` is refused for them.
_NAME_AS_POSITIONAL = frozenset({
"list", "status", "upload", "start", "slot", "cancel", "evidence", "watch", "supervise", "vision", "guard", "bed",
"subscribe", "unsubscribe", "subscriptions", "agent", "doctor",
})
_VALUE = {"--secs", "--as", "--source-3mf", "--bridge-max", "--colour-delta-e", "--fix-max-delta-e"}
_FLAGS = {"--json", "--yes", "--force", "--no-bed-move", "--fix-colour", "--watch"}

Expand Down Expand Up @@ -744,6 +779,30 @@ def _run_todo(rest: list[str], name: str | None) -> int:
return todo(rest, printer=name)


def _run_macro(rest: list[str], name: str | None) -> int:
from printer_ops.macro_cli import run_macro

return run_macro(name, rest)


def _run_aliases(rest: list[str], name: str | None) -> int:
from printer_ops.alias_cli import run_aliases

return run_aliases(name, rest)


def _run_alias(sub: str, rest: list[str], name: str | None) -> int:
"""A subcommand that is none of the above: an alias of printer NAME (home, load, ...), else unknown."""
from printer_ops.alias_cli import run_alias

aliased = run_alias(name, sub, rest)
if aliased is not None:
return aliased
raise UsageError(f"unknown subcommand '{sub}'", command="printer",
remediation=["Use list, status, upload, start, slot, cancel, evidence, watch, supervise, vision, guard, bed, subscribe, unsubscribe, "
"subscriptions, agent, doctor, todo (issues), macro, aliases or an alias (home, level, load, ...) — see `3d printer --help`."])


def _run_subscriptions(sub: str) -> Callable[[list[str], str | None], int]:
"""Handler for subscribe/unsubscribe/subscriptions: the module takes the subcommand word too."""
def handler(rest: list[str], name: str | None) -> int:
Expand Down Expand Up @@ -774,6 +833,8 @@ def handler(rest: list[str], name: str | None) -> int:
"doctor": _run_doctor,
"todo": _run_todo,
"issues": _run_todo,
"macro": _run_macro,
"aliases": _run_aliases,
}


Expand All @@ -789,14 +850,12 @@ def run(argv: list[str]) -> int:
if sub in ("-h", "--help", "help"):
print(USAGE)
return 0
if name is not None and sub not in ("todo", "issues"):
raise UsageError(f"-n/--name selects the printer for `todo`/`issues`; `{sub}` takes the printer as NAME",
if name is not None and sub in _NAME_AS_POSITIONAL:
raise UsageError(f"-n/--name selects the printer for `todo`/`issues`, `macro`, `aliases` and the aliases; `{sub}` takes the printer as NAME",
command="printer", remediation=[f"3d printer {sub} {name} …"])
handler = _SUBCOMMANDS.get(sub)
if handler is None:
raise UsageError(f"unknown subcommand '{sub}'", command="printer",
remediation=["Use list, status, upload, start, slot, cancel, evidence, watch, supervise, vision, guard, bed, subscribe, unsubscribe, "
"subscriptions, agent, doctor or todo (issues) — see `3d printer --help`."])
return _run_alias(sub, rest, name)
return handler(rest, name)


Expand Down
Loading
Loading