|
|
|
|
@@ -21,6 +21,7 @@ import os
|
|
|
|
|
import platform
|
|
|
|
|
import sys
|
|
|
|
|
import select
|
|
|
|
|
import time
|
|
|
|
|
from contextlib import contextmanager
|
|
|
|
|
from dataclasses import dataclass, field
|
|
|
|
|
from datetime import datetime
|
|
|
|
|
@@ -486,15 +487,29 @@ class PrinterState:
|
|
|
|
|
|
|
|
|
|
wifi_signal = print_data.get("wifi_signal", "")
|
|
|
|
|
|
|
|
|
|
# H2C dual-nozzle decoding. The H2C reports per-extruder temperatures
|
|
|
|
|
# under `print.device.extruder.info[]` as a 2-element array (index 0 =
|
|
|
|
|
# right, index 1 = left). The `temp` field is bit-packed:
|
|
|
|
|
# Dual-nozzle decoding (H2C, X2D). Dual-nozzle printers report per-extruder
|
|
|
|
|
# temperatures under `print.device.extruder.info[]` as a 2-element array
|
|
|
|
|
# (index 0 = right, index 1 = left). The `temp` field is bit-packed:
|
|
|
|
|
# `temp_raw = (target << 16) | current`, both °C as ints.
|
|
|
|
|
#
|
|
|
|
|
# The legacy top-level `nozzle_temper`/`nozzle_target_temper` fields track
|
|
|
|
|
# whichever nozzle is currently *active*, not specifically the right one —
|
|
|
|
|
# on printers like the X2D they report the left nozzle's temp while it's
|
|
|
|
|
# printing, even though the dashboard's "Right Nozzle" card reads them.
|
|
|
|
|
# Prefer the decoded right-side value from extruder.info[0] when present.
|
|
|
|
|
nozzle_temp_left = None
|
|
|
|
|
nozzle_target_temp_left = None
|
|
|
|
|
nozzle_temp_right = None
|
|
|
|
|
nozzle_target_temp_right = None
|
|
|
|
|
device = print_data.get("device") or {}
|
|
|
|
|
extruders = (device.get("extruder") or {}).get("info") or []
|
|
|
|
|
if len(extruders) >= 2:
|
|
|
|
|
right = extruders[0]
|
|
|
|
|
t = right.get("temp")
|
|
|
|
|
if isinstance(t, int):
|
|
|
|
|
nozzle_target_temp_right = float((t >> 16) & 0xFFFF)
|
|
|
|
|
nozzle_temp_right = float(t & 0xFFFF)
|
|
|
|
|
|
|
|
|
|
left = extruders[1]
|
|
|
|
|
t = left.get("temp")
|
|
|
|
|
if isinstance(t, int):
|
|
|
|
|
@@ -510,8 +525,14 @@ class PrinterState:
|
|
|
|
|
return cls(
|
|
|
|
|
timestamp=timestamp,
|
|
|
|
|
sequence_id=str(print_data.get("sequence_id", "")),
|
|
|
|
|
nozzle_temp=float(print_data.get("nozzle_temper", 0.0)),
|
|
|
|
|
nozzle_target_temp=float(print_data.get("nozzle_target_temper", 0.0)),
|
|
|
|
|
nozzle_temp=(
|
|
|
|
|
nozzle_temp_right if nozzle_temp_right is not None
|
|
|
|
|
else float(print_data.get("nozzle_temper", 0.0))
|
|
|
|
|
),
|
|
|
|
|
nozzle_target_temp=(
|
|
|
|
|
nozzle_target_temp_right if nozzle_target_temp_right is not None
|
|
|
|
|
else float(print_data.get("nozzle_target_temper", 0.0))
|
|
|
|
|
),
|
|
|
|
|
bed_temp=float(print_data.get("bed_temper", 0.0)),
|
|
|
|
|
bed_target_temp=float(print_data.get("bed_target_temper", 0.0)),
|
|
|
|
|
chamber_temp=float(print_data.get("chamber_temper", 0.0)),
|
|
|
|
|
@@ -809,6 +830,7 @@ class BambuPrinter:
|
|
|
|
|
self._accumulator = PrinterStateAccumulator()
|
|
|
|
|
self._connected = False
|
|
|
|
|
self._devices: List[Dict[str, Any]] = []
|
|
|
|
|
self._last_message_at: Optional[float] = None
|
|
|
|
|
|
|
|
|
|
def _get_fresh_token(self, verification_code_timeout: int = 300) -> str:
|
|
|
|
|
"""Get a fresh token using credentials."""
|
|
|
|
|
@@ -914,6 +936,30 @@ class BambuPrinter:
|
|
|
|
|
"""Internal MQTT message handler"""
|
|
|
|
|
if not data:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
# The printer publishes partial deltas most of the time; the accumulator
|
|
|
|
|
# merges them onto whatever it already has. If the printer went offline
|
|
|
|
|
# (power cycle) and just reconnected, the first delta after the gap would
|
|
|
|
|
# otherwise be merged onto stale pre-outage state, leaving fields like
|
|
|
|
|
# nozzle_temp frozen at their last value until some unrelated full report
|
|
|
|
|
# happens to refresh them. Detect the gap and force a full pushall so the
|
|
|
|
|
# accumulator gets a clean, complete state instead.
|
|
|
|
|
now = time.time()
|
|
|
|
|
if (
|
|
|
|
|
self._last_message_at is not None
|
|
|
|
|
and now - self._last_message_at > app_settings.MQTT_RESYNC_GAP_SECONDS
|
|
|
|
|
and self._mqtt is not None
|
|
|
|
|
):
|
|
|
|
|
try:
|
|
|
|
|
self._mqtt.request_full_status()
|
|
|
|
|
logger.info(
|
|
|
|
|
"MQTT report gap of %.0fs detected for %s; requested full status re-sync",
|
|
|
|
|
now - self._last_message_at, device_id,
|
|
|
|
|
)
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.warning("Full status re-sync request failed (non-fatal): %s", e)
|
|
|
|
|
self._last_message_at = now
|
|
|
|
|
|
|
|
|
|
state = self._accumulator.update(data)
|
|
|
|
|
if self._on_update:
|
|
|
|
|
self._on_update(state)
|
|
|
|
|
|