Quellcode durchsuchen

fix(scheduler): skip per-nozzle filter under FTS so dispatch feeds the right spool (#2186)

On a dual-nozzle H2C with a Filament Track Switch, a queued print targeting
one nozzle fed a same-type wrong-colour spool: the backend mapping
(_match_filaments_to_slots) hard-filtered candidate trays to the requested
extruder, excluding the correct spool loaded in the other nozzle's AMS — which
the FTS can route across. Confirmed from the reporter's captures: same model
mapped to AMS-A slot 2 (black) on the left nozzle but AMS-B slot 3 (red) on the
right. The #1162 FTS-skip existed only in the frontend mapping, never the
queue-dispatch path.

Read fila_switch.installed in _compute_ams_mapping_for_printer and skip the
per-nozzle filter when an FTS is present. Single-nozzle printers are unaffected
(no nozzle_id in the 3MF, no FTS). Regression tests in TestFtsNozzleBypass.
maziggy vor 2 Monaten
Ursprung
Commit
70312b0a11

Datei-Diff unterdrückt, da er zu groß ist
+ 0 - 20
CHANGELOG.md


+ 14 - 2
backend/app/services/print_scheduler.py

@@ -890,6 +890,14 @@ class PrintScheduler:
             logger.warning("Cannot compute AMS mapping: printer %s status unavailable", printer_id)
             return None
 
+        # Filament Track Switch (FTS): when installed it routes any AMS slot to
+        # either extruder, so the per-nozzle hard filter below must NOT apply.
+        # Otherwise a print on one nozzle can't use a spool physically loaded in
+        # an AMS on the *other* nozzle, and the matcher falls through to a
+        # same-type wrong-colour spool on the target nozzle — the H2C + FTS
+        # wrong-filament bug (#2186). Mirrors the frontend skip added for #1162.
+        fts_installed = bool(getattr(getattr(status, "fila_switch", None), "installed", False))
+
         # Get filament requirements from source file
         filament_reqs = await self._get_filament_requirements(db, item)
         if not filament_reqs:
@@ -964,7 +972,7 @@ class PrintScheduler:
 
         # Compute mapping: match required filaments to available slots
         return self._match_filaments_to_slots(
-            filament_reqs, loaded_filaments, prefer_lowest, inventory_remain_overrides
+            filament_reqs, loaded_filaments, prefer_lowest, inventory_remain_overrides, fts_installed
         )
 
     def _build_override_direct_mapping(self, force_overrides: list[dict], status) -> list[int] | None:
@@ -1274,6 +1282,7 @@ class PrintScheduler:
         loaded: list[dict],
         prefer_lowest: bool = False,
         inventory_remain_overrides: dict[int, float] | None = None,
+        fts_installed: bool = False,
     ) -> list[int] | None:
         """Match required filaments to loaded filaments and build AMS mapping.
 
@@ -1316,8 +1325,11 @@ class PrintScheduler:
             # Nozzle-aware filtering: restrict to trays on the correct nozzle.
             # Hard filter — cross-nozzle assignment causes print failures
             # ("position of left hotend is abnormal"), so never fall back.
+            # Skipped when an FTS is installed: it routes any AMS slot to either
+            # extruder, so restricting to one nozzle would wrongly exclude the
+            # correct spool sitting in the other nozzle's AMS (#2186).
             req_nozzle_id = req.get("nozzle_id")
-            if req_nozzle_id is not None:
+            if req_nozzle_id is not None and not fts_installed:
                 available = [f for f in available if f.get("extruder_id") == req_nozzle_id]
 
             # Sort by remaining filament (ascending) so lowest-remain spool wins .find().

+ 47 - 0
backend/tests/unit/test_scheduler_ams_mapping.py

@@ -1501,3 +1501,50 @@ class TestX1CModel:
         ]
         result = scheduler._match_filaments_to_slots(required, loaded)
         assert result == [5, 7]
+
+
+class TestFtsNozzleBypass:
+    """#2186: with a Filament Track Switch (FTS) installed, the per-nozzle hard
+    filter in the dispatch mapping must be skipped so a spool loaded in the OTHER
+    nozzle's AMS can be routed to the requested nozzle. Without an FTS the filter
+    still applies (cross-nozzle assignment fails on the printer).
+    """
+
+    @pytest.fixture
+    def scheduler(self):
+        return PrintScheduler()
+
+    def _repro_loaded(self):
+        # Mirrors the reporter's H2C: AMS-A (id 0) on the LEFT extruder (1) holds
+        # the black PLA the model wants; AMS-B (id 1) on the RIGHT extruder (0)
+        # holds a red PLA.
+        return [
+            {"type": "PLA", "color": "#000000", "global_tray_id": 1, "extruder_id": 1},  # AMS-A slot2 black, LEFT
+            {"type": "PLA", "color": "#FF0000", "global_tray_id": 6, "extruder_id": 0},  # AMS-B slot3 red, RIGHT
+        ]
+
+    def test_fts_routes_correct_colour_from_other_nozzle(self, scheduler):
+        """Model wants black PLA on the RIGHT nozzle; the black spool sits on the
+        LEFT AMS. With FTS the nozzle filter is skipped, so it picks the black spool
+        (gtid 1) — not the wrong-colour red already on the right (gtid 6).
+        Reproduces #2186 (b3 fed red instead of black)."""
+        required = [{"slot_id": 1, "type": "PLA", "color": "#000000", "nozzle_id": 0}]
+        result = scheduler._match_filaments_to_slots(required, self._repro_loaded(), fts_installed=True)
+        assert result == [1]  # black PLA from AMS-A, routed to the right nozzle by the FTS
+
+    def test_without_fts_nozzle_filter_still_applies(self, scheduler):
+        """No FTS: cross-nozzle assignment isn't possible, so a RIGHT-nozzle request
+        is restricted to right-side trays and matches the red PLA by type. Correct
+        non-FTS behaviour — and exactly why #2186 needed the FTS bypass."""
+        required = [{"slot_id": 1, "type": "PLA", "color": "#000000", "nozzle_id": 0}]
+        result = scheduler._match_filaments_to_slots(required, self._repro_loaded(), fts_installed=False)
+        assert result == [6]  # only the right-side PLA is eligible
+
+    def test_fts_flag_is_noop_without_nozzle_id(self, scheduler):
+        """Single-nozzle 3MFs carry no nozzle_id, so the per-nozzle filter never
+        runs and the FTS flag changes nothing — the exact-colour match wins either
+        way. (Single-nozzle printers also never have an FTS.)"""
+        required = [{"slot_id": 1, "type": "PLA", "color": "#000000"}]
+        for fts in (True, False):
+            result = scheduler._match_filaments_to_slots(required, self._repro_loaded(), fts_installed=fts)
+            assert result == [1]  # exact black match, no nozzle involved

+ 1 - 1
backend/tests/unit/test_scheduler_backup_gate.py

@@ -50,7 +50,7 @@ async def _run_with_backup(scheduler, backup_state, prefer_lowest_setting):
 
     captured: dict = {}
 
-    def _capture_match(reqs, loaded_, prefer, overrides):
+    def _capture_match(reqs, loaded_, prefer, overrides, fts_installed=False):
         captured["prefer_lowest"] = prefer
         return [0]
 

Einige Dateien werden nicht angezeigt, da zu viele Dateien in diesem Diff geändert wurden.