소스 검색

fix(printers): live updates carry the airduct (cooling/heating) mode, also when it is the only change on an idle printer, and each H2C rack slot's filament type; the recovered-3MF Spoolman tests no longer depend on plate ids another test left behind

maziggy 2 일 전
부모
커밋
808e461c52

+ 2 - 0
CHANGELOG.md

@@ -164,6 +164,8 @@ All notable changes to Bambuddy will be documented in this file.
 - **The frontend build no longer warns about `path` and `crypto` being externalized for the STEP previewer (#2976)** — `occt-import-js`, the Emscripten build behind STEP previews, requires both modules, but only inside its `ENVIRONMENT_IS_NODE` branches; in the browser it loads its `.wasm` from the URL the preview worker passes and draws randomness from `crypto.getRandomValues`. Vite still externalized both and printed two warnings on every build. `vite.config.ts` now drops exactly those two warnings for that one package through `build.rolldownOptions.onLog`, so an externalization anywhere else, or of any other module, still shows.
 
 ### Fixed
+- **Printer card: the cooling/heating mode switched on the printer showed the old mode** — The live updates did not carry it, so the card caught up only with its next refresh.
+- **Printer card: an H2C rack slot's material label disappeared** — The live updates replaced the rack without each slot's filament type, so the label fell back to the filament ID or went blank until the next refresh.
 - **Git backup: a print running when the backup was made came back as a failed print** — Restored as still printing, the archive was closed on the next printer reconnect as if the print had just failed, with a failure notification and a Print Log entry. It is now restored as cancelled ("No status update received"), the way Bambuddy closes a print it lost track of; an archive this instance still has keeps its own status on overwrite.
 - **Git backup: spools came back as never used** — When each spool was last used was not in the backup. It is now backed up and restored (an older backup leaves the value already there).
 - **File manager: a slow folder listing showed as "printer unavailable"** — Listing a large folder (such as the camera recordings) on a slow SD card can outlast the wait, and was reported like an unreachable printer; every refresh also started another listing while the last one still ran. It now says the listing is taking too long, and a refresh waits for the listing already running.

+ 3 - 0
backend/app/main.py

@@ -2061,6 +2061,9 @@ async def on_printer_status_change(printer_id: int, state: PrinterState):
         state.firmware_version,
         state.developer_mode,
         tuple(state.print_ams_mapping) if isinstance(state.print_ams_mapping, list) else None,
+        # Cooling/heating, switched on the printer's screen too: on an idle
+        # printer nothing else moves the key (PSF-4).
+        state.airduct_mode,
     )
     status_key = f"{status_key}:{machine_key}"
 

+ 6 - 0
backend/app/services/printer_manager.py

@@ -1818,6 +1818,9 @@ def printer_state_to_dict(
         "active_extruder": state.active_extruder,
         # Print speed mode (1=silent, 2=standard, 3=sport, 4=ludicrous)
         "speed_level": state.speed_level,
+        # Switched on the printer's screen too; REST-only, the card showed the
+        # old mode until the next poll (PSF-4).
+        "airduct_mode": state.airduct_mode,
         # H2C nozzle rack (tool-changer dock positions)
         # Map raw MQTT field names (type/diameter) to schema names (nozzle_type/nozzle_diameter)
         "nozzle_rack": [
@@ -1831,6 +1834,9 @@ def printer_state_to_dict(
                 "serial_number": n.get("serial_number", ""),
                 "filament_color": n.get("filament_color", ""),
                 "filament_id": n.get("filament_id", ""),
+                # The push replaces the whole rack: without it a slot lost its
+                # material label on every push (PSF-3).
+                "filament_type": n.get("filament_type", ""),
             }
             for n in (state.nozzle_rack or [])
         ],

+ 5 - 0
backend/tests/unit/test_printer_kill_switch.py

@@ -97,6 +97,7 @@ async def test_unauthorized_active_print_triggers_stop(monkeypatch):
         ams_filament_backup=False,
         firmware_version=None,
         developer_mode=None,
+        airduct_mode=0,
         print_ams_mapping=None,
         current_print=None,
         subtask_name="foreign_job",
@@ -195,6 +196,7 @@ async def test_bambuddy_authorized_print_is_not_stopped(monkeypatch):
         ams_filament_backup=False,
         firmware_version=None,
         developer_mode=None,
+        airduct_mode=0,
         print_ams_mapping=None,
         current_print=None,
         subtask_name="foreign_job",
@@ -283,6 +285,7 @@ async def test_unauthorized_print_state_is_cleared_when_print_ends(monkeypatch):
         ams_filament_backup=False,
         firmware_version=None,
         developer_mode=None,
+        airduct_mode=0,
         print_ams_mapping=None,
         current_print=None,
         subtask_name="foreign_job",
@@ -316,6 +319,7 @@ async def test_unauthorized_print_state_is_cleared_when_print_ends(monkeypatch):
         ams_filament_backup=False,
         firmware_version=None,
         developer_mode=None,
+        airduct_mode=0,
         print_ams_mapping=None,
         current_print=None,
         subtask_name="",
@@ -403,6 +407,7 @@ async def test_persisted_print_is_authorized_after_restart(monkeypatch, printer_
         ams_filament_backup=False,
         firmware_version=None,
         developer_mode=None,
+        airduct_mode=0,
         print_ams_mapping=None,
         current_print=None,
         subtask_name="owned_job",

+ 1 - 0
backend/tests/unit/test_printer_manager_status_broadcast.py

@@ -95,6 +95,7 @@ def _fake_state(**overrides):
         "layer_num": None,
         "remaining_time": None,
         "speed_level": None,
+        "airduct_mode": None,
         "stg_cur": 0,  # get_derived_status_name does ``0 <= state.stg_cur < 255``
         "subtask_name": None,
         "total_layers": None,

+ 1 - 0
backend/tests/unit/test_printer_offline_notification.py

@@ -73,6 +73,7 @@ def _state(connected: bool, state: str = "IDLE") -> SimpleNamespace:
         ams_filament_backup=None,
         firmware_version=None,
         developer_mode=None,
+        airduct_mode=0,
         print_ams_mapping=None,
     )
 

+ 3 - 0
backend/tests/unit/test_printer_status_fields_g6.py

@@ -73,6 +73,7 @@ def _push_state(**overrides):
         "firmware_version": "01.09.00.00",
         "developer_mode": False,
         "print_ams_mapping": None,
+        "airduct_mode": 0,
     }
     fields.update(overrides)
     return SimpleNamespace(**fields)
