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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); version

### Changed

- `--json` emitters construct `bambu_cli.contracts` objects. `status --json`
no longer flattens firmware fields onto the envelope; they stay under
`printer`. Errors go through `ErrorEnvelope`; `job` success/failure through
`JobOk` / `JobError`.

- MQTT client construction, command/status, print-ack, and the monitor loop
live in separate `protocols/mqtt_*.py` modules. TLS pinning uses a real
`SSLContext` subclass (`PinningSSLContext`) instead of patching
Expand Down
16 changes: 8 additions & 8 deletions bambu_cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
# namespace without importing this entrypoint (audit item A1). Re-exported here
# because build_parser() is the documented source of truth for the command set,
# and the help/workflow smokes plus several tests import it from this module.
from .contracts import Version
from .contracts import ErrorEnvelope, Version
from .jsonio import json_mode_requested as _json_mode_requested
from .utils import emit_json, emit_json_error

Expand Down Expand Up @@ -150,13 +150,13 @@ def main():
return
if not args.cmd and bool(getattr(args, "json", False)):
emit_json(
{
"status": "error",
"command": "main",
"failed_step": "parse",
"exit_code": EXIT_COMMAND_ERROR,
"error": "Missing subcommand. Put --json with a command that supports it.",
}
ErrorEnvelope(
status="error",
command="main",
failed_step="parse",
exit_code=EXIT_COMMAND_ERROR,
error="Missing subcommand. Put --json with a command that supports it.",
)
)
sys.exit(EXIT_COMMAND_ERROR)

Expand Down
22 changes: 13 additions & 9 deletions bambu_cli/commands/doctor.py
Original file line number Diff line number Diff line change
Expand Up @@ -237,14 +237,18 @@ def shown_ip():
if json_mode:
# Mask IP address inside doctor capabilities report unless --verbose is checked (A0530-SEC-16)
reported_ip = ctx.settings.printer_ip if verbose else "<redacted>"
from bambu_cli.contracts import Doctor

emit_json(
{
"command": "doctor",
"ok": True,
"status": "ok",
"output": cap_path,
"printer_ip": reported_ip,
"certificate_fingerprint": fp,
"capabilities": capabilities,
}
Doctor(
status="ok",
command="doctor",
certificate_fingerprint=fp,
printer_reachable=True,
).to_payload(
ok=True,
output=cap_path,
printer_ip=reported_ip,
capabilities=capabilities,
)
)
77 changes: 40 additions & 37 deletions bambu_cli/commands/files.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,15 +116,17 @@ def cmd_upload(args, ctx=None):

logger.info(f" ✅ Local file {_path_for_message(filepath)} exists ({filesize // 1024}KB)")
if bool(_namespace_get(args, "json", False)):
from bambu_cli.contracts import Upload

emit_json(
{
"status": "dry_run_ok",
"command": "upload",
"file": filepath,
"remote_name": filename,
"bytes": filesize,
"uploaded": False,
}
Upload(
status="dry_run_ok",
command="upload",
file=filepath,
remote_name=filename,
bytes=filesize,
uploaded=False,
)
)
return filename

Expand Down Expand Up @@ -175,15 +177,17 @@ def _cb(block):
if success:
logger.info(f"✅ Uploaded {filename} to printer")
if bool(_namespace_get(args, "json", False)):
from bambu_cli.contracts import Upload

emit_json(
{
"status": "uploaded",
"command": "upload",
"file": filepath,
"remote_name": filename,
"bytes": filesize,
"uploaded": True,
}
Upload(
status="uploaded",
command="upload",
file=filepath,
remote_name=filename,
bytes=filesize,
uploaded=True,
)
)
return filename
else:
Expand Down Expand Up @@ -215,13 +219,15 @@ def cmd_files(args, ctx=None):
raise Exception("Failed to list files via printer API")
remote_files = [{"name": _portable_basename(path), "path": path} for path in files]
if json_mode:
from bambu_cli.contracts import Files, RemoteFile

