| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168 |
- """GET /printers/{id}/slots/{ams}/{tray}/spool-defaults
- What the Configure AMS Slot dialog opens with. The slot usually already holds
- an assigned spool, and that spool carries a filament preset per printer model
- and a K profile per hotend -- the values the user set for exactly this
- situation. Before this endpoint the dialog defaulted to the slot's last manual
- configuration or the tray's RFID data and ignored them.
- Everything is resolved for the nozzle THIS slot feeds, so the answer differs
- between the two hotends of a dual-nozzle machine.
- """
- from unittest.mock import MagicMock, patch
- import pytest
- from httpx import AsyncClient
- URL = "/api/v1/printers/{pid}/slots/{ams}/{tray}/spool-defaults"
- RIGHT, LEFT = 0, 1
- class _Nozzle:
- def __init__(self, diameter):
- self.nozzle_diameter = diameter
- class _State:
- """Dual-nozzle, 0.4 on the right and 0.2 on the left, AMS 0 -> left."""
- def __init__(self, diameters=("0.4", "0.2")):
- self.nozzles = [_Nozzle(d) for d in diameters]
- self.ams_extruder_map = {"0": LEFT, "1": RIGHT}
- self.ams_switch_inlet = None
- self.raw_data = {}
- @pytest.fixture
- def dual_nozzle_printer_state():
- with patch("backend.app.api.routes.printers.printer_manager") as manager:
- manager.get_status = MagicMock(return_value=_State())
- manager.get_model = MagicMock(return_value="H2D")
- yield manager
- @pytest.fixture
- async def assigned_spool(db_session, printer_factory):
- """A spool in AMS 0 tray 0, with a per-model preset and both hotends calibrated."""
- from backend.app.models.spool import Spool
- from backend.app.models.spool_assignment import SpoolAssignment
- from backend.app.models.spool_filament_preset import SpoolFilamentPreset
- from backend.app.models.spool_k_profile import SpoolKProfile
- printer = await printer_factory(model="H2D")
- spool = Spool(
- brand="Bambu",
- material="PLA",
- color_name="Black",
- slicer_filament="GFSA00",
- slicer_filament_name="Bambu PLA Basic @BBL X1C",
- )
- db_session.add(spool)
- await db_session.commit()
- await db_session.refresh(spool)
- db_session.add(SpoolAssignment(spool_id=spool.id, printer_id=printer.id, ams_id=0, tray_id=0))
- db_session.add(
- SpoolFilamentPreset(
- spool_id=spool.id,
- printer_model="H2D",
- nozzle_diameter="0.2",
- slicer_filament="GFSA21",
- slicer_filament_name="Bambu PLA Basic @BBL H2D 0.2 nozzle",
- )
- )
- db_session.add_all(
- [
- SpoolKProfile(
- spool_id=spool.id,
- printer_id=printer.id,
- extruder=LEFT,
- nozzle_diameter="0.2",
- k_value=0.018,
- cali_idx=16,
- name="PLA left",
- ),
- SpoolKProfile(
- spool_id=spool.id,
- printer_id=printer.id,
- extruder=RIGHT,
- nozzle_diameter="0.4",
- k_value=0.020,
- cali_idx=15,
- name="PLA right",
- ),
- ]
- )
- await db_session.commit()
- return printer, spool
- @pytest.mark.integration
- class TestSlotSpoolDefaults:
- @pytest.mark.asyncio
- async def test_answers_for_the_hotend_this_slot_feeds(
- self, async_client: AsyncClient, assigned_spool, dual_nozzle_printer_state
- ):
- printer, _ = assigned_spool
- response = await async_client.get(URL.format(pid=printer.id, ams=0, tray=0))
- assert response.status_code == 200, response.text
- body = response.json()
- # AMS 0 feeds the LEFT hotend, which has the 0.2 fitted.
- assert body["extruder"] == LEFT
- assert body["nozzle_diameter"] == "0.2"
- # So the 0.2 preset override, not the spool's own X1C one...
- assert body["slicer_filament"] == "GFSA21"
- # ...and the profile calibrated on that hotend, not the other's.
- assert body["cali_idx"] == 16
- assert body["k_value"] == pytest.approx(0.018)
- assert body["profile_name"] == "PLA left"
- @pytest.mark.asyncio
- async def test_a_slot_with_no_spool_answers_nulls_not_404(
- self, async_client: AsyncClient, assigned_spool, dual_nozzle_printer_state
- ):
- """ "Nothing configured" is an ordinary answer -- the dialog falls back
- to what it did before rather than treating it as an error."""
- printer, _ = assigned_spool
- response = await async_client.get(URL.format(pid=printer.id, ams=1, tray=3))
- assert response.status_code == 200
- body = response.json()
- assert body["slicer_filament"] is None
- assert body["cali_idx"] is None
- # The nozzle is still resolved -- AMS 1 is the right hotend.
- assert body["extruder"] == RIGHT
- assert body["nozzle_diameter"] == "0.4"
- @pytest.mark.asyncio
- async def test_falls_back_to_the_spools_own_preset_without_an_override(
- self, async_client: AsyncClient, assigned_spool, dual_nozzle_printer_state, db_session
- ):
- from backend.app.models.spool_assignment import SpoolAssignment
- printer, spool = assigned_spool
- # Same spool in a slot on the RIGHT hotend, which has no 0.4 override.
- db_session.add(SpoolAssignment(spool_id=spool.id, printer_id=printer.id, ams_id=1, tray_id=0))
- await db_session.commit()
- body = (await async_client.get(URL.format(pid=printer.id, ams=1, tray=0))).json()
- assert body["slicer_filament"] == "GFSA00"
- assert body["cali_idx"] == 15
- @pytest.mark.asyncio
- async def test_an_offline_printer_still_answers(self, async_client: AsyncClient, assigned_spool):
- """Opening the dialog on a disconnected printer must not 500 -- it just
- cannot say which hotend the slot feeds."""
- printer, _ = assigned_spool
- with patch("backend.app.api.routes.printers.printer_manager") as manager:
- manager.get_status = MagicMock(return_value=None)
- manager.get_model = MagicMock(return_value="H2D")
- response = await async_client.get(URL.format(pid=printer.id, ams=0, tray=0))
- assert response.status_code == 200
- body = response.json()
- assert body["extruder"] is None
- assert body["nozzle_diameter"] == "0.4"
|