fix(mqtt): close #1822 — promote H2S tray_now to 254 on all-external prints

H2S firmware reports tray_now=0 (the AMS's idle slot) throughout
      external-spool prints instead of 254 like X1C/P1S/A1 do, so the
      single-nozzle branch's 0-3 passthrough landed state.tray_now on slot
      0 — UI highlighted AMS SLOT 1 instead of the external spool.
      Usage credit was unaffected (#1276 covers that via ams_mapping).

      The single-nozzle branch now checks _captured_ams_mapping (slicer-
      captured per-filament mapping that the request-topic intercept
      already tracks) before the existing P2S multi-AMS resolver. When
      every entry is -1, the print uses ONLY the external spool, so
      state.tray_now is promoted to 254.

      Narrow on purpose: AMS-only [5] and mixed [5, -1] are NOT
      overridden — we have no evidence H2S misreports mid-print swaps, and
      trusting the firmware preserves correctness for users with multi-
      filament setups. No-mapping prints (printer-screen start) fall
      through unchanged.
This commit is contained in:
maziggy
2026-06-28 12:45:06 +02:00
3 changed files with 134 additions and 28 deletions
+48 -28
View File
@@ -1766,35 +1766,55 @@ class BambuMQTTClient:
self.state.tray_now = parsed_tray_now
elif not self._is_dual_nozzle and 0 <= parsed_tray_now <= 3:
# Single-nozzle printer with tray_now in 0-3 range.
# P2S (and possibly other models) with multiple AMS units sends LOCAL slot IDs
# in tray_now, not global tray IDs (#420). Use the MQTT mapping field
# (snow-encoded) to resolve the correct AMS unit.
ams_exist_raw = ams_data.get("ams_exist_bits", "0")
try:
ams_exist = int(ams_exist_raw, 16) if isinstance(ams_exist_raw, str) else int(ams_exist_raw)
except (ValueError, TypeError):
ams_exist = 0
num_ams = bin(ams_exist).count("1")
if num_ams > 1:
# Multiple AMS on single-nozzle — tray_now is likely a local slot ID.
# Cross-reference with MQTT mapping field to find the correct AMS unit.
mapping_raw = self.state.raw_data.get("mapping")
resolved = self._resolve_local_slot_from_mapping(parsed_tray_now, mapping_raw)
if resolved is not None:
if resolved != parsed_tray_now:
logger.debug(
f"[{self.serial_number}] Multi-AMS tray_now: "
f"local slot {parsed_tray_now} -> global ID {resolved} (from mapping)"
)
self.state.tray_now = resolved
else:
# No mapping available (not printing, or ambiguous) — use as-is.
# This matches the old behavior and is correct for AMS 0.
self.state.tray_now = parsed_tray_now
# #1822: H2S firmware reports tray_now as the AMS's idle
# slot (typically 0) when the active feed is actually the
# external spool. X1C / P1S / A1 correctly report 254 in
# that case; H2S does not. When the slicer-captured
# ams_mapping is all-external (every entry == -1), the
# print can only be feeding from the external spool, so
# promote tray_now to 254. Mixed (e.g. [5, -1]) and
# AMS-only mappings are NOT overridden — there's no
# evidence the firmware misreports in those cases. Prints
# started without a captured mapping (printer-screen start,
# or before Bambuddy connected) fall through unchanged.
captured = self._captured_ams_mapping
if captured and all(s == -1 for s in captured):
if self.state.tray_now != 254:
logger.debug(
f"[{self.serial_number}] tray_now external-spool override (#1822): "
f"slot {parsed_tray_now} -> 254 (ams_mapping={captured})"
)
self.state.tray_now = 254
else:
# Single AMS — local slot 0-3 equals global ID
self.state.tray_now = parsed_tray_now
# P2S (and possibly other models) with multiple AMS units sends LOCAL slot IDs
# in tray_now, not global tray IDs (#420). Use the MQTT mapping field
# (snow-encoded) to resolve the correct AMS unit.
ams_exist_raw = ams_data.get("ams_exist_bits", "0")
try:
ams_exist = int(ams_exist_raw, 16) if isinstance(ams_exist_raw, str) else int(ams_exist_raw)
except (ValueError, TypeError):
ams_exist = 0
num_ams = bin(ams_exist).count("1")
if num_ams > 1:
# Multiple AMS on single-nozzle — tray_now is likely a local slot ID.
# Cross-reference with MQTT mapping field to find the correct AMS unit.
mapping_raw = self.state.raw_data.get("mapping")
resolved = self._resolve_local_slot_from_mapping(parsed_tray_now, mapping_raw)
if resolved is not None:
if resolved != parsed_tray_now:
logger.debug(
f"[{self.serial_number}] Multi-AMS tray_now: "
f"local slot {parsed_tray_now} -> global ID {resolved} (from mapping)"
)
self.state.tray_now = resolved
else:
# No mapping available (not printing, or ambiguous) — use as-is.
# This matches the old behavior and is correct for AMS 0.
self.state.tray_now = parsed_tray_now
else:
# Single AMS — local slot 0-3 equals global ID
self.state.tray_now = parsed_tray_now
else:
# tray_now > 3 means it's already a global ID, or 255 means unloaded
# Note: Do NOT clear pending_tray_target on tray_now=255 here.