emit_json(
{
"status": "ok",
"command": "files",
"count": len(remote_files),
"files": remote_files,
}
Files(
status="ok",
command="files",
count=len(remote_files),
files=[RemoteFile(name=item["name"], path=item["path"]) for item in remote_files],
)
)
return
if not files:
Expand Down Expand Up @@ -251,14 +257,16 @@ def cmd_delete(args, ctx=None):
if not args.confirm:
logger.warning(f"⚠️ This will DELETE '{filename}' from the printer. Add --confirm to proceed.")
if bool(_namespace_get(args, "json", False)):
from bambu_cli.contracts import Delete

emit_json(
{
"status": "confirmation_required",
"command": "delete",
"file": filename,
"deleted": False,
"next_command": ["delete", filename, "--confirm", "--json"],
}
Delete(
status="confirmation_required",
command="delete",
file=filename,
deleted=False,
next_command=["delete", filename, "--confirm", "--json"],
)
)
abort("", exit_code=EXIT_COMMAND_ERROR)

Expand All @@ -267,14 +275,9 @@ def cmd_delete(args, ctx=None):
if printer.delete_file(f"/model/{filename}"):
logger.info(f"🗑️ Deleted {filename} from printer")
if bool(_namespace_get(args, "json", False)):
emit_json(
{
"status": "deleted",
"command": "delete",
"file": filename,
"deleted": True,
}
)
from bambu_cli.contracts import Delete

emit_json(Delete(status="deleted", command="delete", file=filename, deleted=True))
else:
raise Exception("Delete operation failed in printer client.")
except Exception as e:
Expand Down
24 changes: 9 additions & 15 deletions bambu_cli/commands/gcode.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from bambu_cli.argutils import namespace_get as _namespace_get
from bambu_cli.constants import EXIT_COMMAND_ERROR, EXIT_NETWORK_ERROR
from bambu_cli.context import RuntimeContext
from bambu_cli.contracts import Gcode
from bambu_cli.download.naming import _has_command_injection_chars
from bambu_cli.errors import abort
from bambu_cli.logging_utils import logger, safe_log_error
Expand All @@ -29,13 +30,13 @@ def cmd_gcode(args, ctx=None):
logger.warning("⚠️ This will SEND raw G-code to the printer. Add --confirm to proceed.")
if bool(_namespace_get(args, "json", False)):
emit_json(
{
"status": "confirmation_required",
"command": "gcode",
"gcode": gcode,
"sent": False,
"next_command": ["gcode", gcode, "--confirm", "--json"],
}
Gcode(
status="confirmation_required",
command="gcode",
gcode=gcode,
sent=False,
next_command=["gcode", gcode, "--confirm", "--json"],
)
)
abort("", exit_code=EXIT_COMMAND_ERROR)

