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
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ Core printer interaction is `BambuPrinter` in `bambu_cli/printer.py`. Agents and
- Secret-bearing files are tightened to `0600` automatically on POSIX: config.json on load, and the `access_code_file` when `load_access_code()` reads it. Windows relies on NTFS ACLs (see [SECURITY.md](SECURITY.md)).
- Network operations support `timeout` and `retries` through `printer.send_command()` and `printer.status()`.
- `printer.status()` returns a **complete** state snapshot, never a partial MQTT delta: the printer streams incremental updates on its report topic and only answers `pushall` with the whole state, so report messages are merged and the wait continues until `gcode_state`, `mc_percent`, `bed_temper`, and `nozzle_temper` are all present. If only deltas arrive it raises `PrinterStatusIncomplete` (exit `6`) instead of returning a partial — so `plate --json status` never emits a `printer` object missing `gcode_state`. Pass `require_complete=False` for liveness probes that only need to know MQTT works (this is what `doctor` and `print --dry-run` do).
- A long-lived process can call `printer.hold_mqtt()` so `status` / `send_command` / `get_version` reuse one TLS session. The TUI does this: dashboard and monitor refreshes must not open a new connection every few seconds. One-shot CLI commands still connect and tear down. Always pair with `release_mqtt()` so the paho loop thread does not leak (CI runs with `-W error::ResourceWarning`).

### Module layout

Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); version

### Changed

- The TUI dashboard and monitor keep one MQTT TLS session for the process
instead of opening a new connection on every refresh. One-shot CLI commands
still connect and tear down.

- `--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
Expand Down
33 changes: 33 additions & 0 deletions bambu_cli/printer.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import os
import secrets
import ssl
import threading
import time
from typing import Any, Optional

Expand Down Expand Up @@ -47,6 +48,38 @@ def __init__(
self.mqtt_timeout = 5.0
self.ftps_timeout = 15.0

# Long-lived MQTT session. None for one-shot CLI (connect + teardown).
# The TUI calls hold_mqtt() so dashboard/monitor refreshes reuse one
# TLS connection instead of opening a new one every 10s.
self._mqtt_session: Any = None
self._mqtt_hold_lock = threading.Lock()

def hold_mqtt(self, *, client_factory=None) -> None:
"""Keep one MQTT client for subsequent status/send_command/get_version.

No-op in simulation mode (nothing opens a socket). Safe to call twice.
Pair with ``release_mqtt()`` so the loop thread and socket do not leak.
"""
if self.simulation_mode:
return
with self._mqtt_hold_lock:
if self._mqtt_session is None:
from bambu_cli.protocols.mqtt_session import MqttSession

self._mqtt_session = MqttSession(self, client_factory=client_factory)

def release_mqtt(self) -> None:
"""Disconnect a held MQTT session. Safe to call twice."""
with self._mqtt_hold_lock:
session = self._mqtt_session
self._mqtt_session = None
if session is not None:
session.close()

@property
def mqtt_held(self) -> bool:
return self._mqtt_session is not None

def send_command(self, payload: str, timeout: Optional[float] = None, retries: int = 2) -> bool:
"""Send a JSON command payload via MQTT."""
return mqtt_protocol.send_command(self, payload, timeout=timeout, retries=retries)
Expand Down
9 changes: 6 additions & 3 deletions bambu_cli/protocols/mqtt.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
"""MQTT transport facade.

Implementations live in ``mqtt_tls``, ``mqtt_cmd``, ``mqtt_print``, and
``mqtt_monitor``. This module re-exports the public names so existing imports
and test patches on ``bambu_cli.protocols.mqtt`` keep working.
Implementations live in ``mqtt_tls``, ``mqtt_cmd``, ``mqtt_print``,
``mqtt_monitor``, and ``mqtt_session``. This module re-exports the public
names so existing imports and test patches on ``bambu_cli.protocols.mqtt``
keep working.
"""

from __future__ import annotations
Expand All @@ -19,6 +20,7 @@
)
from bambu_cli.protocols.mqtt_monitor import _status_event, monitor_status
from bambu_cli.protocols.mqtt_print import _printer_error_hex, execute_print_command
from bambu_cli.protocols.mqtt_session import MqttSession
from bambu_cli.protocols.mqtt_tls import (
PinningSSLContext,
_mqtt_connect,
Expand All @@ -39,6 +41,7 @@
"_printer_error_hex",
"_require_mqtt",
"_status_event",
"MqttSession",
"create_mqtt_client",
"execute_print_command",
"get_status",
Expand Down
12 changes: 12 additions & 0 deletions bambu_cli/protocols/mqtt_cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ def send_command(
logger.info(f"🤖 [SIM] Sending command: {payload}")
return True

session = getattr(printer, "_mqtt_session", None)
if session is not None:
return session.send_command(payload, timeout, retries)

for attempt in range(retries + 1):
client = _factory(printer)
client.user_data_set({})
Expand Down Expand Up @@ -164,6 +168,10 @@ def get_status(printer, timeout=None, retries=2, *, require_complete=True):
},
}

session = getattr(printer, "_mqtt_session", None)
if session is not None:
return session.get_status(timeout, retries, require_complete=require_complete)

merged: dict = {}
merged_lock = threading.Lock()
_factory = _client_factory(None)
Expand Down Expand Up @@ -265,6 +273,10 @@ def get_version(printer, timeout=5, retries=1):
if printer.simulation_mode:
return [{"name": "ota", "sw_ver": "01.00.00.00", "hw_ver": "P1P-SIM"}]

session = getattr(printer, "_mqtt_session", None)
if session is not None:
return session.get_version(timeout, retries)

_factory = _client_factory(None)
_sleep_fn = _sleep(None)

Expand Down
Loading
Loading