Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
27 changes: 27 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,33 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); version

## [Unreleased]

### Fixed

- **First-run honesty for people, not agents.** Bare `plate` outside a terminal
(a pipe, CI, `plate | less`) now prints a short four-step first-run guide —
install OrcaSlicer (a second slicer, not Bambu Studio), turn on LAN mode and
read the IP / serial / LAN access code off the touchscreen (not the account
password; it rotates), `plate setup`, `plate go` — instead of the full
argparse dump. It still exits `5`, keeps stdout empty, and never launches the
wizard or the TUI. The README and user guide lead with that same path;
`plate job <url> --confirm` moves to the scripts-and-agents section.
- `--confirm` copy now says what happens without it, per command: `job` /
`send` still download, slice, and **upload** (exit `0`,
`"uploaded_not_printed"`); only `print` / `stop` / `pause` / `resume` /
`gcode` / `delete` refuse. `plate go` help no longer claims "no slicer" — it
still runs OrcaSlicer, you just never learn its flags. `--sim` help says it
is a fake printer (no hardware, no config, not a protocol test). The `go` /
`tui` non-TTY refusal points scripts at `plate job <url> --json` and says
that `--confirm` is what starts a print.
- Expected failures were logged twice — once by the failing step and again by
the CLI's error handler. They now print once. The missing-OrcaSlicer error no
longer adds a contradictory "update config.json / tools/" hint when nothing
is installed, and the "HTML page did not contain a direct model file link"
error names what to use instead (a direct `.stl` / `.3mf` URL, a Printables
model page, or a local file) — previously that hint was unreachable code.
`plate doctor` with no printer configured now points at `plate preflight`
and `plate --sim status`.

## [0.5.0] - 2026-08-26

### Added
Expand Down
35 changes: 17 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,18 @@
<img alt="platecli demo: live printer status and slicing from the terminal" src="https://raw.githubusercontent.com/DLANSAMA/platecli/main/docs/demo-dark.gif">
</picture>

Paste a Printables link, get a physical print. `plate` downloads the model, slices it with OrcaSlicer, and sends it to your Bambu Lab printer — one command, entirely on your local network. No cloud account, no telemetry. Runs on **Linux, macOS, and Windows**, driven by hand or by AI agents.
Paste a Printables link, get a physical print. `plate` downloads the model, slices it with OrcaSlicer, and sends it to your Bambu Lab printer — one guided command, entirely on your local network. No cloud account, no telemetry. Runs on **Linux, macOS, and Windows**, driven by hand or by AI agents.

```text
model URL or file → download → slice (OrcaSlicer) → upload → print
one command: plate job <url> --confirm
one command: plate go
```

