Files
maziggy 905bda4f3f fix(ams): read the firmware presence bit, not the tray state (issue #3084)
Swapping a Bambu spool for one the AMS cannot read left Assign Spool
publishing no ams_filament_setting at all. The printer kept showing "?"
on its screen and in the slicer, and only Configure, which publishes
unconditionally, put anything there.

Four places asked the tray's `state` field whether a spool was in the
slot. It cannot answer that. An AMS-HT reports its LOADED tray as 9
rather than 11, because it does not feed into a shared buffer the way a
4-slot AMS does -- the merge has skipped its own state heuristic for HT
units since #2594 for exactly this reason. And the field is partly our
own writing: apply_tray_exist_bits stamps state=9 on every slot whose
tray_exist_bits bit is 0, and when the bit comes back it refreshes only
the `exists` annotation beside it. Either way the slot sits at
exists=True, state=9 until something configures it.

That 9 also kept the deferred-configuration replay from firing -- its
own "has a spool appeared" test was the same heuristic -- which is the
deadlock #1322 removed from the assign path, still in place one step
further along. And it is what deleted the assignments in #3100: with the
replay never firing, the row kept the empty fingerprint it was stored
with, and the first tray report naming a filament was read as a swap.

All four now read tray_exist_bits first, which is the mask firmware
answers this question with and the one the printer card has drawn its
"?" from since #2527. The bit is allowed to overrule an "empty" state
and nothing else: a bit reading empty deliberately does not start
suppressing pushes that go out today, because the cost of computing a
bit position wrong is a slot that silently stops configuring, against a
saving of one message firmware would have dropped.

A blank tray report from a slot the bit calls occupied no longer unlinks
anything, off a print as well as during one, in both inventory modes --
Spoolman's parse_ams_tray calls a tray with no type empty, so a tag-less
spool assigned through the UI had its row deleted by the first idle push
after it went in. A filament the AMS cannot identify is not a filament
that was removed.
2026-09-20 11:13:20 +02:00

46 lines
2.2 KiB
Python

"""Unit tests for the firmware presence bit helper.
#3084: a slot can read ``exists=True, state=9`` — the bit says a spool is in
it, the state says the opposite — because ``apply_tray_exist_bits`` writes the
9 itself and never takes it back. Everything downstream has to know which of
the two to believe, and this helper is the single place that says so.
"""
from backend.app.services.ams_slot_presence import spool_present
class TestSpoolPresent:
def test_the_bit_is_reported_as_it_stands(self):
assert spool_present({"id": 0, "exists": True}) is True
assert spool_present({"id": 0, "exists": False}) is False
def test_the_bit_answers_over_a_contradicting_state(self):
# The #3084 slot: non-Bambu spool swapped in, so the bit is set, while
# the 9 Bambuddy stamped on the slot when it was briefly empty is still
# sitting there.
assert spool_present({"id": 0, "exists": True, "state": 9}) is True
# And the converse — a spool pulled from a slot the firmware last
# described as loaded.
assert spool_present({"id": 0, "exists": False, "state": 11}) is False
def test_a_tray_with_no_annotation_answers_nothing(self):
# vt_tray entries and the VP bridge's cache carry no presence bit, so
# callers have to fall back to their own reading rather than be handed
# a guess dressed up as firmware's answer.
assert spool_present({"id": 0, "state": 11, "tray_type": "PLA"}) is None
assert spool_present({}) is None
def test_a_non_bool_exists_is_not_a_presence_bit(self):
# Only apply_tray_exist_bits writes this key, and it writes a bool.
# Anything else reached the dict some other way and is not firmware's
# answer — 0 and "" would otherwise read as a confident "empty".
assert spool_present({"exists": 0}) is None
assert spool_present({"exists": ""}) is None
assert spool_present({"exists": "true"}) is None
assert spool_present({"exists": None}) is None
def test_a_missing_tray_answers_nothing(self):
assert spool_present(None) is None
assert spool_present([]) is None
assert spool_present("tray") is None