Browse Source

fix(printers): decode airduct part state from its low 8 bits

Review feedback on #2691: `state` is bit-packed like its sibling `range`
(end << 16 | start), and Bambu Studio decodes it with
get_flag_bits(state, 0, 8). Masking with & 0xFF before clamping means a
packed value decodes to the real percentage instead of clamping to 100.

Also moves the uses_exhaust_fan_label import to the top of printers.py with
the other imports.

Tests: packed value (60 << 16 | 45) decodes to 45, and plain 0-100 values
round-trip unchanged.
Gabe 1 month ago
parent
commit
84a7b797cd

+ 1 - 2
backend/app/api/routes/printers.py

@@ -64,6 +64,7 @@ from backend.app.services.printer_manager import (
 )
 )
 from backend.app.utils.filament_ids import filament_id_to_setting_id
 from backend.app.utils.filament_ids import filament_id_to_setting_id
 from backend.app.utils.http import build_content_disposition
 from backend.app.utils.http import build_content_disposition
+from backend.app.utils.printer_models import uses_exhaust_fan_label
 
 
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
 router = APIRouter(prefix="/printers", tags=["printers"])
 router = APIRouter(prefix="/printers", tags=["printers"])
@@ -3229,8 +3230,6 @@ async def set_fan_speed(
     # The enclosure fan is called "Exhaust" on P2S/X2D and "Chamber" elsewhere;
     # The enclosure fan is called "Exhaust" on P2S/X2D and "Chamber" elsewhere;
     # match whatever the printer card badge shows so the toast agrees with the
     # match whatever the printer card badge shows so the toast agrees with the
     # control the user just clicked.
     # control the user just clicked.
-    from backend.app.utils.printer_models import uses_exhaust_fan_label
-
     fan_names = {
     fan_names = {
         "part": "Part cooling fan",
         "part": "Part cooling fan",
         "aux": "Auxiliary fan",
         "aux": "Auxiliary fan",

+ 7 - 1
backend/app/services/bambu_mqtt.py

@@ -3245,7 +3245,13 @@ class BambuMQTTClient:
                             continue
                             continue
                         try:
                         try:
                             part_id = int(part["id"]) >> 4
                             part_id = int(part["id"]) >> 4
-                            part_state = int(part["state"])
+                            # `state` is bit-packed like its sibling `range`
+                            # (end << 16 | start), so take only the low 8 bits —
+                            # the same decode Bambu Studio does with
+                            # get_flag_bits(state, 0, 8). Without the mask a
+                            # packed value would clamp to 100 instead of
+                            # decoding to the real percentage.
+                            part_state = int(part["state"]) & 0xFF
                         except (KeyError, ValueError, TypeError):
                         except (KeyError, ValueError, TypeError):
                             continue
                             continue
                         if part_id == 10:
                         if part_id == 10:

+ 19 - 0
backend/tests/unit/services/test_p2s_accessory_fans.py

@@ -105,6 +105,25 @@ class TestLeftAuxFanParsing:
         mqtt_client._update_state(_airduct_device(parts))
         mqtt_client._update_state(_airduct_device(parts))
         assert mqtt_client.state.left_aux_fan_speed == 100
         assert mqtt_client.state.left_aux_fan_speed == 100
 
 
+    def test_packed_state_decodes_from_low_8_bits(self, mqtt_client):
+        """`state` is bit-packed like its sibling `range` (end << 16 | start).
+
+        Bambu Studio decodes it with get_flag_bits(state, 0, 8), so only the low
+        byte carries the percentage. Without the mask a packed value would clamp
+        to 100 instead of decoding to the real speed.
+        """
+        packed = (60 << 16) | 45  # sibling field in the high bits, 45% in the low byte
+        parts = [{"func": 5, "id": 160, "range": 6553600, "state": packed, "tar_state": 0}]
+        mqtt_client._update_state(_airduct_device(parts))
+        assert mqtt_client.state.left_aux_fan_speed == 45
+
+    def test_unpacked_state_is_unaffected_by_the_mask(self, mqtt_client):
+        # Plain 0-100 values (what a P2S actually sends) must round-trip exactly.
+        for speed in (0, 30, 80, 100):
+            parts = [{"func": 5, "id": 160, "range": 6553600, "state": speed, "tar_state": speed}]
+            mqtt_client._update_state(_airduct_device(parts))
+            assert mqtt_client.state.left_aux_fan_speed == speed
+
     def test_malformed_part_entries_ignored(self, mqtt_client):
     def test_malformed_part_entries_ignored(self, mqtt_client):
         parts = [
         parts = [
             "not-a-dict",
             "not-a-dict",