mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-18 21:47:39 +02:00
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:
@@ -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.
|
||||
|
||||
@@ -5884,3 +5884,87 @@ class TestAmsFilamentBackupHoldTimer:
|
||||
assert mqtt_client.state.ams_filament_backup is True
|
||||
# Hold timer still armed — sub-second push didn't reset it.
|
||||
assert mqtt_client._xcam_hold_start["print_option_auto_switch_filament"] == before_hold
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 2c. Single-nozzle H2S — external-spool tray_now override (#1822)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestTrayNowH2SExternalSpoolOverride:
|
||||
"""H2S firmware reports tray_now as the AMS's idle slot (typically 0)
|
||||
instead of 254 when the active feed is the external spool.
|
||||
|
||||
Bambuddy detects the all-external case via the slicer-captured
|
||||
ams_mapping (every entry == -1) and promotes tray_now to 254 so the
|
||||
UI active-tray highlight matches the real feed.
|
||||
|
||||
The override is intentionally narrow:
|
||||
* only fires when ams_mapping is captured AND every entry is -1
|
||||
* does not touch mixed prints ([5, -1]) or AMS-only prints ([5])
|
||||
* does not fire when no ams_mapping is captured (printer-screen start)
|
||||
"""
|
||||
|
||||
@pytest.fixture
|
||||
def mqtt_client(self):
|
||||
from backend.app.services.bambu_mqtt import BambuMQTTClient
|
||||
|
||||
return BambuMQTTClient(
|
||||
ip_address="192.168.1.100",
|
||||
serial_number="TEST_H2S",
|
||||
access_code="12345678",
|
||||
)
|
||||
|
||||
def test_all_external_mapping_promotes_tray_now_to_254(self, mqtt_client):
|
||||
"""Reporter's scenario: H2S, single nozzle, captured ams_mapping=[-1],
|
||||
firmware sends tray_now=0 -> Bambuddy promotes to 254."""
|
||||
mqtt_client._captured_ams_mapping = [-1]
|
||||
mqtt_client._process_message(_ams_payload(0))
|
||||
assert mqtt_client.state.tray_now == 254
|
||||
|
||||
def test_multi_external_mapping_also_promotes(self, mqtt_client):
|
||||
"""Multi-filament print, every filament mapped to external. Still
|
||||
all-external -> still promotes."""
|
||||
mqtt_client._captured_ams_mapping = [-1, -1, -1]
|
||||
mqtt_client._process_message(_ams_payload(0))
|
||||
assert mqtt_client.state.tray_now == 254
|
||||
|
||||
def test_ams_only_mapping_does_not_override(self, mqtt_client):
|
||||
"""ams_mapping=[5] (AMS slot 5 only) -> firmware value trusted as-is.
|
||||
Without the all-external guard, this would falsely override real
|
||||
AMS-slot prints."""
|
||||
mqtt_client._captured_ams_mapping = [5]
|
||||
mqtt_client._process_message(_ams_payload(0))
|
||||
assert mqtt_client.state.tray_now == 0
|
||||
|
||||
def test_mixed_mapping_does_not_override(self, mqtt_client):
|
||||
"""Mixed mapping (AMS slot 5 + external): we have no evidence the
|
||||
firmware misreports mid-print swaps, so leave tray_now alone."""
|
||||
mqtt_client._captured_ams_mapping = [5, -1]
|
||||
mqtt_client._process_message(_ams_payload(0))
|
||||
assert mqtt_client.state.tray_now == 0
|
||||
|
||||
def test_no_captured_mapping_does_not_override(self, mqtt_client):
|
||||
"""Prints started from the printer screen (or before Bambuddy
|
||||
connected) have no captured ams_mapping. Behaviour unchanged from
|
||||
pre-#1822 — we accept the wrong value rather than guess."""
|
||||
mqtt_client._captured_ams_mapping = None
|
||||
mqtt_client._process_message(_ams_payload(0))
|
||||
assert mqtt_client.state.tray_now == 0
|
||||
|
||||
def test_empty_captured_mapping_does_not_override(self, mqtt_client):
|
||||
"""Empty list (defensive — should not happen in practice but
|
||||
all([]) returns True). Treat as no signal, not as all-external."""
|
||||
mqtt_client._captured_ams_mapping = []
|
||||
mqtt_client._process_message(_ams_payload(0))
|
||||
assert mqtt_client.state.tray_now == 0
|
||||
|
||||
def test_unloaded_after_print_still_resolves_correctly(self, mqtt_client):
|
||||
"""When the firmware unloads (tray_now=255), the override is skipped
|
||||
because the branch only fires for tray_now in 0-3."""
|
||||
mqtt_client._captured_ams_mapping = [-1]
|
||||
mqtt_client._process_message(_ams_payload(0))
|
||||
assert mqtt_client.state.tray_now == 254
|
||||
|
||||
mqtt_client._process_message(_ams_payload(255))
|
||||
assert mqtt_client.state.tray_now == 255
|
||||
|
||||
Reference in New Issue
Block a user