Browse Source

fix(ams): detect spool removal on X1C firmware that reports power_on_flag=False (#1365)

  The #765 guard against shutdown-time data wipes skipped any AMS update
  with power_on_flag=False, but some X1C firmware emits power_on_flag=False
  while idle with tray_exist_bits still reflecting the real slot inventory.
  Older firmware (01.08.02.00) doesn't emit per-tray state=9/10 events, so
  the bitfield path is the only signal — muting it left spool removals
  undetected until a manual reconnect.

  Narrow the skip to the exact shutdown pattern: zero bits AND
  power_on_flag=False. Non-zero bits with power_on_flag=False are now
  applied. The #765 shutdown protection is preserved (its regression test
  uses tray_exist_bits='0' and still passes); newer firmwares are
  unaffected because their per-tray state path catches the removal first.
maziggy 1 week ago
parent
commit
7aa5ff0156
3 changed files with 75 additions and 5 deletions
  1. 0 0
      CHANGELOG.md
  2. 13 5
      backend/app/services/bambu_mqtt.py
  3. 62 0
      backend/tests/unit/services/test_bambu_mqtt.py

File diff suppressed because it is too large
+ 0 - 0
CHANGELOG.md


+ 13 - 5
backend/app/services/bambu_mqtt.py

@@ -1714,13 +1714,23 @@ class BambuMQTTClient:
         # Check tray_exist_bits to clear empty slots (Issue #147)
         # New AMS models don't send empty tray data - they just update tray_exist_bits
         # Each bit in tray_exist_bits represents a slot: bit=0 means empty, bit=1 means has spool
-        # Skip when power_on_flag=False: printer shutdown sends all-zero bits which would
-        # wipe all slot data and cause auto-unlink to remove spool assignments (#765)
+        # Skip ONLY the printer-shutdown pattern: all-zero bits paired with
+        # power_on_flag=False (#765). On shutdown that combination would wipe all
+        # slot data and cause auto-unlink to remove spool assignments. Non-zero
+        # bits with power_on_flag=False are valid AMS state from an idle printer
+        # (#1365 — X1C reports power_on_flag=False between prints while the AMS
+        # keeps reporting its actual slot inventory); the update MUST be applied
+        # so spool removal is detected without requiring a manual reconnect.
         tray_exist_bits_str = ams_data.get("tray_exist_bits") if isinstance(ams_data, dict) else None
         power_on = ams_data.get("power_on_flag", True) if isinstance(ams_data, dict) else True
-        if tray_exist_bits_str and power_on:
+        if tray_exist_bits_str:
             try:
                 tray_exist_bits = int(tray_exist_bits_str, 16)
+            except (ValueError, TypeError) as e:
+                logger.debug("[%s] Could not parse tray_exist_bits: %s", self.serial_number, e)
+                tray_exist_bits = None
+
+            if tray_exist_bits is not None and not (tray_exist_bits == 0 and not power_on):
                 for ams_unit in merged_ams:
                     ams_id_raw = ams_unit.get("id")
                     if ams_id_raw is None:
@@ -1752,8 +1762,6 @@ class BambuMQTTClient:
                             tray["tray_uuid"] = "00000000000000000000000000000000"
                             tray["tray_info_idx"] = ""
                             tray["remain"] = 0
-            except (ValueError, TypeError) as e:
-                logger.debug("[%s] Could not parse tray_exist_bits: %s", self.serial_number, e)
 
         self.state.raw_data["ams"] = merged_ams
 

+ 62 - 0
backend/tests/unit/services/test_bambu_mqtt.py

@@ -873,6 +873,68 @@ class TestAMSDataMerging:
             "Without power_on_flag, clearing should proceed (defaults to True)"
         )
 
+    def test_idle_printer_with_power_off_and_nonzero_bits_clears_removed_slot(self, mqtt_client):
+        """Spool removal on an idle X1C must be detected even when power_on_flag=False (#1365).
+
+        On some X1C firmware (e.g. 01.08.02.00 reported by an3k) the AMS keeps
+        publishing push_status with `power_on_flag: False` while the printer
+        sits idle between prints — but `tray_exist_bits` continues to reflect
+        the real slot inventory. The original #765 guard skipped clearing
+        whenever power_on_flag was false, so the bit transition that would
+        mark a slot empty was discarded and the only way to refresh state
+        was a manual reconnect (pushall). The guard now skips clearing only
+        on the exact shutdown pattern (zero bits + power_on_flag=False).
+        """
+        # Initial state: two AMS units, slot 1 of AMS 0 loaded (the one
+        # we'll later remove).
+        initial_ams = {
+            "ams": [
+                {
+                    "id": 0,
+                    "tray": [
+                        {"id": 0, "tray_type": "PLA", "tray_color": "FF0000FF", "remain": 80},
+                        {"id": 1, "tray_type": "PETG", "tray_color": "00FF00FF", "remain": 60},
+                    ],
+                },
+                {
+                    "id": 1,
+                    "tray": [
+                        {"id": 0, "tray_type": "PETG", "tray_color": "DBDDD9FF", "remain": 90},
+                    ],
+                },
+            ],
+            "tray_exist_bits": "13",  # 0b00010011 — AMS0 slots 0+1, AMS1 slot 0
+            "power_on_flag": True,
+        }
+        mqtt_client._handle_ams_data(initial_ams)
+        assert mqtt_client.state.raw_data["ams"][0]["tray"][1]["tray_type"] == "PETG"
+
+        # Spool pulled from AMS 0 slot 1 while the printer is idle.
+        # tray_exist_bits goes from 0x13 -> 0x11, but firmware still reports
+        # power_on_flag=False because the printer is between prints. The real
+        # push_status payloads on the affected X1C still carry the full `ams`
+        # list (matches the bug-report log) — the slot inventory shrinks via
+        # the bitfield rather than via per-tray content updates.
+        removal_ams = {
+            "ams": [
+                {"id": 0, "tray": [{"id": 0}, {"id": 1}]},
+                {"id": 1, "tray": [{"id": 0}]},
+            ],
+            "tray_exist_bits": "11",  # 0b00010001 — slot 1 now empty
+            "power_on_flag": False,
+            "insert_flag": True,
+        }
+        mqtt_client._handle_ams_data(removal_ams)
+
+        ams_data = mqtt_client.state.raw_data["ams"]
+        assert ams_data[0]["tray"][1]["tray_type"] == "", (
+            "Removal must be detected even with power_on_flag=False when bits are non-zero (#1365)"
+        )
+        assert ams_data[0]["tray"][1]["tray_color"] == "", "Removed slot color must be cleared"
+        # Other slots untouched.
+        assert ams_data[0]["tray"][0]["tray_type"] == "PLA", "AMS0 slot 0 preserved"
+        assert ams_data[1]["tray"][0]["tray_type"] == "PETG", "AMS1 slot 0 preserved"
+
 
 class TestAMSTrayStateClearning:
     """Tests for AMS tray state-based clearing (#784).

Some files were not shown because too many files changed in this diff