| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120 |
- """Which nozzle an AMS slot feeds.
- Every path that configures a slot needs the extruder and that nozzle's
- diameter, and both the filament preset and the K profile are stored per
- diameter -- so a wrong answer here silently selects the wrong preset AND the
- wrong K value. Seven call sites used to work it out independently, each reading
- ``nozzles[0]`` for every slot on the machine.
- ``nozzles[0]`` is right on a single-nozzle printer, and right on a dual-nozzle
- printer with matching nozzles, which is why it survived. These pin the case it
- is wrong for: two different sizes fitted, which is the machine the per-nozzle
- feature exists for.
- """
- from __future__ import annotations
- from backend.app.services.slot_nozzle import (
- DEFAULT_NOZZLE_DIAMETER,
- nozzle_diameter_for_extruder,
- resolve_slot_nozzle,
- )
- class _Nozzle:
- def __init__(self, diameter: str):
- self.nozzle_diameter = diameter
- class _State:
- def __init__(self, diameters, ams_extruder_map=None, ams_switch_inlet=None):
- self.nozzles = [_Nozzle(d) for d in diameters]
- self.ams_extruder_map = ams_extruder_map
- self.ams_switch_inlet = ams_switch_inlet
- # extruder 0 is the RIGHT hotend, 1 the left.
- MIXED = ["0.4", "0.2"]
- class TestDualNozzleMixedDiameters:
- """The case the old shortcut got wrong."""
- def test_each_hotend_reports_its_own_diameter(self):
- state = _State(MIXED)
- assert nozzle_diameter_for_extruder(state, 0, "H2C") == "0.4"
- assert nozzle_diameter_for_extruder(state, 1, "H2C") == "0.2"
- def test_the_slots_extruder_decides_which_one(self):
- # AMS 0 is bound to the left hotend, AMS 1 to the right.
- state = _State(MIXED, ams_extruder_map={"0": 1, "1": 0})
- left = resolve_slot_nozzle(state, 0, 0, "H2C")
- right = resolve_slot_nozzle(state, 1, 0, "H2C")
- assert (left.extruder, left.diameter) == (1, "0.2")
- assert (right.extruder, right.diameter) == (0, "0.4")
- def test_external_slots_name_their_side_by_tray_id(self):
- # Ext-L is tray 0 -> extruder 1, Ext-R is tray 1 -> extruder 0.
- state = _State(MIXED)
- assert resolve_slot_nozzle(state, 255, 0, "H2D").diameter == "0.2"
- assert resolve_slot_nozzle(state, 255, 1, "H2D").diameter == "0.4"
- def test_an_fts_inlet_answers_when_there_is_no_extruder_map(self):
- state = _State(MIXED, ams_switch_inlet={"0": "A"})
- resolved = resolve_slot_nozzle(state, 0, 0, "H2C")
- assert resolved.extruder is not None
- assert resolved.diameter in {"0.4", "0.2"}
- class TestSingleNozzle:
- """Must behave exactly as the old shortcut did."""
- def test_index_zero_whatever_the_map_says(self):
- # A single-nozzle model has one entry; an extruder id from a stale map
- # must not index past it.
- state = _State(["0.6"], ams_extruder_map={"0": 1})
- assert nozzle_diameter_for_extruder(state, 1, "X1C") == "0.6"
- assert resolve_slot_nozzle(state, 0, 0, "X1C").diameter == "0.6"
- def test_no_map_means_unknown_extruder_not_zero(self):
- # "I don't know" and "the right-hand nozzle" are different answers; the
- # caller's own default of 0 is correct on a single-nozzle machine, but
- # storing it as a fact is what bound a left K-profile to a right slot.
- state = _State(["0.4"])
- resolved = resolve_slot_nozzle(state, 0, 0, "X1C")
- assert resolved.extruder is None
- assert resolved.extruder_or_default == 0
- class TestMissingHardware:
- """Called on every assign, so it must never raise."""
- def test_no_state_at_all(self):
- resolved = resolve_slot_nozzle(None, 0, 0, "H2C")
- assert (resolved.extruder, resolved.diameter) == (None, DEFAULT_NOZZLE_DIAMETER)
- def test_printer_has_reported_no_nozzles(self):
- assert nozzle_diameter_for_extruder(_State([]), 0, "H2C") == DEFAULT_NOZZLE_DIAMETER
- def test_the_second_hotend_is_absent_from_the_report(self):
- # H2C parks a nozzle back in its rack and the entry can go missing;
- # falling back to the other hotend beats returning nothing.
- assert nozzle_diameter_for_extruder(_State(["0.4"]), 1, "H2C") == "0.4"
- def test_a_blank_diameter_is_not_a_diameter(self):
- # NozzleInfo starts life with an empty string before MQTT fills it in.
- # Falling back to the OTHER hotend's size would invent a fact about a
- # different nozzle, so a blank primary takes the default instead --
- # which is what every call site did before this module existed.
- assert nozzle_diameter_for_extruder(_State(["", "0.6"]), 0, "H2C") == DEFAULT_NOZZLE_DIAMETER
- assert nozzle_diameter_for_extruder(_State(["", ""]), 0, "H2C") == DEFAULT_NOZZLE_DIAMETER
- class TestMatchingNozzles:
- """The common fleet: both hotends the same size."""
- def test_both_conventions_agree_so_the_answer_cannot_be_wrong(self):
- state = _State(["0.4", "0.4"], ams_extruder_map={"0": 1})
- assert nozzle_diameter_for_extruder(state, 0, "H2C") == "0.4"
- assert nozzle_diameter_for_extruder(state, 1, "H2C") == "0.4"
|