fix: H2D tray_now disambiguation produces bogus IDs for AMS-HT (#364)

When the snow field hadn't arrived yet, the ams_extruder_map fallback
computed ams_id * 4 + slot for AMS-HT units (128-135), producing
invalid tray IDs like 512+. AMS-HT has a single slot; global ID = unit
ID.

- Single-AMS fallback: return ams_id directly for AMS-HT (128-135)
- Multi-AMS matching: decompose AMS-HT trays correctly (slot=0)
- Multi-AMS narrowing: slot > 0 eliminates AMS-HT candidates
- last_loaded_tray: tighten to valid IDs only (0-15, 128-135, 254)
- Add 10 tests for AMS-HT fallback and last_loaded_tray validation
This commit is contained in:
maziggy
2026-02-19 11:32:42 +01:00
parent d24e7cdf84
commit c856badb84
3 changed files with 127 additions and 11 deletions
+40 -11
View File
@@ -990,29 +990,56 @@ class BambuMQTTClient:
if len(ams_on_extruder) == 1:
# Single AMS on this extruder - unambiguous
active_ams_id = ams_on_extruder[0]
global_tray_id = active_ams_id * 4 + parsed_tray_now
if 128 <= active_ams_id <= 135:
# AMS-HT: single slot per unit, global ID = unit ID
global_tray_id = active_ams_id
else:
global_tray_id = active_ams_id * 4 + parsed_tray_now
logger.debug(
f"[{self.serial_number}] H2D tray_now fallback: "
f"slot {parsed_tray_now} + single AMS {active_ams_id} -> global ID {global_tray_id}"
)
self.state.tray_now = global_tray_id
elif len(ams_on_extruder) > 1:
# Multiple AMS on this extruder - keep current if valid, else use slot as-is
# Multiple AMS on this extruder - keep current if valid, else try to narrow down
current_tray = self.state.tray_now
current_ams = current_tray // 4 if current_tray < 128 else -1
if current_ams in ams_on_extruder and (current_tray % 4) == parsed_tray_now:
# Determine which AMS unit and slot the current tray belongs to
if 0 <= current_tray <= 15:
current_ams = current_tray // 4
current_slot = current_tray % 4
elif 128 <= current_tray <= 135:
current_ams = current_tray # AMS-HT: ID = tray ID
current_slot = 0
else:
current_ams = -1
current_slot = -1
if current_ams in ams_on_extruder and current_slot == parsed_tray_now:
# Current is valid and matches slot - keep it
logger.debug(
f"[{self.serial_number}] H2D tray_now: multiple AMS {ams_on_extruder}, "
f"keeping current {current_tray} (matches slot {parsed_tray_now})"
)
else:
# Can't disambiguate - use slot as-is (will be wrong for non-first AMS)
logger.warning(
f"[{self.serial_number}] H2D tray_now: multiple AMS {ams_on_extruder} on extruder {active_ext}, "
f"no snow field, using slot {parsed_tray_now} (may be incorrect)"
)
self.state.tray_now = parsed_tray_now
# Filter candidates: AMS-HT (128-135) only valid for slot 0
if parsed_tray_now > 0:
candidates = [a for a in ams_on_extruder if a <= 3]
else:
candidates = ams_on_extruder
if len(candidates) == 1:
cand = candidates[0]
resolved = cand if 128 <= cand <= 135 else cand * 4 + parsed_tray_now
logger.debug(
f"[{self.serial_number}] H2D tray_now: multiple AMS {ams_on_extruder}, "
f"narrowed to AMS {cand} -> global ID {resolved}"
)
self.state.tray_now = resolved
else:
# Genuinely ambiguous - use slot as-is (will be wrong for non-first AMS)
logger.warning(
f"[{self.serial_number}] H2D tray_now: multiple AMS {ams_on_extruder} on extruder {active_ext}, "
f"no snow field, using slot {parsed_tray_now} (may be incorrect)"
)
self.state.tray_now = parsed_tray_now
else:
# No AMS on this extruder - use slot as-is
logger.warning(
@@ -1029,7 +1056,9 @@ class BambuMQTTClient:
self.state.tray_now = parsed_tray_now
# Track last valid tray for usage tracking (survives retract → 255 at print end)
if 0 <= self.state.tray_now <= 253:
# Valid physical trays: 0-15 (regular AMS), 128-135 (AMS-HT), 254 (external spool)
tn = self.state.tray_now
if (0 <= tn <= 15) or (128 <= tn <= 135) or tn == 254:
self.state.last_loaded_tray = self.state.tray_now
logger.debug("[%s] tray_now updated: %s", self.serial_number, self.state.tray_now)
@@ -1632,6 +1632,92 @@ class TestTrayNowDualNozzleH2DFallback(_H2DFixtureMixin):
h2d_client._process_message(_ams_payload(2))
assert h2d_client.state.tray_now == 2
def test_single_ams_ht_on_extruder_returns_unit_id(self, h2d_client):
"""AMS-HT 128 alone on left extruder, slot 0 → global ID 128 (not 512)."""
# Switch to left extruder (where AMS-HT 128 is mapped)
h2d_client._process_message(_extruder_state_payload(0x0100))
# Only AMS-HT 128 on left extruder; no snow available
h2d_client._process_message(_ams_payload(0))
assert h2d_client.state.tray_now == 128
def test_single_ams_ht_ignores_nonzero_slot(self, h2d_client):
"""AMS-HT has single slot; even if printer reports slot 1, global ID = unit ID."""
h2d_client.state.ams_extruder_map = {"129": 0}
h2d_client._process_message(_ams_payload(1))
# AMS-HT 129: global ID = 129, not 129*4+1=517
assert h2d_client.state.tray_now == 129
def test_multiple_ams_keeps_current_ams_ht(self, h2d_client):
"""Current tray is AMS-HT 128, slot 0 reported → keeps 128."""
h2d_client.state.ams_extruder_map = {"0": 0, "128": 0}
h2d_client.state.tray_now = 128
h2d_client._process_message(_ams_payload(0))
assert h2d_client.state.tray_now == 128
def test_multiple_ams_slot_nonzero_excludes_ams_ht(self, h2d_client):
"""Slot > 0 eliminates AMS-HT candidates; single regular AMS left → resolves."""
# AMS 0 + AMS-HT 128 both on right extruder
h2d_client.state.ams_extruder_map = {"0": 0, "128": 0}
h2d_client.state.tray_now = 255 # no current match
# Slot 2 → can't be AMS-HT → only AMS 0 → global = 0*4+2 = 2
h2d_client._process_message(_ams_payload(2))
assert h2d_client.state.tray_now == 2
def test_multiple_ams_slot_nonzero_narrows_to_single_ht_excluded(self, h2d_client):
"""Two regular AMS + one AMS-HT, slot > 0 → AMS-HT excluded but still ambiguous."""
h2d_client.state.ams_extruder_map = {"0": 0, "1": 0, "128": 0}
h2d_client.state.tray_now = 255
# Slot 3 → excludes AMS-HT, but AMS 0 and AMS 1 both remain → ambiguous
h2d_client._process_message(_ams_payload(3))
assert h2d_client.state.tray_now == 3 # raw slot fallback
# ---------------------------------------------------------------------------
# 6b. H2D last_loaded_tray validation
# ---------------------------------------------------------------------------
class TestLastLoadedTrayValidation(_H2DFixtureMixin):
"""last_loaded_tray only stores physically valid tray IDs."""
def test_regular_ams_tray_stored(self, h2d_client):
"""Valid regular AMS tray (0-15) → stored in last_loaded_tray."""
h2d_client.state.tray_now = 7
# Trigger tray_now processing via AMS message
h2d_client._process_message(
_extruder_info_payload(
[
{"id": 0, "snow": 1 << 8 | 3}, # AMS 1 slot 3 → global 7
{"id": 1, "snow": 0xFF00FF},
]
)
)
h2d_client._process_message(_ams_payload(3))
assert h2d_client.state.tray_now == 7
assert h2d_client.state.last_loaded_tray == 7
def test_ams_ht_tray_stored(self, h2d_client):
"""Valid AMS-HT tray (128-135) → stored in last_loaded_tray."""
h2d_client._process_message(_extruder_state_payload(0x0100))
h2d_client._process_message(
_extruder_info_payload(
[
{"id": 0, "snow": 0xFF00FF},
{"id": 1, "snow": 128 << 8 | 0},
]
)
)
h2d_client._process_message(_ams_payload(0))
assert h2d_client.state.tray_now == 128
assert h2d_client.state.last_loaded_tray == 128
def test_unloaded_not_stored(self, h2d_client):
"""tray_now=255 (unloaded) → last_loaded_tray unchanged."""
h2d_client.state.last_loaded_tray = 5
h2d_client._process_message(_ams_payload(255))
assert h2d_client.state.tray_now == 255
assert h2d_client.state.last_loaded_tray == 5
# ---------------------------------------------------------------------------
# 7. H2D Active extruder switching