5 Commits

Author SHA1 Message Date
github-actions[bot]
1e35c48be1 chore: bump version to 0.1.11 [skip ci] 2026-08-03 12:43:07 +00:00
RunLit
b16f4b3cac Merge pull request #16 from RunLit/fix/x2d-stale-state-after-reconnect
Force MQTT pushall re-sync after a report gap
2026-08-03 22:42:51 +10:00
RunLit
58400f16cf Merge pull request #15 from RunLit/fix/x2d-right-nozzle-temp
Fix X2D right nozzle temp showing active-nozzle value
2026-08-03 22:42:48 +10:00
RNL
6f281a0cab Force MQTT pushall re-sync after a report gap (fixes #14)
The printer publishes partial deltas most of the time; the accumulator
merges each one onto whatever state it already has. When the printer
powers off, our cloud MQTT connection stays up (it's a persistent
session to Bambu's broker, not a socket to the printer), so nothing
signals the outage. When the printer reconnects and resumes its normal
partial updates, those deltas get merged onto the stale pre-outage
state — fields the delta doesn't mention (e.g. nozzle_temp) stay frozen
at their old values. Sending any command happens to trigger a full
report from the printer, which is why toggling the light "fixes" it.

Track the time of the last message per BambuPrinter instance. When a
new message arrives after a gap longer than
BAMBU_RUN_MQTT_RESYNC_GAP_SECONDS (default 90s), request a pushall
before processing it, forcing a complete state refresh instead of
trusting the partial delta to overwrite stale fields.
2026-08-02 23:14:03 +10:00
RNL
4db0a7d728 Fix X2D right nozzle temp showing active-nozzle value (fixes #13)
Dual-nozzle decoding only read extruder.info[1] for the left nozzle,
leaving the right nozzle to fall back on the legacy top-level
nozzle_temper/nozzle_target_temper fields. On the H2C that field
happens to always mirror the right extruder, but on the X2D it tracks
whichever nozzle is currently active — so during a left-only print the
"Right Nozzle" card showed the left nozzle's temperature.

Decode extruder.info[0] the same bit-packed way as the left side and
prefer it over the legacy field when present.
2026-08-02 22:57:03 +10:00
3 changed files with 59 additions and 6 deletions

View File

@@ -81,5 +81,12 @@ class _Settings:
def CLOUD_SYNC_DAYS(self):
return get_setting("BAMBU_RUN_CLOUD_SYNC_DAYS", 30)
# Seconds of silence on the MQTT report topic that, once broken by a new
# message, is treated as "the printer was probably offline" and triggers
# a pushall re-sync instead of trusting the partial delta to fill in stale
# fields left over from before the gap.
@property
def MQTT_RESYNC_GAP_SECONDS(self):
return get_setting("BAMBU_RUN_MQTT_RESYNC_GAP_SECONDS", 90)
app_settings = _Settings()

View File

@@ -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)

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "bambu-run"
version = "0.1.10"
version = "0.1.11"
description = "Django reusable app for Bambu Lab 3D printer monitoring and filament inventory management"
readme = "README.md"
license = {text = "MIT"}