Просмотр исходного кода

Clear a print_error fault once the printer clears it (#3487)

- A fault reported through print_error (such as the A1 mini's
  filament-load prompt) stayed listed after the printer set the field
  back to 0, until "Clear errors" was clicked
- The print_error entry is now removed when the code goes to 0, and a
  different code replaces it; the same code keeps its entry and job id
- An hms[] update without print_error keeps a still-active print_error
  fault instead of dropping it, and lists it once if hms[] carries it
- Short-code dedupe moved into helpers shared by both paths
maziggy 3 дней назад
Родитель
Сommit
e0955820fc
3 измененных файлов с 81 добавлено и 11 удалено
  1. 1 0
      CHANGELOG.md
  2. 33 11
      backend/app/services/bambu_mqtt.py
  3. 47 0
      backend/tests/unit/services/test_bambu_mqtt.py

+ 1 - 0
CHANGELOG.md

@@ -13,6 +13,7 @@ All notable changes to Bambuddy will be documented in this file.
 - **"Any model" jobs go to the printer with the fewest print hours (#3480, sponsor vote Q4 2026)** — When several printers of a model can take a job, the queue used to pick the first one in its list, so on a farm the same printer got most of the work. It now picks the one with the fewest print hours (the Maintenance counter, including baseline hours entered there), so jobs and wear spread across the farm. Preferred colours still come first, and the queue powers on the least used printer too when it has to switch one on with an Auto Power On plug.
 - **"Any model" jobs go to the printer with the fewest print hours (#3480, sponsor vote Q4 2026)** — When several printers of a model can take a job, the queue used to pick the first one in its list, so on a farm the same printer got most of the work. It now picks the one with the fewest print hours (the Maintenance counter, including baseline hours entered there), so jobs and wear spread across the farm. Preferred colours still come first, and the queue powers on the least used printer too when it has to switch one on with an Auto Power On plug.
 
 
 ### Fixed
 ### Fixed
+- **A printer prompt answered on the printer leaves the HMS list (#3487)** — Some faults, such as the A1 mini's "is the filament extruded?" prompt while loading filament, come in the printer's print_error field rather than its HMS list. Bambuddy showed them with the HMS errors but did not notice when the printer set that field back to 0, so the entry stayed on the printer card until "Clear errors" was clicked. It now goes as soon as the printer clears it, and a newer code replaces the old one. An HMS list update that arrives while such a fault is still active no longer drops it either.
 - **Failed and cancelled prints get their timelapse too (#3481)** — The printer saves the timelapse of a print that fails or is cancelled, up to where it stopped, and that video is the best way to see what went wrong. Bambuddy fetched timelapses only for prints that completed, so a stopped print's video stayed on the printer until it was attached by hand with **Scan for Timelapse**. It is now fetched and attached to that print's run like any other. Wiki updated.
 - **Failed and cancelled prints get their timelapse too (#3481)** — The printer saves the timelapse of a print that fails or is cancelled, up to where it stopped, and that video is the best way to see what went wrong. Bambuddy fetched timelapses only for prints that completed, so a stopped print's video stayed on the printer until it was attached by hand with **Scan for Timelapse**. It is now fetched and attached to that print's run like any other. Wiki updated.
 - **A printer named after its model no longer hides the model in the support package (#3442, reported by @daltonclift)** — The support package replaces printer names with [PRINTER]. A printer named "H3C" turned every "H3C" in the package into [PRINTER], including the model itself. A name that is only a model name or model code is not private, so it now stays; any other name is still replaced.
 - **A printer named after its model no longer hides the model in the support package (#3442, reported by @daltonclift)** — The support package replaces printer names with [PRINTER]. A printer named "H3C" turned every "H3C" in the package into [PRINTER], including the model itself. A name that is only a model name or model code is not private, so it now stays; any other name is still replaced.
 - **Auto-drying no longer waits on a drying timer the printer never started (#3478, reported by @superflyer11)** — An AMS HT can accept a drying command while it feeds a print and never heat: its timer stays at the full time (12h 0m) and its status reads Off, for days. Since 1.3.1b1 the printer card shows such a timer as not running, but auto-drying still took it for a running cycle and never dried that unit again, while the humidity alarms kept firing. A timer that has not moved for a few minutes with the status Off now counts as not drying, so auto-drying tries again after its usual 30-minute pause. If that happens twice in a row, auto-drying stops on that unit and sends the **Auto-Drying Suspended** notification, saying the timer stood still (new template **Auto-Drying Not Heating**). It resumes once the humidity is back at or below the threshold. Wiki updated.
 - **Auto-drying no longer waits on a drying timer the printer never started (#3478, reported by @superflyer11)** — An AMS HT can accept a drying command while it feeds a print and never heat: its timer stays at the full time (12h 0m) and its status reads Off, for days. Since 1.3.1b1 the printer card shows such a timer as not running, but auto-drying still took it for a running cycle and never dried that unit again, while the humidity alarms kept firing. A timer that has not moved for a few minutes with the status Off now counts as not drying, so auto-drying tries again after its usual 30-minute pause. If that happens twice in a row, auto-drying stops on that unit and sends the **Auto-Drying Suspended** notification, saying the timer stood still (new template **Auto-Drying Not Heating**). It resumes once the humidity is back at or below the threshold. Wiki updated.

+ 33 - 11
backend/app/services/bambu_mqtt.py

@@ -836,6 +836,26 @@ class HMSError:
         }
         }
 
 
 
 
+def _is_print_error_entry(error: HMSError) -> bool:
+    """Whether this entry came from the 32-bit `print_error` field, whose
+    full code has 8 hex digits; an `hms[]` entry's has 16."""
+    return len(error.full_code) == 8
+
+
+def _hms_short_code(error: HMSError) -> str:
+    """The MMMM_EEEE short code of an entry from either source."""
+    module = (error.attr >> 16) & 0xFFFF
+    # The low half only: an hms[] code carries its alert level above it
+    # (0x18061 for 0500_8061), and read whole it never matched, listing the
+    # fault twice (M-U9).
+    number = (int(error.code.replace("0x", ""), 16) & 0xFFFF) if error.code else 0
+    return f"{module:04X}_{number:04X}"
+
+
+def _hms_short_codes(errors: list) -> set[str]:
+    return {_hms_short_code(e) for e in errors}
+
+
 # HMS short codes the firmware emits during normal user-cancel sequences.
 # HMS short codes the firmware emits during normal user-cancel sequences.
 # These aren't faults — they're status echoes that confirm the cancel happened.
 # These aren't faults — they're status echoes that confirm the cancel happened.
 # Filtering them at parse-time keeps them out of state.hms_errors entirely,
 # Filtering them at parse-time keeps them out of state.hms_errors entirely,
@@ -5298,6 +5318,9 @@ class BambuMQTTClient:
         if "hms" in data:
         if "hms" in data:
             hms_list = data["hms"]
             hms_list = data["hms"]
             logger.debug("[%s] HMS data received: %s", self.serial_number, hms_list)
             logger.debug("[%s] HMS data received: %s", self.serial_number, hms_list)
+            # A `print_error` fault is not in `hms[]` and its field only comes
+            # when it changes, so a list rebuilt from `hms[]` keeps it (#3487).
+            print_error_entries = [e for e in self.state.hms_errors if _is_print_error_entry(e)]
             self.state.hms_errors = []
             self.state.hms_errors = []
             verify_failed = False
             verify_failed = False
             if isinstance(hms_list, list):
             if isinstance(hms_list, list):
@@ -5361,6 +5384,8 @@ class BambuMQTTClient:
                                 description=describe_fault(full_code, self.serial_number[:3]),
                                 description=describe_fault(full_code, self.serial_number[:3]),
                             )
                             )
                         )
                         )
+            hms_short_codes = _hms_short_codes(self.state.hms_errors)
+            self.state.hms_errors.extend(e for e in print_error_entries if _hms_short_code(e) not in hms_short_codes)
             self._apply_mqtt_verify_state(verify_failed)
             self._apply_mqtt_verify_state(verify_failed)
 
 
         # Parse print_error - this is a different error format than HMS
         # Parse print_error - this is a different error format than HMS
@@ -5370,6 +5395,13 @@ class BambuMQTTClient:
         # Format on printer screen: [0500-8061] -> short code: 0500_8061
         # Format on printer screen: [0500-8061] -> short code: 0500_8061
         if "print_error" in data:
         if "print_error" in data:
             print_error = data["print_error"]
             print_error = data["print_error"]
+            # The field holds the one current fault, and 0 once it is resolved,
+            # for example when the filament-load prompt is answered on the
+            # printer. The entry of a code that is no longer reported goes
+            # (#3487); the entry of the same code is kept as it is.
+            self.state.hms_errors = [
+                e for e in self.state.hms_errors if not _is_print_error_entry(e) or e.attr == print_error
+            ]
             if print_error and print_error != 0:
             if print_error and print_error != 0:
                 # Extract components: MMMMEEEE -> MMMM_EEEE
                 # Extract components: MMMMEEEE -> MMMM_EEEE
                 module = (print_error >> 16) & 0xFFFF  # High 16 bits (e.g., 0x0500)
                 module = (print_error >> 16) & 0xFFFF  # High 16 bits (e.g., 0x0500)
@@ -5397,17 +5429,7 @@ class BambuMQTTClient:
                         pass  # cancel echo — silently drop
                         pass  # cancel echo — silently drop
                     else:
                     else:
                         # Only add if not already in HMS errors (avoid duplicates)
                         # Only add if not already in HMS errors (avoid duplicates)
-                        existing_short_codes = set()
-                        for e in self.state.hms_errors:
-                            # Extract short code from existing errors
-                            e_module = (e.attr >> 16) & 0xFFFF
-                            # The low half only: an hms[] code carries its alert
-                            # level above it (0x18061 for 0500_8061), and read
-                            # whole it never matched, listing the fault twice (M-U9).
-                            e_error = (int(e.code.replace("0x", ""), 16) & 0xFFFF) if e.code else 0
-                            existing_short_codes.add(f"{e_module:04X}_{e_error:04X}")
-
-                        if short_code not in existing_short_codes:
+                        if short_code not in _hms_short_codes(self.state.hms_errors):
                             # Bambu's HMS catalog keys by 3-letter device code (the SN
                             # Bambu's HMS catalog keys by 3-letter device code (the SN
                             # prefix) and a 16-char short error code without the
                             # prefix) and a 16-char short error code without the
                             # underscore separator we store internally.
                             # underscore separator we store internally.

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

@@ -5586,6 +5586,53 @@ class TestHMSUserActionFiltering:
 
 
         assert len(mqtt_client.state.hms_errors) == 1
         assert len(mqtt_client.state.hms_errors) == 1
 
 
+    def test_a_print_error_that_returns_to_zero_is_removed(self, mqtt_client):
+        """#3487: the A1 mini sets print_error for the filament-load prompt and
+        back to 0 once it is answered on the printer, without an hms[] key.
+        The entry stayed until "Clear errors"."""
+        mqtt_client._update_state({"print_error": 0x07FF_C010})
+        assert [e.full_code for e in mqtt_client.state.hms_errors] == ["07FFC010"]
+
+        mqtt_client._update_state({"print_error": 0})
+
+        assert mqtt_client.state.hms_errors == []
+
+    def test_a_new_print_error_replaces_the_previous_one(self, mqtt_client):
+        mqtt_client._update_state({"print_error": 0x07FF_C010})
+        mqtt_client._update_state({"print_error": 0x0500_8061})
+
+        assert [e.full_code for e in mqtt_client.state.hms_errors] == ["05008061"]
+
+    def test_the_same_print_error_again_keeps_its_entry(self, mqtt_client):
+        """A repeat keeps the entry and the job_id it was frozen with."""
+        mqtt_client.state.subtask_id = "job-1"
+        mqtt_client._update_state({"print_error": 0x0500_8061})
+        entry = mqtt_client.state.hms_errors[0]
+        mqtt_client.state.subtask_id = "job-2"
+
+        mqtt_client._update_state({"print_error": 0x0500_8061})
+
+        assert mqtt_client.state.hms_errors == [entry]
+        assert entry.job_id == "job-1"
+
+    def test_an_hms_list_without_print_error_keeps_the_print_error_fault(self, mqtt_client):
+        """print_error comes only when it changes, so an hms[] update between
+        must not drop a fault that is still active."""
+        mqtt_client._update_state({"print_error": 0x07FF_C010})
+
+        mqtt_client._update_state({"hms": []})
+        assert [e.full_code for e in mqtt_client.state.hms_errors] == ["07FFC010"]
+
+        mqtt_client._update_state({"hms": [{"attr": 0x03000100, "code": 0x00018004}]})
+        assert sorted(e.full_code for e in mqtt_client.state.hms_errors) == ["0300010000018004", "07FFC010"]
+
+    def test_an_hms_list_that_now_carries_the_print_error_fault_lists_it_once(self, mqtt_client):
+        mqtt_client._update_state({"print_error": 0x0500_8061})
+
+        mqtt_client._update_state({"hms": [{"attr": 0x05000100, "code": 0x00018061}]})
+
+        assert [e.full_code for e in mqtt_client.state.hms_errors] == ["0500010000018061"]
+
 
 
 class TestHMSFullCode:
 class TestHMSFullCode:
     """full_code is the firmware-matching key for HMS-related commands.
     """full_code is the firmware-matching key for HMS-related commands.