**Supports:** any Bambu Lab printer with LAN mode — P1P, P1S, X1C, X1E, A1, A1 Mini. **Hardware-tested on the P1 series (P1P/P1S) only.** The rest speak the same LAN protocols and are expected to work, but are unverified on real hardware — treat them as best-effort and please [open an issue](https://github.com/DLANSAMA/platecli/issues) with what you hit. One caveat: `plate snapshot` grabs the camera directly (no extra software) on P1/A1-class printers. X1-series cameras need a locally-running Docker streamer, and that path is opt-in (`camera_allow_streamer` or `--allow-camera-streamer`) because the streamer does not honour `cert_fingerprint`.

## Install

**Requirements:** Python 3.10+, and [OrcaSlicer](https://github.com/OrcaSlicer/OrcaSlicer/releases) installed locally if you want to slice. `plate slice` and `plate job` shell out to the OrcaSlicer binary; `download`, `status`, `upload`, and `print` do not need it. `plate setup` auto-detects the usual install locations (macOS app bundle, Windows Program Files, and on Linux a `$PATH` binary, Flatpak export, or AppImage), and `plate preflight` (or `plate config validate`) tells you if it can't find one.
**Requirements:** Python 3.10+, and [OrcaSlicer](https://github.com/OrcaSlicer/OrcaSlicer/releases) installed locally to slice — it is a **second slicer app, not Bambu Studio**, and `plate` cannot slice without it. `plate go`, `plate job`, and `plate slice` shell out to the OrcaSlicer binary; `download`, `status`, `upload`, and `print` do not need it. `plate setup` auto-detects the usual install locations (macOS app bundle, Windows Program Files, and on Linux a `$PATH` binary, Flatpak export, or AppImage), and `plate preflight` (or `plate config validate`) tells you if it can't find one.

Fastest way to get OrcaSlicer, if you don't have it:

Expand Down Expand Up @@ -78,11 +78,17 @@ plate --sim status

## Print something

Enable LAN mode on your printer, grab the IP, serial, and access code from its touchscreen, then let the interactive setup walk you through the rest:
Four steps, no flags to learn:

1. **Install OrcaSlicer** (see [Install](#install)). `plate` runs it to slice; it is a second slicer app, not Bambu Studio.
2. **Turn on LAN mode** on the printer's touchscreen and note the **IP address**, **serial number**, and **LAN access code** shown next to it. The access code is *not* your Bambu account password, and it changes every time LAN mode is toggled or the printer is factory-reset — re-run `plate setup` if it stops connecting.
3. **`plate setup`** asks for those three values and finds OrcaSlicer for you. `plate doctor` then checks the connection end to end (optional).
4. **`plate go`** — paste a model link (or a local file), pick a material and quality, look at the time and filament estimate, and say yes. Plain `plate` in a terminal opens the same wizard.

```bash
plate setup
plate doctor # optional: verify the connection end to end
plate go # or: plate go "https://www.printables.com/model/3161-3d-benchy"
```

<picture>
Expand All @@ -91,24 +97,17 @@ plate doctor # optional: verify the connection end to end
<img alt="plate doctor: config, MQTT, and FTPS health checks with TLS-pin verification against a real printer" src="https://raw.githubusercontent.com/DLANSAMA/platecli/main/docs/doctor-dark.gif">
</picture>

Now go from a link on the internet to plastic on the bed:
The wizard walks you from a model URL (or local file) to a running print: paste a source, confirm the printer, pick a material and quality preset, answer one supports question, then see a time and filament preview before a final confirm. OrcaSlicer still does the slicing underneath — you just never touch its settings. If your printer has an AMS, the material step defaults to whatever filament is loaded. Declining the final question offers upload-only, and cancelling keeps the sliced file.

```bash
plate job "https://www.printables.com/model/3161-3d-benchy" --confirm
```
### Scripts and agents: `plate job`

`--confirm` is required for anything that moves the printer or destroys data on it: `print`, `stop`, `pause`, `resume`, `gcode`, and `delete`. Leave it off and the command refuses with exit code `5` — nothing on the printer moves. For `job` / `send`, omitting `--confirm` still runs the download → slice → upload pipeline and exits `0` with `"status": "uploaded_not_printed"`; only the print step is withheld. (`light` is exempt; an LED is not a physical action.)

### Prefer a guided walk-through?

If you'd rather not think about flags, run the wizard — or just type `plate` on its own:
`plate go` needs an interactive terminal. Everything it asks maps to a flag on `plate job`, which runs the same `download` → `slice` → `upload` → `print` pipeline in one shot, so the result is identical:

```bash
plate go # or: plate go "https://www.printables.com/model/3161-3d-benchy"
plate # bare `plate` on a terminal launches the same wizard
plate job "https://www.printables.com/model/3161-3d-benchy" --confirm
```

It walks you from a model URL (or local file) to a running print without touching a slicer: paste a source, confirm the printer, pick a material and quality preset, answer one supports question, then see a time and filament preview before a final confirm. If your printer has an AMS, the material step defaults to whatever filament is loaded. It drives the same `download` → `slice` → `job` pipeline as `plate job`, so the result is identical — it just asks the questions for you. `plate go` needs an interactive terminal; for scripts and agents, use `plate job <url> --confirm`.
`--confirm` is what starts the print. **Without it, `job` / `send` still download, slice, and upload the file to the printer** — they exit `0` with `"status": "uploaded_not_printed"`, and the file sits on the printer unstarted. The commands that act on the printer directly are stricter: `print`, `stop`, `pause`, `resume`, `gcode`, and `delete` refuse without `--confirm` (exit code `5`) and do nothing. (`light` is exempt; an LED is not a physical action.)

### Watch the printer while it works

Expand All @@ -132,7 +131,7 @@ It is a front-end, not new machinery: it slices and builds the `job` request thr

## Why platecli

- **One command, whole pipeline** — `plate job <url>` downloads, slices, uploads, and prints in one shot; or run `download` / `slice` / `upload` / `print` individually.
- **One command, whole pipeline** — `plate go` asks the questions; `plate job <url> --confirm` takes flags. Both download, slice, upload, and print in one shot; or run `download` / `slice` / `upload` / `print` individually.
- **Fully local & private** — talks straight to the printer over your LAN; no Bambu cloud account, ever.
- **Deliberate-action gate** — physical commands refuse without `--confirm` (exit `5`), so a typo, a truncated argument list, or a replayed read-only command can't start a print. It is a gate against *accidents*, not an authorization boundary: `plate` cannot tell your `--confirm` from an agent's, so anything you let run `plate` can pass the flag. Sandbox agents accordingly.
- **AI-agent ready** — every command speaks `--json` with published schemas, plus a `--sim` mode for hardware-free automation.
Expand Down Expand Up @@ -164,7 +163,7 @@ you can put in a shell script or hand to an agent, use this.

## Built for AI agents

Every command emits machine-readable `--json` output backed by published [JSON Schemas](https://github.com/DLANSAMA/platecli/tree/main/docs/schemas/), `--sim` provides a canned printer (not a protocol test) for development without hardware, and the `--confirm` gate means physical actions never happen by accident. Two commands are deliberately human-only — the `go` wizard and the `tui` full-screen UI refuse `--json` and a non-TTY stdin with exit `5`; `plate job <url> --confirm` is the machine path that does the same work. See the [user guide](https://github.com/DLANSAMA/platecli/blob/main/docs/manual.md) and [docs/api.md](https://github.com/DLANSAMA/platecli/blob/main/docs/api.md) for the JSON contracts and stability policy.
Every command emits machine-readable `--json` output backed by published [JSON Schemas](https://github.com/DLANSAMA/platecli/tree/main/docs/schemas/), `--sim` provides a canned printer (not a protocol test) for development without hardware, and the `--confirm` gate means a print never starts by accident — note that `job` / `send` without `--confirm` still upload (`uploaded_not_printed`); only the six direct printer commands refuse outright. Two commands are deliberately human-only — the `go` wizard and the `tui` full-screen UI refuse `--json` and a non-TTY stdin with exit `5`; `plate job <url> --confirm` is the machine path that does the same work. See the [user guide](https://github.com/DLANSAMA/platecli/blob/main/docs/manual.md) and [docs/api.md](https://github.com/DLANSAMA/platecli/blob/main/docs/api.md) for the JSON contracts and stability policy.

## Documentation

Expand Down
24 changes: 19 additions & 5 deletions bambu_cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
_SilentArgumentParser,
_SilentParseError,
build_parser,
first_run_text,
get_global_parser,
)
from .constants import (
Expand Down Expand Up @@ -205,7 +206,9 @@ def _handle_bambu_error(exc, command_name):
failed_step=exc.failed_step,
**extra,
)
if msg and not msg.startswith("Command failed (exit "):
# Sites that raise through emit_json_error have already logged the line
# (exc.logged); printing it here again was the "every error twice" bug.
if msg and not msg.startswith("Command failed (exit ") and not getattr(exc, "logged", False):
_safe_log_error(msg)
sys.exit(exc.exit_code)

Expand Down Expand Up @@ -237,6 +240,13 @@ def _handle_interrupt(interrupt_args, command_name):
printer_ip = _context.current_settings().printer_ip
if printer_ip == "0.0.0.0":
message = "Printer IP is not configured. Please run `plate setup` first."
if args.cmd == "doctor":
# doctor is the "is my setup OK?" command; without a printer the
# useful checks are the local ones, so name them.
message += (
" No printer yet? `plate preflight` checks the local setup (Python, OrcaSlicer, profiles)"
" without one, and `plate --sim status` fakes a printer."
)
write_error_envelope(args, args.cmd or "main", EXIT_CONFIG_ERROR, message, failed_step="config")
logger.error(message)
sys.exit(EXIT_CONFIG_ERROR)
Expand Down Expand Up @@ -282,11 +292,12 @@ def _handle_interrupt(interrupt_args, command_name):
# and without --json launches the guided wizard — the highest-leverage
# ease-of-use win for someone who just installed `plate` and typed it to
# see what happens (plan §11 Q1). Any machine-use flag (--json) or a
# non-TTY stream (CI, pipes, subprocess, `plate | less`) keeps today's
# exact behavior below: help to stderr, EXIT_COMMAND_ERROR.
# non-TTY stream (CI, pipes, subprocess, `plate | less`) gets the short
# first-run text below on stderr and EXIT_COMMAND_ERROR — never the
# wizard or the TUI.
_go = _resolve_command("go")
if _go is None: # pragma: no cover -- go is always registered
parser.print_help(sys.stderr)
print(first_run_text(), file=sys.stderr)
sys.exit(EXIT_COMMAND_ERROR)
args.cmd = "go"
try:
Expand All @@ -296,7 +307,10 @@ def _handle_interrupt(interrupt_args, command_name):
except BambuError as exc:
_handle_bambu_error(exc, "go")
else:
parser.print_help(sys.stderr)
# Bare `plate` without a terminal: a person reading this has probably
# just installed it, so say what to do next instead of dumping every
# subcommand. Still a usage error for scripts (exit 5, stdout untouched).
print(first_run_text(), file=sys.stderr)
sys.exit(EXIT_COMMAND_ERROR)


Expand Down
58 changes: 55 additions & 3 deletions bambu_cli/cliparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,14 @@ def _add_slice_override_args(parser):

def _add_job_arguments(parser):
parser.add_argument("source", help="URL or local path to .stl/.step/.stp/.obj/.3mf/.gcode/.zip")
parser.add_argument("--confirm", action="store_true", help="Confirm print start after upload")
parser.add_argument(
"--confirm",
action="store_true",
help=(
"Start the print after upload. Without it the model is still downloaded, sliced, and "
"uploaded to the printer (status uploaded_not_printed); only the print start is withheld"
),
)
parser.add_argument(
"--dry-run", action="store_true", help="No-side-effect validation; skip download/slice/upload/print"
)
Expand Down Expand Up @@ -217,12 +224,54 @@ def _add_job_arguments(parser):
_add_slice_override_args(parser)


def first_run_text() -> str:
"""The short guide bare ``plate`` prints when it cannot open the wizard.

Written for a Bambu owner who has never used a CLI: the four steps from a
fresh install to a first print, in order, with the two things that trip
people up said out loud (OrcaSlicer is a second slicer, not Bambu Studio;
the LAN access code is not the account password and it rotates).
Deliberately not the argparse dump — that is one ``plate --help`` away.
"""
# Local import: config probes the filesystem for defaults at import time,
# and parser construction should stay side-effect free.
from bambu_cli.config import orca_install_command

return (
"plate — print to a Bambu Lab printer from your terminal, no cloud account.\n"
"\n"
"First time? Four steps, in order:\n"
"\n"
" 1. Install OrcaSlicer. plate uses it to slice. It is a separate slicer app,\n"
" not Bambu Studio:\n"
f" {orca_install_command()}\n"
" 2. On the printer's touchscreen turn on LAN mode, then note the IP address,\n"
" serial number, and LAN access code shown there. That code is not your\n"
" Bambu account password, and it changes whenever LAN mode is toggled or\n"
" the printer is factory-reset.\n"
" 3. Run: plate setup\n"
" 4. Run: plate go (paste a model link; in a terminal, plain `plate` does the same)\n"
"\n"
"Every command: plate --help\n"
"No printer yet: plate --sim status (a fake printer, no hardware needed)\n"
"Scripts and agents: plate job <url> --json (add --confirm only to start the print)\n"
)


def get_global_parser():
global_parser = argparse.ArgumentParser(add_help=False)
global_parser.add_argument(
"-v", "--verbose", action="store_true", default=argparse.SUPPRESS, help="Enable debug logging"
)
global_parser.add_argument("--sim", action="store_true", default=argparse.SUPPRESS, help="Enable simulation mode")
global_parser.add_argument(
"--sim",
action="store_true",
default=argparse.SUPPRESS,
help=(
"Use a fake printer with canned responses: no hardware and no printer config needed. "
"It is not a protocol test of MQTT/FTPS"
),
)
global_parser.add_argument(
"--json",
action="store_true",
Expand Down Expand Up @@ -332,7 +381,10 @@ def build_parser():
p_go = sub.add_parser(
"go",
parents=[get_global_parser()],
help="Interactive guided print: URL in, plastic out — no slicer knowledge needed",
help=(
"Guided print: paste a model URL or file, answer a few questions, print. "
"OrcaSlicer still does the slicing; you just never learn its flags"
),
)
p_go.add_argument("source", nargs="?", help="Model URL or local file (skips the first prompt)")

Expand Down
2 changes: 1 addition & 1 deletion bambu_cli/commands/go.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@


def cmd_go(args: argparse.Namespace) -> None:
"""Interactive guided print: URL in, plastic out — no slicer knowledge needed."""
"""Guided print: URL or file in, plastic out — OrcaSlicer runs underneath, no flags to learn."""
from bambu_cli.interactive.session import cmd_go as _cmd_go

_cmd_go(args)
8 changes: 6 additions & 2 deletions bambu_cli/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -294,15 +294,19 @@ def detect_profiles_dir():
}


def orca_install_command():
"""Return the fastest one-line OrcaSlicer install command for this platform."""
return _ORCA_INSTALL_COMMANDS.get(sys.platform, _ORCA_INSTALL_COMMANDS["linux"])


def orca_install_hint():
"""Return a one-line, platform-appropriate 'how to install OrcaSlicer' hint.

Both `slice` and `preflight` append this when no OrcaSlicer exists anywhere
on the machine — at that point suggesting a config edit is useless, because
there is nothing to point the config at.
"""
command = _ORCA_INSTALL_COMMANDS.get(sys.platform, _ORCA_INSTALL_COMMANDS["linux"])
return f"Install it with `{command}` (or download from {ORCA_RELEASES_URL}), then run `plate setup`."
return f"Install it with `{orca_install_command()}` (or download from {ORCA_RELEASES_URL}), then run `plate setup`."


_DEFAULT_ORCA = _default_orca_path()
Expand Down
Loading