Expand All @@ -48,11 +49,4 @@ def cmd_gcode(args, ctx=None):
abort("", exit_code=EXIT_NETWORK_ERROR)
logger.info(f"📡 Sent: {gcode}")
if bool(_namespace_get(args, "json", False)):
emit_json(
{
"status": "sent",
"command": "gcode",
"gcode": gcode,
"sent": True,
}
)
emit_json(Gcode(status="sent", command="gcode", gcode=gcode, sent=True))
29 changes: 15 additions & 14 deletions bambu_cli/commands/print_cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from bambu_cli.argutils import namespace_get as _namespace_get
from bambu_cli.constants import EXIT_COMMAND_ERROR, EXIT_FILE_ERROR
from bambu_cli.context import RuntimeContext
from bambu_cli.contracts import Print
from bambu_cli.download.naming import (
_is_print_ready_name,
_name_for_message,
Expand Down Expand Up @@ -45,13 +46,13 @@ def cmd_print(args, ctx=None):
logger.warning("⚠️ This will START a print. Add --confirm to proceed.")
if bool(_namespace_get(args, "json", False)):
emit_json(
{
"status": "confirmation_required",
"command": "print",
"file": basename,
"printed": False,
"next_command": _print_next_command(args, basename),
}
Print(
status="confirmation_required",
command="print",
file=basename,
printed=False,
next_command=_print_next_command(args, basename),
)
)
abort("", exit_code=EXIT_COMMAND_ERROR)

Expand Down Expand Up @@ -88,12 +89,12 @@ def cmd_print(args, ctx=None):
raise
if bool(_namespace_get(args, "json", False)):
emit_json(
{
"status": "dry_run_ok" if dry_run else "print_started",
"command": "print",
"file": basename,
"printed": not dry_run,
"dry_run": bool(dry_run),
}
Print(
status="dry_run_ok" if dry_run else "print_started",
command="print",
file=basename,
printed=not dry_run,
dry_run=bool(dry_run),
)
)
return basename
42 changes: 23 additions & 19 deletions bambu_cli/commands/snapshot.py
Original file line number Diff line number Diff line change
Expand Up @@ -274,16 +274,18 @@ def cmd_snapshot(
sha256 = hashlib.sha256(_frame).hexdigest()
logger.info(f"\U0001f4f8 Snapshot saved: {_path_for_message(outpath)} ({size // 1024}KB)")
if bool(_namespace_get(args, "json", False)):
from bambu_cli.contracts import Snapshot

emit_json(
{
"status": "saved",
"command": "snapshot",
"output": outpath,
"size_bytes": size,
"captured_at": captured_at,
"sha256": sha256,
"method": "direct",
}
Snapshot(
status="saved",
command="snapshot",
output=outpath,
size_bytes=size,
captured_at=captured_at,
sha256=sha256,
method="direct",
)
)
return

Expand Down Expand Up @@ -416,17 +418,19 @@ def cmd_snapshot(
sha256 = hashlib.sha256(data).hexdigest()
logger.info(f"✅ Snapshot saved: {_path_for_message(outpath)} ({size // 1024}KB)")
if bool(_namespace_get(args, "json", False)):
from bambu_cli.contracts import Snapshot

emit_json(
{
"status": "saved",
"command": "snapshot",
"output": outpath,
"size_bytes": size,
"captured_at": captured_at,
"sha256": sha256,
"camera_image": camera_image,
"docker_container": "bambu_camera",
}
Snapshot(
status="saved",
command="snapshot",
output=outpath,
size_bytes=size,
captured_at=captured_at,
sha256=sha256,
camera_image=camera_image,
docker_container="bambu_camera",
)
)
except urllib.error.URLError as e:
message = f"Snapshot network error: {e}"
Expand Down
32 changes: 22 additions & 10 deletions bambu_cli/commands/status.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,32 @@
"""Printer status command."""

import dataclasses

from bambu_cli.argutils import namespace_get as _namespace_get
from bambu_cli.context import RuntimeContext
from bambu_cli.contracts import PrinterState, Status
from bambu_cli.errors import PrinterConnectionError
from bambu_cli.logging_utils import logger
from bambu_cli.utils import emit_json

_PRINTER_STATE_FIELDS = {f.name for f in dataclasses.fields(PrinterState)}


def _status_payload(data, ams):
"""Build the Status contract. Firmware extras stay on ``printer``, never the envelope."""
known = {key: data[key] for key in _PRINTER_STATE_FIELDS if key in data and key != "ams"}
extras = {
key: value
for key, value in data.items()
if key not in _PRINTER_STATE_FIELDS and key not in {"status", "command", "printer", "ams"}
}
payload = Status(status="ok", command="status", printer=PrinterState(**known), ams=ams).to_payload()
if extras:
printer = dict(payload["printer"])
printer.update(extras)
payload["printer"] = printer
return payload


def cmd_status(args, ctx=None):
"""Query and display the printer's current status."""
Expand All @@ -30,16 +51,7 @@ def cmd_status(args, ctx=None):
ams = parse_ams(data)

if bool(_namespace_get(args, "json", False)):
payload = {
"status": "ok",
"command": "status",
"printer": data,
}
payload.update({k: v for k, v in data.items() if k not in ("status", "command")})
# Normalized AMS view (trays/filaments) for agents building --ams-mapping;
# None on printers without an AMS.
payload["ams"] = ams
emit_json(payload)
emit_json(_status_payload(data, ams))
return

state = data.get("gcode_state", "UNKNOWN")
Expand Down
Loading
Loading