@@ -117,6 +118,8 @@ class TestAChangeAloneIsPushed:
         [
             {"developer_mode": True},
             {"firmware_version": "01.10.00.00"},
+            # Cooling/heating switched on the printer's screen (PSF-4).
+            {"airduct_mode": 1},
             {"nozzles": [NozzleInfo(nozzle_type="stainless_steel", nozzle_diameter="0.6")]},
             {"print_ams_mapping": [0, 2]},
         ],

+ 7 - 1
backend/tests/unit/test_spoolman_recovered_3mf_tracking.py

@@ -63,7 +63,13 @@ async def seeded(test_engine, tmp_path):
     from backend.app import main as main_module
 
     maker, printer_id, archive_id = await _seed(test_engine, tmp_path)
-    with patch.dict(main_module._active_prints, {(printer_id, DISPATCH_FILENAME): archive_id}, clear=True):
+    # Empty, not whatever a test before on this worker left: the recovery reads
+    # the printed plate from it, and a plate the 3MF has no usage for tracks
+    # nothing.
+    with (
+        patch.dict(main_module._active_prints, {(printer_id, DISPATCH_FILENAME): archive_id}, clear=True),
+        patch.dict(main_module._print_plate_ids, {}, clear=True),
+    ):
         yield maker, printer_id, archive_id
 
 

+ 1 - 0
backend/tests/unit/test_status_broadcast_ams_slot_config.py

@@ -92,6 +92,7 @@ def _state(trays: list[dict], ams_extra: dict | None = None) -> SimpleNamespace:
         firmware_version=None,
         developer_mode=None,
         print_ams_mapping=None,
+        airduct_mode=0,
     )
 
 

+ 27 - 0
backend/tests/unit/test_ws_status_carries_rest_fields.py

@@ -0,0 +1,27 @@
+"""The live status push carries what the printer card reads from the status (PSF-3, PSF-4).
+
+The websocket push replaces the fields it carries in the card's cached status.
+``airduct_mode`` was missing, so switching cooling/heating on the printer's
+screen showed the old mode until the next REST poll; ``nozzle_rack[]`` lacked
+``filament_type``, and since the push replaces the whole rack an H2C slot
+lost its material label on every push.
+"""
+
+import backend.app.core.database  # noqa: F401  -- model import order
+from backend.app.services.bambu_mqtt import PrinterState
+from backend.app.services.printer_manager import printer_state_to_dict
+
+
+def test_the_airduct_mode_is_pushed():
+    state = PrinterState()
+    state.airduct_mode = 1
+
+    assert printer_state_to_dict(state)["airduct_mode"] == 1
+
+
+def test_a_rack_slot_keeps_its_material():
+    state = PrinterState()
+    state.nozzle_rack = [{"id": 2, "type": "HS", "diameter": "0.4", "filament_id": "GFL99", "filament_type": "PLA"}]
+
+    [slot] = printer_state_to_dict(state)["nozzle_rack"]
+    assert slot["filament_type"] == "PLA"