mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-09-30 03:01:21 +02:00
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.
This commit is contained in:
-2767
File diff suppressed because one or more lines are too long
@@ -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().
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user