|
@@ -1655,13 +1655,24 @@ class TestApplyPaAfterRefresh:
|
|
|
spool = Spool(material="PLA", color_name="Red", rgba="FF0000FF")
|
|
spool = Spool(material="PLA", color_name="Red", rgba="FF0000FF")
|
|
|
db_session.add(spool)
|
|
db_session.add(spool)
|
|
|
await db_session.flush()
|
|
await db_session.flush()
|
|
|
- db_session.add(SpoolAssignment(
|
|
|
|
|
- spool_id=spool.id, printer_id=printer.id, ams_id=0, tray_id=2,
|
|
|
|
|
- ))
|
|
|
|
|
- db_session.add(SpoolKProfile(
|
|
|
|
|
- spool_id=spool.id, printer_id=printer.id,
|
|
|
|
|
- extruder=0, nozzle_diameter="0.4", k_value=0.025, cali_idx=42,
|
|
|
|
|
- ))
|
|
|
|
|
|
|
+ db_session.add(
|
|
|
|
|
+ SpoolAssignment(
|
|
|
|
|
+ spool_id=spool.id,
|
|
|
|
|
+ printer_id=printer.id,
|
|
|
|
|
+ ams_id=0,
|
|
|
|
|
+ tray_id=2,
|
|
|
|
|
+ )
|
|
|
|
|
+ )
|
|
|
|
|
+ db_session.add(
|
|
|
|
|
+ SpoolKProfile(
|
|
|
|
|
+ spool_id=spool.id,
|
|
|
|
|
+ printer_id=printer.id,
|
|
|
|
|
+ extruder=0,
|
|
|
|
|
+ nozzle_diameter="0.4",
|
|
|
|
|
+ k_value=0.025,
|
|
|
|
|
+ cali_idx=42,
|
|
|
|
|
+ )
|
|
|
|
|
+ )
|
|
|
await db_session.commit()
|
|
await db_session.commit()
|
|
|
|
|
|
|
|
mock_client = MagicMock()
|
|
mock_client = MagicMock()
|
|
@@ -1693,9 +1704,14 @@ class TestApplyPaAfterRefresh:
|
|
|
spool = Spool(material="PLA")
|
|
spool = Spool(material="PLA")
|
|
|
db_session.add(spool)
|
|
db_session.add(spool)
|
|
|
await db_session.flush()
|
|
await db_session.flush()
|
|
|
- db_session.add(SpoolAssignment(
|
|
|
|
|
- spool_id=spool.id, printer_id=printer.id, ams_id=0, tray_id=2,
|
|
|
|
|
- ))
|
|
|
|
|
|
|
+ db_session.add(
|
|
|
|
|
+ SpoolAssignment(
|
|
|
|
|
+ spool_id=spool.id,
|
|
|
|
|
+ printer_id=printer.id,
|
|
|
|
|
+ ams_id=0,
|
|
|
|
|
+ tray_id=2,
|
|
|
|
|
+ )
|
|
|
|
|
+ )
|
|
|
await db_session.commit()
|
|
await db_session.commit()
|
|
|
|
|
|
|
|
mock_client = MagicMock()
|
|
mock_client = MagicMock()
|
|
@@ -1724,13 +1740,24 @@ class TestApplyPaAfterRefresh:
|
|
|
from backend.app.models.spoolman_slot_assignment import SpoolmanSlotAssignment
|
|
from backend.app.models.spoolman_slot_assignment import SpoolmanSlotAssignment
|
|
|
|
|
|
|
|
printer = await printer_factory()
|
|
printer = await printer_factory()
|
|
|
- db_session.add(SpoolmanSlotAssignment(
|
|
|
|
|
- printer_id=printer.id, ams_id=0, tray_id=2, spoolman_spool_id=99,
|
|
|
|
|
- ))
|
|
|
|
|
- db_session.add(SpoolmanKProfile(
|
|
|
|
|
- spoolman_spool_id=99, printer_id=printer.id,
|
|
|
|
|
- extruder=0, nozzle_diameter="0.4", k_value=0.030, cali_idx=77,
|
|
|
|
|
- ))
|
|
|
|
|
|
|
+ db_session.add(
|
|
|
|
|
+ SpoolmanSlotAssignment(
|
|
|
|
|
+ printer_id=printer.id,
|
|
|
|
|
+ ams_id=0,
|
|
|
|
|
+ tray_id=2,
|
|
|
|
|
+ spoolman_spool_id=99,
|
|
|
|
|
+ )
|
|
|
|
|
+ )
|
|
|
|
|
+ db_session.add(
|
|
|
|
|
+ SpoolmanKProfile(
|
|
|
|
|
+ spoolman_spool_id=99,
|
|
|
|
|
+ printer_id=printer.id,
|
|
|
|
|
+ extruder=0,
|
|
|
|
|
+ nozzle_diameter="0.4",
|
|
|
|
|
+ k_value=0.030,
|
|
|
|
|
+ cali_idx=77,
|
|
|
|
|
+ )
|
|
|
|
|
+ )
|
|
|
await db_session.commit()
|
|
await db_session.commit()
|
|
|
|
|
|
|
|
mock_client = MagicMock()
|
|
mock_client = MagicMock()
|
|
@@ -1758,9 +1785,14 @@ class TestApplyPaAfterRefresh:
|
|
|
from backend.app.models.spoolman_slot_assignment import SpoolmanSlotAssignment
|
|
from backend.app.models.spoolman_slot_assignment import SpoolmanSlotAssignment
|
|
|
|
|
|
|
|
printer = await printer_factory()
|
|
printer = await printer_factory()
|
|
|
- db_session.add(SpoolmanSlotAssignment(
|
|
|
|
|
- printer_id=printer.id, ams_id=0, tray_id=2, spoolman_spool_id=99,
|
|
|
|
|
- ))
|
|
|
|
|
|
|
+ db_session.add(
|
|
|
|
|
+ SpoolmanSlotAssignment(
|
|
|
|
|
+ printer_id=printer.id,
|
|
|
|
|
+ ams_id=0,
|
|
|
|
|
+ tray_id=2,
|
|
|
|
|
+ spoolman_spool_id=99,
|
|
|
|
|
+ )
|
|
|
|
|
+ )
|
|
|
await db_session.commit()
|
|
await db_session.commit()
|
|
|
|
|
|
|
|
mock_client = MagicMock()
|
|
mock_client = MagicMock()
|
|
@@ -1847,17 +1879,21 @@ class TestApplyPaAfterRefresh:
|
|
|
state.nozzles = [nozzle]
|
|
state.nozzles = [nozzle]
|
|
|
state.ams_extruder_map = {"0": 0}
|
|
state.ams_extruder_map = {"0": 0}
|
|
|
state.raw_data = {
|
|
state.raw_data = {
|
|
|
- "ams": [{
|
|
|
|
|
- "id": 0,
|
|
|
|
|
- "tray": [{
|
|
|
|
|
- "id": 2,
|
|
|
|
|
- "tray_type": "PLA",
|
|
|
|
|
- "tag_uid": "AABBCC1122334400",
|
|
|
|
|
- "tray_uuid": "11223344556677880011223344556677",
|
|
|
|
|
- "tray_info_idx": "GFL05",
|
|
|
|
|
- # cali_idx field intentionally omitted
|
|
|
|
|
- }],
|
|
|
|
|
- }]
|
|
|
|
|
|
|
+ "ams": [
|
|
|
|
|
+ {
|
|
|
|
|
+ "id": 0,
|
|
|
|
|
+ "tray": [
|
|
|
|
|
+ {
|
|
|
|
|
+ "id": 2,
|
|
|
|
|
+ "tray_type": "PLA",
|
|
|
|
|
+ "tag_uid": "AABBCC1122334400",
|
|
|
|
|
+ "tray_uuid": "11223344556677880011223344556677",
|
|
|
|
|
+ "tray_info_idx": "GFL05",
|
|
|
|
|
+ # cali_idx field intentionally omitted
|
|
|
|
|
+ }
|
|
|
|
|
+ ],
|
|
|
|
|
+ }
|
|
|
|
|
+ ]
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
with (
|
|
with (
|
|
@@ -1873,8 +1909,17 @@ class TestApplyPaAfterRefresh:
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.asyncio
|
|
|
@pytest.mark.integration
|
|
@pytest.mark.integration
|
|
|
- async def test_extruder_mismatch_falls_through_to_live(self, db_session, printer_factory):
|
|
|
|
|
- """K-profile for extruder=1 but slot is extruder=0 → KP filtered, live fallback used."""
|
|
|
|
|
|
|
+ async def test_extruder_mismatch_uses_kp_as_fallback(self, db_session, printer_factory):
|
|
|
|
|
+ """K-profile for extruder=1 but slot is extruder=0 → no exact match,
|
|
|
|
|
+ but the kp is used as extruder-agnostic fallback rather than dropped.
|
|
|
|
|
+
|
|
|
|
|
+ Hard-skipping on extruder mismatch was the previous behavior; in
|
|
|
|
|
+ practice it caused stored K-profiles to be silently ignored whenever
|
|
|
|
|
+ the AMS-extruder mapping had shifted (or when only one of the two
|
|
|
|
|
+ extruders was ever calibrated for a given spool). The cascade now
|
|
|
|
|
+ prefers an exact extruder match but falls back to any matching kp
|
|
|
|
|
+ for the same printer + nozzle.
|
|
|
|
|
+ """
|
|
|
from backend.app.api.routes.printers import _apply_pa_after_refresh
|
|
from backend.app.api.routes.printers import _apply_pa_after_refresh
|
|
|
from backend.app.models.spool import Spool
|
|
from backend.app.models.spool import Spool
|
|
|
from backend.app.models.spool_assignment import SpoolAssignment
|
|
from backend.app.models.spool_assignment import SpoolAssignment
|
|
@@ -1884,14 +1929,25 @@ class TestApplyPaAfterRefresh:
|
|
|
spool = Spool(material="PLA")
|
|
spool = Spool(material="PLA")
|
|
|
db_session.add(spool)
|
|
db_session.add(spool)
|
|
|
await db_session.flush()
|
|
await db_session.flush()
|
|
|
- db_session.add(SpoolAssignment(
|
|
|
|
|
- spool_id=spool.id, printer_id=printer.id, ams_id=0, tray_id=2,
|
|
|
|
|
- ))
|
|
|
|
|
- # K-profile is for extruder=1, but slot's ams_extruder_map["0"]=0 → mismatch
|
|
|
|
|
- db_session.add(SpoolKProfile(
|
|
|
|
|
- spool_id=spool.id, printer_id=printer.id,
|
|
|
|
|
- extruder=1, nozzle_diameter="0.4", k_value=0.025, cali_idx=42,
|
|
|
|
|
- ))
|
|
|
|
|
|
|
+ db_session.add(
|
|
|
|
|
+ SpoolAssignment(
|
|
|
|
|
+ spool_id=spool.id,
|
|
|
|
|
+ printer_id=printer.id,
|
|
|
|
|
+ ams_id=0,
|
|
|
|
|
+ tray_id=2,
|
|
|
|
|
+ )
|
|
|
|
|
+ )
|
|
|
|
|
+ # K-profile is for extruder=1, but slot's ams_extruder_map["0"]=0
|
|
|
|
|
+ db_session.add(
|
|
|
|
|
+ SpoolKProfile(
|
|
|
|
|
+ spool_id=spool.id,
|
|
|
|
|
+ printer_id=printer.id,
|
|
|
|
|
+ extruder=1,
|
|
|
|
|
+ nozzle_diameter="0.4",
|
|
|
|
|
+ k_value=0.025,
|
|
|
|
|
+ cali_idx=42,
|
|
|
|
|
+ )
|
|
|
|
|
+ )
|
|
|
await db_session.commit()
|
|
await db_session.commit()
|
|
|
|
|
|
|
|
mock_client = MagicMock()
|
|
mock_client = MagicMock()
|
|
@@ -1907,9 +1963,78 @@ class TestApplyPaAfterRefresh:
|
|
|
mock_pm.get_status.return_value = state
|
|
mock_pm.get_status.return_value = state
|
|
|
await _apply_pa_after_refresh(printer.id, ams_id=0, slot_id=2)
|
|
await _apply_pa_after_refresh(printer.id, ams_id=0, slot_id=2)
|
|
|
|
|
|
|
|
- # K-profile mismatch → falls through to live cali_idx=5 (NOT 42)
|
|
|
|
|
|
|
+ # No exact extruder match, but the stored kp wins as the
|
|
|
|
|
+ # extruder-agnostic fallback over live cali_idx=5.
|
|
|
mock_client.extrusion_cali_sel.assert_called_once()
|
|
mock_client.extrusion_cali_sel.assert_called_once()
|
|
|
- assert mock_client.extrusion_cali_sel.call_args.kwargs["cali_idx"] == 5
|
|
|
|
|
|
|
+ assert mock_client.extrusion_cali_sel.call_args.kwargs["cali_idx"] == 42
|
|
|
|
|
+
|
|
|
|
|
+ @pytest.mark.asyncio
|
|
|
|
|
+ @pytest.mark.integration
|
|
|
|
|
+ async def test_extruder_exact_match_preferred_over_fallback(
|
|
|
|
|
+ self,
|
|
|
|
|
+ db_session,
|
|
|
|
|
+ printer_factory,
|
|
|
|
|
+ ):
|
|
|
|
|
+ """When two kp rows exist, one with matching extruder and one without,
|
|
|
|
|
+ the exact-extruder kp wins (extruder-agnostic fallback only fires when
|
|
|
|
|
+ no exact match exists).
|
|
|
|
|
+ """
|
|
|
|
|
+ from backend.app.api.routes.printers import _apply_pa_after_refresh
|
|
|
|
|
+ from backend.app.models.spool import Spool
|
|
|
|
|
+ from backend.app.models.spool_assignment import SpoolAssignment
|
|
|
|
|
+ from backend.app.models.spool_k_profile import SpoolKProfile
|
|
|
|
|
+
|
|
|
|
|
+ printer = await printer_factory()
|
|
|
|
|
+ spool = Spool(material="PLA")
|
|
|
|
|
+ db_session.add(spool)
|
|
|
|
|
+ await db_session.flush()
|
|
|
|
|
+ db_session.add(
|
|
|
|
|
+ SpoolAssignment(
|
|
|
|
|
+ spool_id=spool.id,
|
|
|
|
|
+ printer_id=printer.id,
|
|
|
|
|
+ ams_id=0,
|
|
|
|
|
+ tray_id=2,
|
|
|
|
|
+ )
|
|
|
|
|
+ )
|
|
|
|
|
+ # Two kp rows: extruder=1 (mismatch w/ slot extruder=0) and extruder=0 (exact)
|
|
|
|
|
+ db_session.add(
|
|
|
|
|
+ SpoolKProfile(
|
|
|
|
|
+ spool_id=spool.id,
|
|
|
|
|
+ printer_id=printer.id,
|
|
|
|
|
+ extruder=1,
|
|
|
|
|
+ nozzle_diameter="0.4",
|
|
|
|
|
+ k_value=0.030,
|
|
|
|
|
+ cali_idx=99,
|
|
|
|
|
+ )
|
|
|
|
|
+ )
|
|
|
|
|
+ db_session.add(
|
|
|
|
|
+ SpoolKProfile(
|
|
|
|
|
+ spool_id=spool.id,
|
|
|
|
|
+ printer_id=printer.id,
|
|
|
|
|
+ extruder=0,
|
|
|
|
|
+ nozzle_diameter="0.4",
|
|
|
|
|
+ k_value=0.025,
|
|
|
|
|
+ cali_idx=42,
|
|
|
|
|
+ )
|
|
|
|
|
+ )
|
|
|
|
|
+ await db_session.commit()
|
|
|
|
|
+
|
|
|
|
|
+ mock_client = MagicMock()
|
|
|
|
|
+ mock_client.extrusion_cali_sel = MagicMock(return_value=True)
|
|
|
|
|
+ state = _build_h2d_state(cali_idx=5)
|
|
|
|
|
+
|
|
|
|
|
+ with (
|
|
|
|
|
+ patch("backend.app.api.routes.printers.asyncio.sleep", AsyncMock()),
|
|
|
|
|
+ patch("backend.app.api.routes.printers.printer_manager") as mock_pm,
|
|
|
|
|
+ _patch_async_session_to(db_session),
|
|
|
|
|
+ ):
|
|
|
|
|
+ mock_pm.get_client.return_value = mock_client
|
|
|
|
|
+ mock_pm.get_status.return_value = state
|
|
|
|
|
+ await _apply_pa_after_refresh(printer.id, ams_id=0, slot_id=2)
|
|
|
|
|
+
|
|
|
|
|
+ # Exact-extruder=0 kp wins (cali_idx=42), not the extruder=1 fallback (99)
|
|
|
|
|
+ mock_client.extrusion_cali_sel.assert_called_once()
|
|
|
|
|
+ assert mock_client.extrusion_cali_sel.call_args.kwargs["cali_idx"] == 42
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.asyncio
|
|
|
@pytest.mark.integration
|
|
@pytest.mark.integration
|
|
@@ -1930,14 +2055,25 @@ class TestApplyPaAfterRefresh:
|
|
|
spool = Spool(material="PLA")
|
|
spool = Spool(material="PLA")
|
|
|
db_session.add(spool)
|
|
db_session.add(spool)
|
|
|
await db_session.flush()
|
|
await db_session.flush()
|
|
|
- db_session.add(SpoolAssignment(
|
|
|
|
|
- spool_id=spool.id, printer_id=printer.id, ams_id=0, tray_id=2,
|
|
|
|
|
- ))
|
|
|
|
|
|
|
+ db_session.add(
|
|
|
|
|
+ SpoolAssignment(
|
|
|
|
|
+ spool_id=spool.id,
|
|
|
|
|
+ printer_id=printer.id,
|
|
|
|
|
+ ams_id=0,
|
|
|
|
|
+ tray_id=2,
|
|
|
|
|
+ )
|
|
|
|
|
+ )
|
|
|
# extruder=0 matches slot_extruder=0 (from ams_extruder_map={"0":0})
|
|
# extruder=0 matches slot_extruder=0 (from ams_extruder_map={"0":0})
|
|
|
- db_session.add(SpoolKProfile(
|
|
|
|
|
- spool_id=spool.id, printer_id=printer.id,
|
|
|
|
|
- extruder=0, nozzle_diameter="0.4", k_value=0.025, cali_idx=42,
|
|
|
|
|
- ))
|
|
|
|
|
|
|
+ db_session.add(
|
|
|
|
|
+ SpoolKProfile(
|
|
|
|
|
+ spool_id=spool.id,
|
|
|
|
|
+ printer_id=printer.id,
|
|
|
|
|
+ extruder=0,
|
|
|
|
|
+ nozzle_diameter="0.4",
|
|
|
|
|
+ k_value=0.025,
|
|
|
|
|
+ cali_idx=42,
|
|
|
|
|
+ )
|
|
|
|
|
+ )
|
|
|
await db_session.commit()
|
|
await db_session.commit()
|
|
|
|
|
|
|
|
mock_client = MagicMock()
|
|
mock_client = MagicMock()
|
|
@@ -1958,6 +2094,185 @@ class TestApplyPaAfterRefresh:
|
|
|
mock_client.extrusion_cali_sel.assert_called_once()
|
|
mock_client.extrusion_cali_sel.assert_called_once()
|
|
|
assert mock_client.extrusion_cali_sel.call_args.kwargs["cali_idx"] == 42
|
|
assert mock_client.extrusion_cali_sel.call_args.kwargs["cali_idx"] == 42
|
|
|
|
|
|
|
|
|
|
+ @pytest.mark.asyncio
|
|
|
|
|
+ @pytest.mark.integration
|
|
|
|
|
+ async def test_tag_fallback_finds_spool_when_assignment_missing(
|
|
|
|
|
+ self,
|
|
|
|
|
+ db_session,
|
|
|
|
|
+ printer_factory,
|
|
|
|
|
+ ):
|
|
|
|
|
+ """Stage 1b regression for the maintainer's #2 reproducer on H2D:
|
|
|
|
|
+ reset slot, trigger re-read → slot ends up on the default K-profile
|
|
|
|
|
+ instead of the spool's stored profile.
|
|
|
|
|
+
|
|
|
|
|
+ Setup mirrors the bug:
|
|
|
|
|
+ - Spool has tray_uuid set (the RFID tag was registered earlier).
|
|
|
|
|
+ - SpoolKProfile exists for that spool with cali_idx=42.
|
|
|
|
|
+ - NO SpoolAssignment row — the reset deleted it before the re-read
|
|
|
|
|
+ triggered _apply_pa_after_refresh, and tag-auto-detect has not
|
|
|
|
|
+ re-created it yet within the 5 s sleep window.
|
|
|
|
|
+ - Live tray.cali_idx=5 (firmware-default after the RFID re-read).
|
|
|
|
|
+
|
|
|
|
|
+ Without Stage 1b the cascade falls through to Stage 3 and re-asserts
|
|
|
|
|
+ the firmware-default cali_idx=5. With Stage 1b it locates the spool by
|
|
|
|
|
+ the live tray's tray_uuid and applies the stored cali_idx=42.
|
|
|
|
|
+ """
|
|
|
|
|
+ from backend.app.api.routes.printers import _apply_pa_after_refresh
|
|
|
|
|
+ from backend.app.models.spool import Spool
|
|
|
|
|
+ from backend.app.models.spool_k_profile import SpoolKProfile
|
|
|
|
|
+
|
|
|
|
|
+ printer = await printer_factory()
|
|
|
|
|
+ # Spool with tray_uuid matching the one _build_h2d_state puts on the tray
|
|
|
|
|
+ spool = Spool(
|
|
|
|
|
+ material="PLA",
|
|
|
|
|
+ color_name="Red",
|
|
|
|
|
+ rgba="FF0000FF",
|
|
|
|
|
+ tray_uuid="11223344556677880011223344556677",
|
|
|
|
|
+ tag_uid="AABBCC1122334400",
|
|
|
|
|
+ )
|
|
|
|
|
+ db_session.add(spool)
|
|
|
|
|
+ await db_session.flush()
|
|
|
|
|
+ # K-profile is bound to the spool, not to a slot
|
|
|
|
|
+ db_session.add(
|
|
|
|
|
+ SpoolKProfile(
|
|
|
|
|
+ spool_id=spool.id,
|
|
|
|
|
+ printer_id=printer.id,
|
|
|
|
|
+ extruder=0,
|
|
|
|
|
+ nozzle_diameter="0.4",
|
|
|
|
|
+ k_value=0.025,
|
|
|
|
|
+ cali_idx=42,
|
|
|
|
|
+ )
|
|
|
|
|
+ )
|
|
|
|
|
+ # NOTE: deliberately no SpoolAssignment — that's the bug condition.
|
|
|
|
|
+ await db_session.commit()
|
|
|
|
|
+
|
|
|
|
|
+ mock_client = MagicMock()
|
|
|
|
|
+ mock_client.extrusion_cali_sel = MagicMock(return_value=True)
|
|
|
|
|
+ state = _build_h2d_state(cali_idx=5)
|
|
|
|
|
+
|
|
|
|
|
+ with (
|
|
|
|
|
+ patch("backend.app.api.routes.printers.asyncio.sleep", AsyncMock()),
|
|
|
|
|
+ patch("backend.app.api.routes.printers.printer_manager") as mock_pm,
|
|
|
|
|
+ _patch_async_session_to(db_session),
|
|
|
|
|
+ ):
|
|
|
|
|
+ mock_pm.get_client.return_value = mock_client
|
|
|
|
|
+ mock_pm.get_status.return_value = state
|
|
|
|
|
+ await _apply_pa_after_refresh(printer.id, ams_id=0, slot_id=2)
|
|
|
|
|
+
|
|
|
|
|
+ # Stage 1b should match the spool by tray_uuid → stored cali_idx=42 wins
|
|
|
|
|
+ # over live cali_idx=5. Pre-fix this would have been 5 (firmware default).
|
|
|
|
|
+ mock_client.extrusion_cali_sel.assert_called_once()
|
|
|
|
|
+ assert mock_client.extrusion_cali_sel.call_args.kwargs["cali_idx"] == 42
|
|
|
|
|
+
|
|
|
|
|
+ @pytest.mark.asyncio
|
|
|
|
|
+ @pytest.mark.integration
|
|
|
|
|
+ async def test_tag_fallback_matches_by_tag_uid_when_uuid_zero(
|
|
|
|
|
+ self,
|
|
|
|
|
+ db_session,
|
|
|
|
|
+ printer_factory,
|
|
|
|
|
+ ):
|
|
|
|
|
+ """Stage 1b: when tray_uuid is the zero sentinel but tag_uid is real,
|
|
|
|
|
+ match by tag_uid. Older firmwares occasionally report a zero tray_uuid
|
|
|
|
|
+ right after RFID re-read while the tag_uid is already populated."""
|
|
|
|
|
+ from backend.app.api.routes.printers import _apply_pa_after_refresh
|
|
|
|
|
+ from backend.app.models.spool import Spool
|
|
|
|
|
+ from backend.app.models.spool_k_profile import SpoolKProfile
|
|
|
|
|
+
|
|
|
|
|
+ printer = await printer_factory()
|
|
|
|
|
+ # Spool indexed by tag_uid, not tray_uuid
|
|
|
|
|
+ spool = Spool(material="PLA", tag_uid="AABBCC1122334400")
|
|
|
|
|
+ db_session.add(spool)
|
|
|
|
|
+ await db_session.flush()
|
|
|
|
|
+ db_session.add(
|
|
|
|
|
+ SpoolKProfile(
|
|
|
|
|
+ spool_id=spool.id,
|
|
|
|
|
+ printer_id=printer.id,
|
|
|
|
|
+ extruder=0,
|
|
|
|
|
+ nozzle_diameter="0.4",
|
|
|
|
|
+ k_value=0.025,
|
|
|
|
|
+ cali_idx=99,
|
|
|
|
|
+ )
|
|
|
|
|
+ )
|
|
|
|
|
+ await db_session.commit()
|
|
|
|
|
+
|
|
|
|
|
+ mock_client = MagicMock()
|
|
|
|
|
+ mock_client.extrusion_cali_sel = MagicMock(return_value=True)
|
|
|
|
|
+ # Build a state where the tray reports a real tag_uid but a zero tray_uuid
|
|
|
|
|
+ # while still passing is_bambu_tag (tag_uid + tray_info_idx is sufficient).
|
|
|
|
|
+ state = _build_h2d_state(cali_idx=5)
|
|
|
|
|
+ state.raw_data["ams"][0]["tray"][0]["tray_uuid"] = "00000000000000000000000000000000"
|
|
|
|
|
+
|
|
|
|
|
+ with (
|
|
|
|
|
+ patch("backend.app.api.routes.printers.asyncio.sleep", AsyncMock()),
|
|
|
|
|
+ patch("backend.app.api.routes.printers.printer_manager") as mock_pm,
|
|
|
|
|
+ _patch_async_session_to(db_session),
|
|
|
|
|
+ ):
|
|
|
|
|
+ mock_pm.get_client.return_value = mock_client
|
|
|
|
|
+ mock_pm.get_status.return_value = state
|
|
|
|
|
+ await _apply_pa_after_refresh(printer.id, ams_id=0, slot_id=2)
|
|
|
|
|
+
|
|
|
|
|
+ mock_client.extrusion_cali_sel.assert_called_once()
|
|
|
|
|
+ assert mock_client.extrusion_cali_sel.call_args.kwargs["cali_idx"] == 99
|
|
|
|
|
+
|
|
|
|
|
+ @pytest.mark.asyncio
|
|
|
|
|
+ @pytest.mark.integration
|
|
|
|
|
+ async def test_tag_fallback_skipped_when_zero_sentinels(
|
|
|
|
|
+ self,
|
|
|
|
|
+ db_session,
|
|
|
|
|
+ printer_factory,
|
|
|
|
|
+ ):
|
|
|
|
|
+ """Stage 1b: when both tray_uuid and tag_uid are zero sentinels, the
|
|
|
|
|
+ fallback must not match any spool (would otherwise pick up an
|
|
|
|
|
+ unrelated spool created with empty/zero tag fields). Falls through
|
|
|
|
|
+ to Stage 3 live cali_idx as before.
|
|
|
|
|
+ """
|
|
|
|
|
+ from backend.app.api.routes.printers import _apply_pa_after_refresh
|
|
|
|
|
+ from backend.app.models.spool import Spool
|
|
|
|
|
+ from backend.app.models.spool_k_profile import SpoolKProfile
|
|
|
|
|
+
|
|
|
|
|
+ printer = await printer_factory()
|
|
|
|
|
+ # Decoy spool with no tag info — must NOT match
|
|
|
|
|
+ spool = Spool(material="PLA")
|
|
|
|
|
+ db_session.add(spool)
|
|
|
|
|
+ await db_session.flush()
|
|
|
|
|
+ db_session.add(
|
|
|
|
|
+ SpoolKProfile(
|
|
|
|
|
+ spool_id=spool.id,
|
|
|
|
|
+ printer_id=printer.id,
|
|
|
|
|
+ extruder=0,
|
|
|
|
|
+ nozzle_diameter="0.4",
|
|
|
|
|
+ k_value=0.025,
|
|
|
|
|
+ cali_idx=42,
|
|
|
|
|
+ )
|
|
|
|
|
+ )
|
|
|
|
|
+ await db_session.commit()
|
|
|
|
|
+
|
|
|
|
|
+ mock_client = MagicMock()
|
|
|
|
|
+ mock_client.extrusion_cali_sel = MagicMock(return_value=True)
|
|
|
|
|
+ state = _build_h2d_state(cali_idx=7)
|
|
|
|
|
+ # Force both tag fields to the zero sentinels but keep tray_info_idx
|
|
|
|
|
+ # so is_bambu_tag still passes (preset present)
|
|
|
|
|
+ state.raw_data["ams"][0]["tray"][0]["tag_uid"] = "0000000000000000"
|
|
|
|
|
+ state.raw_data["ams"][0]["tray"][0]["tray_uuid"] = "00000000000000000000000000000000"
|
|
|
|
|
+
|
|
|
|
|
+ with (
|
|
|
|
|
+ patch("backend.app.api.routes.printers.asyncio.sleep", AsyncMock()),
|
|
|
|
|
+ patch("backend.app.api.routes.printers.printer_manager") as mock_pm,
|
|
|
|
|
+ _patch_async_session_to(db_session),
|
|
|
|
|
+ ):
|
|
|
|
|
+ mock_pm.get_client.return_value = mock_client
|
|
|
|
|
+ mock_pm.get_status.return_value = state
|
|
|
|
|
+ # is_bambu_tag actually rejects both-zero + only-preset, so the
|
|
|
|
|
+ # function returns early. We just want to confirm we didn't blow
|
|
|
|
|
+ # up scanning for a tag-fallback spool.
|
|
|
|
|
+ await _apply_pa_after_refresh(printer.id, ams_id=0, slot_id=2)
|
|
|
|
|
+
|
|
|
|
|
+ # is_bambu_tag short-circuits early when both UID and UUID are zero,
|
|
|
|
|
+ # so no MQTT call should fire and the decoy spool's cali_idx=42 must
|
|
|
|
|
+ # NOT leak through.
|
|
|
|
|
+ if mock_client.extrusion_cali_sel.called:
|
|
|
|
|
+ assert mock_client.extrusion_cali_sel.call_args.kwargs["cali_idx"] != 42
|
|
|
|
|
+
|
|
|
|
|
|
|
|
class TestConfigureAmsSlotPersistsKProfile:
|
|
class TestConfigureAmsSlotPersistsKProfile:
|
|
|
"""Phase 13 P13-T-BE-2: configure_ams_slot persists K-profile to DB.
|
|
"""Phase 13 P13-T-BE-2: configure_ams_slot persists K-profile to DB.
|
|
@@ -1970,16 +2285,24 @@ class TestConfigureAmsSlotPersistsKProfile:
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.asyncio
|
|
|
@pytest.mark.integration
|
|
@pytest.mark.integration
|
|
|
async def test_writes_spoolman_kprofile_when_spoolman_assigned(
|
|
async def test_writes_spoolman_kprofile_when_spoolman_assigned(
|
|
|
- self, async_client: AsyncClient, db_session, printer_factory,
|
|
|
|
|
|
|
+ self,
|
|
|
|
|
+ async_client: AsyncClient,
|
|
|
|
|
+ db_session,
|
|
|
|
|
+ printer_factory,
|
|
|
):
|
|
):
|
|
|
"""SpoolmanSlotAssignment present → SpoolmanKProfile row created with cali_idx + k_value + name."""
|
|
"""SpoolmanSlotAssignment present → SpoolmanKProfile row created with cali_idx + k_value + name."""
|
|
|
from backend.app.models.spoolman_k_profile import SpoolmanKProfile
|
|
from backend.app.models.spoolman_k_profile import SpoolmanKProfile
|
|
|
from backend.app.models.spoolman_slot_assignment import SpoolmanSlotAssignment
|
|
from backend.app.models.spoolman_slot_assignment import SpoolmanSlotAssignment
|
|
|
|
|
|
|
|
printer = await printer_factory(model="H2D")
|
|
printer = await printer_factory(model="H2D")
|
|
|
- db_session.add(SpoolmanSlotAssignment(
|
|
|
|
|
- printer_id=printer.id, ams_id=0, tray_id=3, spoolman_spool_id=216,
|
|
|
|
|
- ))
|
|
|
|
|
|
|
+ db_session.add(
|
|
|
|
|
+ SpoolmanSlotAssignment(
|
|
|
|
|
+ printer_id=printer.id,
|
|
|
|
|
+ ams_id=0,
|
|
|
|
|
+ tray_id=3,
|
|
|
|
|
+ spoolman_spool_id=216,
|
|
|
|
|
+ )
|
|
|
|
|
+ )
|
|
|
await db_session.commit()
|
|
await db_session.commit()
|
|
|
|
|
|
|
|
mock_client = MagicMock()
|
|
mock_client = MagicMock()
|
|
@@ -2012,9 +2335,7 @@ class TestConfigureAmsSlotPersistsKProfile:
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
assert response.status_code == 200
|
|
assert response.status_code == 200
|
|
|
- kp_result = await db_session.execute(
|
|
|
|
|
- select(SpoolmanKProfile).where(SpoolmanKProfile.spoolman_spool_id == 216)
|
|
|
|
|
- )
|
|
|
|
|
|
|
+ kp_result = await db_session.execute(select(SpoolmanKProfile).where(SpoolmanKProfile.spoolman_spool_id == 216))
|
|
|
kp = kp_result.scalar_one_or_none()
|
|
kp = kp_result.scalar_one_or_none()
|
|
|
assert kp is not None
|
|
assert kp is not None
|
|
|
assert kp.cali_idx == 5
|
|
assert kp.cali_idx == 5
|
|
@@ -2026,7 +2347,10 @@ class TestConfigureAmsSlotPersistsKProfile:
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.asyncio
|
|
|
@pytest.mark.integration
|
|
@pytest.mark.integration
|
|
|
async def test_writes_spool_kprofile_when_local_assigned(
|
|
async def test_writes_spool_kprofile_when_local_assigned(
|
|
|
- self, async_client: AsyncClient, db_session, printer_factory,
|
|
|
|
|
|
|
+ self,
|
|
|
|
|
+ async_client: AsyncClient,
|
|
|
|
|
+ db_session,
|
|
|
|
|
+ printer_factory,
|
|
|
):
|
|
):
|
|
|
"""Local SpoolAssignment present → SpoolKProfile row created."""
|
|
"""Local SpoolAssignment present → SpoolKProfile row created."""
|
|
|
from backend.app.models.spool import Spool
|
|
from backend.app.models.spool import Spool
|
|
@@ -2037,9 +2361,14 @@ class TestConfigureAmsSlotPersistsKProfile:
|
|
|
spool = Spool(material="PLA", color_name="Red", rgba="FF0000FF")
|
|
spool = Spool(material="PLA", color_name="Red", rgba="FF0000FF")
|
|
|
db_session.add(spool)
|
|
db_session.add(spool)
|
|
|
await db_session.flush()
|
|
await db_session.flush()
|
|
|
- db_session.add(SpoolAssignment(
|
|
|
|
|
- spool_id=spool.id, printer_id=printer.id, ams_id=0, tray_id=3,
|
|
|
|
|
- ))
|
|
|
|
|
|
|
+ db_session.add(
|
|
|
|
|
+ SpoolAssignment(
|
|
|
|
|
+ spool_id=spool.id,
|
|
|
|
|
+ printer_id=printer.id,
|
|
|
|
|
+ ams_id=0,
|
|
|
|
|
+ tray_id=3,
|
|
|
|
|
+ )
|
|
|
|
|
+ )
|
|
|
await db_session.commit()
|
|
await db_session.commit()
|
|
|
spool_id = spool.id
|
|
spool_id = spool.id
|
|
|
|
|
|
|
@@ -2073,9 +2402,7 @@ class TestConfigureAmsSlotPersistsKProfile:
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
assert response.status_code == 200
|
|
assert response.status_code == 200
|
|
|
- kp_result = await db_session.execute(
|
|
|
|
|
- select(SpoolKProfile).where(SpoolKProfile.spool_id == spool_id)
|
|
|
|
|
- )
|
|
|
|
|
|
|
+ kp_result = await db_session.execute(select(SpoolKProfile).where(SpoolKProfile.spool_id == spool_id))
|
|
|
kp = kp_result.scalar_one_or_none()
|
|
kp = kp_result.scalar_one_or_none()
|
|
|
assert kp is not None
|
|
assert kp is not None
|
|
|
assert kp.cali_idx == 7
|
|
assert kp.cali_idx == 7
|
|
@@ -2087,7 +2414,10 @@ class TestConfigureAmsSlotPersistsKProfile:
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.asyncio
|
|
|
@pytest.mark.integration
|
|
@pytest.mark.integration
|
|
|
async def test_no_assignment_no_persist(
|
|
async def test_no_assignment_no_persist(
|
|
|
- self, async_client: AsyncClient, db_session, printer_factory,
|
|
|
|
|
|
|
+ self,
|
|
|
|
|
+ async_client: AsyncClient,
|
|
|
|
|
+ db_session,
|
|
|
|
|
+ printer_factory,
|
|
|
):
|
|
):
|
|
|
"""No SpoolAssignment AND no SpoolmanSlotAssignment → no DB write, MQTT still sent."""
|
|
"""No SpoolAssignment AND no SpoolmanSlotAssignment → no DB write, MQTT still sent."""
|
|
|
from backend.app.models.spool_k_profile import SpoolKProfile
|
|
from backend.app.models.spool_k_profile import SpoolKProfile
|
|
@@ -2111,10 +2441,14 @@ class TestConfigureAmsSlotPersistsKProfile:
|
|
|
response = await async_client.post(
|
|
response = await async_client.post(
|
|
|
f"/api/v1/printers/{printer.id}/slots/0/3/configure",
|
|
f"/api/v1/printers/{printer.id}/slots/0/3/configure",
|
|
|
params={
|
|
params={
|
|
|
- "tray_info_idx": "GFL05", "tray_type": "PLA",
|
|
|
|
|
- "tray_sub_brands": "PLA Basic", "tray_color": "FFFFFFFF",
|
|
|
|
|
- "nozzle_temp_min": 190, "nozzle_temp_max": 230,
|
|
|
|
|
- "cali_idx": 5, "k_value": 0.020,
|
|
|
|
|
|
|
+ "tray_info_idx": "GFL05",
|
|
|
|
|
+ "tray_type": "PLA",
|
|
|
|
|
+ "tray_sub_brands": "PLA Basic",
|
|
|
|
|
+ "tray_color": "FFFFFFFF",
|
|
|
|
|
+ "nozzle_temp_min": 190,
|
|
|
|
|
+ "nozzle_temp_max": 230,
|
|
|
|
|
+ "cali_idx": 5,
|
|
|
|
|
+ "k_value": 0.020,
|
|
|
},
|
|
},
|
|
|
)
|
|
)
|
|
|
|
|
|
|
@@ -2129,16 +2463,24 @@ class TestConfigureAmsSlotPersistsKProfile:
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.asyncio
|
|
|
@pytest.mark.integration
|
|
@pytest.mark.integration
|
|
|
async def test_negative_cali_idx_no_persist(
|
|
async def test_negative_cali_idx_no_persist(
|
|
|
- self, async_client: AsyncClient, db_session, printer_factory,
|
|
|
|
|
|
|
+ self,
|
|
|
|
|
+ async_client: AsyncClient,
|
|
|
|
|
+ db_session,
|
|
|
|
|
+ printer_factory,
|
|
|
):
|
|
):
|
|
|
"""cali_idx=-1 (no profile selected) → no DB write even when assignment exists."""
|
|
"""cali_idx=-1 (no profile selected) → no DB write even when assignment exists."""
|
|
|
from backend.app.models.spoolman_k_profile import SpoolmanKProfile
|
|
from backend.app.models.spoolman_k_profile import SpoolmanKProfile
|
|
|
from backend.app.models.spoolman_slot_assignment import SpoolmanSlotAssignment
|
|
from backend.app.models.spoolman_slot_assignment import SpoolmanSlotAssignment
|
|
|
|
|
|
|
|
printer = await printer_factory(model="H2D")
|
|
printer = await printer_factory(model="H2D")
|
|
|
- db_session.add(SpoolmanSlotAssignment(
|
|
|
|
|
- printer_id=printer.id, ams_id=0, tray_id=3, spoolman_spool_id=216,
|
|
|
|
|
- ))
|
|
|
|
|
|
|
+ db_session.add(
|
|
|
|
|
+ SpoolmanSlotAssignment(
|
|
|
|
|
+ printer_id=printer.id,
|
|
|
|
|
+ ams_id=0,
|
|
|
|
|
+ tray_id=3,
|
|
|
|
|
+ spoolman_spool_id=216,
|
|
|
|
|
+ )
|
|
|
|
|
+ )
|
|
|
await db_session.commit()
|
|
await db_session.commit()
|
|
|
|
|
|
|
|
mock_client = MagicMock()
|
|
mock_client = MagicMock()
|
|
@@ -2158,32 +2500,46 @@ class TestConfigureAmsSlotPersistsKProfile:
|
|
|
response = await async_client.post(
|
|
response = await async_client.post(
|
|
|
f"/api/v1/printers/{printer.id}/slots/0/3/configure",
|
|
f"/api/v1/printers/{printer.id}/slots/0/3/configure",
|
|
|
params={
|
|
params={
|
|
|
- "tray_info_idx": "GFL05", "tray_type": "PLA",
|
|
|
|
|
- "tray_sub_brands": "PLA Basic", "tray_color": "FFFFFFFF",
|
|
|
|
|
- "nozzle_temp_min": 190, "nozzle_temp_max": 230,
|
|
|
|
|
- "cali_idx": -1, "k_value": 0.0,
|
|
|
|
|
|
|
+ "tray_info_idx": "GFL05",
|
|
|
|
|
+ "tray_type": "PLA",
|
|
|
|
|
+ "tray_sub_brands": "PLA Basic",
|
|
|
|
|
+ "tray_color": "FFFFFFFF",
|
|
|
|
|
+ "nozzle_temp_min": 190,
|
|
|
|
|
+ "nozzle_temp_max": 230,
|
|
|
|
|
+ "cali_idx": -1,
|
|
|
|
|
+ "k_value": 0.0,
|
|
|
},
|
|
},
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
assert response.status_code == 200
|
|
assert response.status_code == 200
|
|
|
- sm_kps = (await db_session.execute(
|
|
|
|
|
- select(SpoolmanKProfile).where(SpoolmanKProfile.spoolman_spool_id == 216)
|
|
|
|
|
- )).scalars().all()
|
|
|
|
|
|
|
+ sm_kps = (
|
|
|
|
|
+ (await db_session.execute(select(SpoolmanKProfile).where(SpoolmanKProfile.spoolman_spool_id == 216)))
|
|
|
|
|
+ .scalars()
|
|
|
|
|
+ .all()
|
|
|
|
|
+ )
|
|
|
assert len(sm_kps) == 0 # cali_idx=-1 means "no profile" — don't write
|
|
assert len(sm_kps) == 0 # cali_idx=-1 means "no profile" — don't write
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.asyncio
|
|
|
@pytest.mark.integration
|
|
@pytest.mark.integration
|
|
|
async def test_zero_cali_idx_persists(
|
|
async def test_zero_cali_idx_persists(
|
|
|
- self, async_client: AsyncClient, db_session, printer_factory,
|
|
|
|
|
|
|
+ self,
|
|
|
|
|
+ async_client: AsyncClient,
|
|
|
|
|
+ db_session,
|
|
|
|
|
+ printer_factory,
|
|
|
):
|
|
):
|
|
|
"""cali_idx=0 is the first valid profile slot (NOT a sentinel for missing)."""
|
|
"""cali_idx=0 is the first valid profile slot (NOT a sentinel for missing)."""
|
|
|
from backend.app.models.spoolman_k_profile import SpoolmanKProfile
|
|
from backend.app.models.spoolman_k_profile import SpoolmanKProfile
|
|
|
from backend.app.models.spoolman_slot_assignment import SpoolmanSlotAssignment
|
|
from backend.app.models.spoolman_slot_assignment import SpoolmanSlotAssignment
|
|
|
|
|
|
|
|
printer = await printer_factory(model="H2D")
|
|
printer = await printer_factory(model="H2D")
|
|
|
- db_session.add(SpoolmanSlotAssignment(
|
|
|
|
|
- printer_id=printer.id, ams_id=0, tray_id=3, spoolman_spool_id=216,
|
|
|
|
|
- ))
|
|
|
|
|
|
|
+ db_session.add(
|
|
|
|
|
+ SpoolmanSlotAssignment(
|
|
|
|
|
+ printer_id=printer.id,
|
|
|
|
|
+ ams_id=0,
|
|
|
|
|
+ tray_id=3,
|
|
|
|
|
+ spoolman_spool_id=216,
|
|
|
|
|
+ )
|
|
|
|
|
+ )
|
|
|
await db_session.commit()
|
|
await db_session.commit()
|
|
|
|
|
|
|
|
mock_client = MagicMock()
|
|
mock_client = MagicMock()
|
|
@@ -2202,33 +2558,45 @@ class TestConfigureAmsSlotPersistsKProfile:
|
|
|
response = await async_client.post(
|
|
response = await async_client.post(
|
|
|
f"/api/v1/printers/{printer.id}/slots/0/3/configure",
|
|
f"/api/v1/printers/{printer.id}/slots/0/3/configure",
|
|
|
params={
|
|
params={
|
|
|
- "tray_info_idx": "GFL05", "tray_type": "PLA",
|
|
|
|
|
- "tray_sub_brands": "PLA Basic", "tray_color": "FFFFFFFF",
|
|
|
|
|
- "nozzle_temp_min": 190, "nozzle_temp_max": 230,
|
|
|
|
|
- "cali_idx": 0, "k_value": 0.020,
|
|
|
|
|
|
|
+ "tray_info_idx": "GFL05",
|
|
|
|
|
+ "tray_type": "PLA",
|
|
|
|
|
+ "tray_sub_brands": "PLA Basic",
|
|
|
|
|
+ "tray_color": "FFFFFFFF",
|
|
|
|
|
+ "nozzle_temp_min": 190,
|
|
|
|
|
+ "nozzle_temp_max": 230,
|
|
|
|
|
+ "cali_idx": 0,
|
|
|
|
|
+ "k_value": 0.020,
|
|
|
},
|
|
},
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
assert response.status_code == 200
|
|
assert response.status_code == 200
|
|
|
- kp = (await db_session.execute(
|
|
|
|
|
- select(SpoolmanKProfile).where(SpoolmanKProfile.spoolman_spool_id == 216)
|
|
|
|
|
- )).scalar_one_or_none()
|
|
|
|
|
|
|
+ kp = (
|
|
|
|
|
+ await db_session.execute(select(SpoolmanKProfile).where(SpoolmanKProfile.spoolman_spool_id == 216))
|
|
|
|
|
+ ).scalar_one_or_none()
|
|
|
assert kp is not None
|
|
assert kp is not None
|
|
|
assert kp.cali_idx == 0 # explicitly testing 0 is valid
|
|
assert kp.cali_idx == 0 # explicitly testing 0 is valid
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.asyncio
|
|
|
@pytest.mark.integration
|
|
@pytest.mark.integration
|
|
|
async def test_upsert_idempotent(
|
|
async def test_upsert_idempotent(
|
|
|
- self, async_client: AsyncClient, db_session, printer_factory,
|
|
|
|
|
|
|
+ self,
|
|
|
|
|
+ async_client: AsyncClient,
|
|
|
|
|
+ db_session,
|
|
|
|
|
+ printer_factory,
|
|
|
):
|
|
):
|
|
|
"""Repeated POSTs update the same row (UNIQUE on spool_id+printer+extruder+nozzle_diameter)."""
|
|
"""Repeated POSTs update the same row (UNIQUE on spool_id+printer+extruder+nozzle_diameter)."""
|
|
|
from backend.app.models.spoolman_k_profile import SpoolmanKProfile
|
|
from backend.app.models.spoolman_k_profile import SpoolmanKProfile
|
|
|
from backend.app.models.spoolman_slot_assignment import SpoolmanSlotAssignment
|
|
from backend.app.models.spoolman_slot_assignment import SpoolmanSlotAssignment
|
|
|
|
|
|
|
|
printer = await printer_factory(model="H2D")
|
|
printer = await printer_factory(model="H2D")
|
|
|
- db_session.add(SpoolmanSlotAssignment(
|
|
|
|
|
- printer_id=printer.id, ams_id=0, tray_id=3, spoolman_spool_id=216,
|
|
|
|
|
- ))
|
|
|
|
|
|
|
+ db_session.add(
|
|
|
|
|
+ SpoolmanSlotAssignment(
|
|
|
|
|
+ printer_id=printer.id,
|
|
|
|
|
+ ams_id=0,
|
|
|
|
|
+ tray_id=3,
|
|
|
|
|
+ spoolman_spool_id=216,
|
|
|
|
|
+ )
|
|
|
|
|
+ )
|
|
|
await db_session.commit()
|
|
await db_session.commit()
|
|
|
|
|
|
|
|
mock_client = MagicMock()
|
|
mock_client = MagicMock()
|
|
@@ -2248,27 +2616,37 @@ class TestConfigureAmsSlotPersistsKProfile:
|
|
|
await async_client.post(
|
|
await async_client.post(
|
|
|
f"/api/v1/printers/{printer.id}/slots/0/3/configure",
|
|
f"/api/v1/printers/{printer.id}/slots/0/3/configure",
|
|
|
params={
|
|
params={
|
|
|
- "tray_info_idx": "GFL05", "tray_type": "PLA",
|
|
|
|
|
- "tray_sub_brands": "PLA Basic", "tray_color": "FFFFFFFF",
|
|
|
|
|
- "nozzle_temp_min": 190, "nozzle_temp_max": 230,
|
|
|
|
|
- "cali_idx": 5, "k_value": 0.020,
|
|
|
|
|
|
|
+ "tray_info_idx": "GFL05",
|
|
|
|
|
+ "tray_type": "PLA",
|
|
|
|
|
+ "tray_sub_brands": "PLA Basic",
|
|
|
|
|
+ "tray_color": "FFFFFFFF",
|
|
|
|
|
+ "nozzle_temp_min": 190,
|
|
|
|
|
+ "nozzle_temp_max": 230,
|
|
|
|
|
+ "cali_idx": 5,
|
|
|
|
|
+ "k_value": 0.020,
|
|
|
},
|
|
},
|
|
|
)
|
|
)
|
|
|
# Second call with cali_idx=10 (same slot/spool/extruder/nozzle)
|
|
# Second call with cali_idx=10 (same slot/spool/extruder/nozzle)
|
|
|
await async_client.post(
|
|
await async_client.post(
|
|
|
f"/api/v1/printers/{printer.id}/slots/0/3/configure",
|
|
f"/api/v1/printers/{printer.id}/slots/0/3/configure",
|
|
|
params={
|
|
params={
|
|
|
- "tray_info_idx": "GFL05", "tray_type": "PLA",
|
|
|
|
|
- "tray_sub_brands": "PLA Matte", "tray_color": "FFFFFFFF",
|
|
|
|
|
- "nozzle_temp_min": 190, "nozzle_temp_max": 230,
|
|
|
|
|
- "cali_idx": 10, "k_value": 0.025,
|
|
|
|
|
|
|
+ "tray_info_idx": "GFL05",
|
|
|
|
|
+ "tray_type": "PLA",
|
|
|
|
|
+ "tray_sub_brands": "PLA Matte",
|
|
|
|
|
+ "tray_color": "FFFFFFFF",
|
|
|
|
|
+ "nozzle_temp_min": 190,
|
|
|
|
|
+ "nozzle_temp_max": 230,
|
|
|
|
|
+ "cali_idx": 10,
|
|
|
|
|
+ "k_value": 0.025,
|
|
|
},
|
|
},
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
# Should be exactly ONE row (updated), not two
|
|
# Should be exactly ONE row (updated), not two
|
|
|
- kps = (await db_session.execute(
|
|
|
|
|
- select(SpoolmanKProfile).where(SpoolmanKProfile.spoolman_spool_id == 216)
|
|
|
|
|
- )).scalars().all()
|
|
|
|
|
|
|
+ kps = (
|
|
|
|
|
+ (await db_session.execute(select(SpoolmanKProfile).where(SpoolmanKProfile.spoolman_spool_id == 216)))
|
|
|
|
|
+ .scalars()
|
|
|
|
|
+ .all()
|
|
|
|
|
+ )
|
|
|
assert len(kps) == 1
|
|
assert len(kps) == 1
|
|
|
assert kps[0].cali_idx == 10 # updated to most recent
|
|
assert kps[0].cali_idx == 10 # updated to most recent
|
|
|
assert kps[0].name == "PLA Matte"
|
|
assert kps[0].name == "PLA Matte"
|
|
@@ -2276,7 +2654,10 @@ class TestConfigureAmsSlotPersistsKProfile:
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.asyncio
|
|
|
@pytest.mark.integration
|
|
@pytest.mark.integration
|
|
|
async def test_external_slot_extruder_inversion(
|
|
async def test_external_slot_extruder_inversion(
|
|
|
- self, async_client: AsyncClient, db_session, printer_factory,
|
|
|
|
|
|
|
+ self,
|
|
|
|
|
+ async_client: AsyncClient,
|
|
|
|
|
+ db_session,
|
|
|
|
|
+ printer_factory,
|
|
|
):
|
|
):
|
|
|
"""ams_id=255 + tray_id=0 → kp.extruder=1 (ext-L); tray_id=1 → extruder=0 (ext-R)."""
|
|
"""ams_id=255 + tray_id=0 → kp.extruder=1 (ext-L); tray_id=1 → extruder=0 (ext-R)."""
|
|
|
from backend.app.models.spool import Spool
|
|
from backend.app.models.spool import Spool
|
|
@@ -2290,9 +2671,14 @@ class TestConfigureAmsSlotPersistsKProfile:
|
|
|
# Note: SpoolmanSlotAssignment can't store ams_id=255 with tray_id=1
|
|
# Note: SpoolmanSlotAssignment can't store ams_id=255 with tray_id=1
|
|
|
# under the ck_tray_id_range constraint (0-3 valid). External-slot
|
|
# under the ck_tray_id_range constraint (0-3 valid). External-slot
|
|
|
# K-profile persistence is therefore tested via local SpoolAssignment.
|
|
# K-profile persistence is therefore tested via local SpoolAssignment.
|
|
|
- db_session.add(SpoolAssignment(
|
|
|
|
|
- spool_id=spool.id, printer_id=printer.id, ams_id=255, tray_id=0,
|
|
|
|
|
- ))
|
|
|
|
|
|
|
+ db_session.add(
|
|
|
|
|
+ SpoolAssignment(
|
|
|
|
|
+ spool_id=spool.id,
|
|
|
|
|
+ printer_id=printer.id,
|
|
|
|
|
+ ams_id=255,
|
|
|
|
|
+ tray_id=0,
|
|
|
|
|
+ )
|
|
|
|
|
+ )
|
|
|
await db_session.commit()
|
|
await db_session.commit()
|
|
|
spool_id = spool.id
|
|
spool_id = spool.id
|
|
|
|
|
|
|
@@ -2312,17 +2698,21 @@ class TestConfigureAmsSlotPersistsKProfile:
|
|
|
response = await async_client.post(
|
|
response = await async_client.post(
|
|
|
f"/api/v1/printers/{printer.id}/slots/255/0/configure",
|
|
f"/api/v1/printers/{printer.id}/slots/255/0/configure",
|
|
|
params={
|
|
params={
|
|
|
- "tray_info_idx": "GFL05", "tray_type": "PLA",
|
|
|
|
|
- "tray_sub_brands": "PLA Basic", "tray_color": "FFFFFFFF",
|
|
|
|
|
- "nozzle_temp_min": 190, "nozzle_temp_max": 230,
|
|
|
|
|
- "cali_idx": 5, "k_value": 0.020,
|
|
|
|
|
|
|
+ "tray_info_idx": "GFL05",
|
|
|
|
|
+ "tray_type": "PLA",
|
|
|
|
|
+ "tray_sub_brands": "PLA Basic",
|
|
|
|
|
+ "tray_color": "FFFFFFFF",
|
|
|
|
|
+ "nozzle_temp_min": 190,
|
|
|
|
|
+ "nozzle_temp_max": 230,
|
|
|
|
|
+ "cali_idx": 5,
|
|
|
|
|
+ "k_value": 0.020,
|
|
|
},
|
|
},
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
assert response.status_code == 200
|
|
assert response.status_code == 200
|
|
|
- kp = (await db_session.execute(
|
|
|
|
|
- select(SpoolKProfile).where(SpoolKProfile.spool_id == spool_id)
|
|
|
|
|
- )).scalar_one_or_none()
|
|
|
|
|
|
|
+ kp = (
|
|
|
|
|
+ await db_session.execute(select(SpoolKProfile).where(SpoolKProfile.spool_id == spool_id))
|
|
|
|
|
+ ).scalar_one_or_none()
|
|
|
assert kp is not None
|
|
assert kp is not None
|
|
|
# tray_id=0 → extruder = 1 - 0 = 1
|
|
# tray_id=0 → extruder = 1 - 0 = 1
|
|
|
assert kp.extruder == 1
|
|
assert kp.extruder == 1
|
|
@@ -2330,7 +2720,10 @@ class TestConfigureAmsSlotPersistsKProfile:
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.asyncio
|
|
|
@pytest.mark.integration
|
|
@pytest.mark.integration
|
|
|
async def test_dual_nozzle_extruder_persists(
|
|
async def test_dual_nozzle_extruder_persists(
|
|
|
- self, async_client: AsyncClient, db_session, printer_factory,
|
|
|
|
|
|
|
+ self,
|
|
|
|
|
+ async_client: AsyncClient,
|
|
|
|
|
+ db_session,
|
|
|
|
|
+ printer_factory,
|
|
|
):
|
|
):
|
|
|
"""ams_extruder_map with extruder=1 → kp.extruder=1 persisted correctly."""
|
|
"""ams_extruder_map with extruder=1 → kp.extruder=1 persisted correctly."""
|
|
|
from backend.app.models.spool import Spool
|
|
from backend.app.models.spool import Spool
|
|
@@ -2341,9 +2734,14 @@ class TestConfigureAmsSlotPersistsKProfile:
|
|
|
spool = Spool(material="PLA")
|
|
spool = Spool(material="PLA")
|
|
|
db_session.add(spool)
|
|
db_session.add(spool)
|
|
|
await db_session.flush()
|
|
await db_session.flush()
|
|
|
- db_session.add(SpoolAssignment(
|
|
|
|
|
- spool_id=spool.id, printer_id=printer.id, ams_id=2, tray_id=3,
|
|
|
|
|
- ))
|
|
|
|
|
|
|
+ db_session.add(
|
|
|
|
|
+ SpoolAssignment(
|
|
|
|
|
+ spool_id=spool.id,
|
|
|
|
|
+ printer_id=printer.id,
|
|
|
|
|
+ ams_id=2,
|
|
|
|
|
+ tray_id=3,
|
|
|
|
|
+ )
|
|
|
|
|
+ )
|
|
|
await db_session.commit()
|
|
await db_session.commit()
|
|
|
spool_id = spool.id
|
|
spool_id = spool.id
|
|
|
|
|
|
|
@@ -2363,24 +2761,31 @@ class TestConfigureAmsSlotPersistsKProfile:
|
|
|
response = await async_client.post(
|
|
response = await async_client.post(
|
|
|
f"/api/v1/printers/{printer.id}/slots/2/3/configure",
|
|
f"/api/v1/printers/{printer.id}/slots/2/3/configure",
|
|
|
params={
|
|
params={
|
|
|
- "tray_info_idx": "GFL05", "tray_type": "PLA",
|
|
|
|
|
- "tray_sub_brands": "PLA Basic", "tray_color": "FFFFFFFF",
|
|
|
|
|
- "nozzle_temp_min": 190, "nozzle_temp_max": 230,
|
|
|
|
|
- "cali_idx": 5, "k_value": 0.020,
|
|
|
|
|
|
|
+ "tray_info_idx": "GFL05",
|
|
|
|
|
+ "tray_type": "PLA",
|
|
|
|
|
+ "tray_sub_brands": "PLA Basic",
|
|
|
|
|
+ "tray_color": "FFFFFFFF",
|
|
|
|
|
+ "nozzle_temp_min": 190,
|
|
|
|
|
+ "nozzle_temp_max": 230,
|
|
|
|
|
+ "cali_idx": 5,
|
|
|
|
|
+ "k_value": 0.020,
|
|
|
},
|
|
},
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
assert response.status_code == 200
|
|
assert response.status_code == 200
|
|
|
- kp = (await db_session.execute(
|
|
|
|
|
- select(SpoolKProfile).where(SpoolKProfile.spool_id == spool_id)
|
|
|
|
|
- )).scalar_one_or_none()
|
|
|
|
|
|
|
+ kp = (
|
|
|
|
|
+ await db_session.execute(select(SpoolKProfile).where(SpoolKProfile.spool_id == spool_id))
|
|
|
|
|
+ ).scalar_one_or_none()
|
|
|
assert kp is not None
|
|
assert kp is not None
|
|
|
assert kp.extruder == 1
|
|
assert kp.extruder == 1
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.asyncio
|
|
|
@pytest.mark.integration
|
|
@pytest.mark.integration
|
|
|
async def test_db_error_does_not_fail_endpoint(
|
|
async def test_db_error_does_not_fail_endpoint(
|
|
|
- self, async_client: AsyncClient, db_session, printer_factory,
|
|
|
|
|
|
|
+ self,
|
|
|
|
|
+ async_client: AsyncClient,
|
|
|
|
|
+ db_session,
|
|
|
|
|
+ printer_factory,
|
|
|
):
|
|
):
|
|
|
"""DB errors during K-profile persistence are best-effort — endpoint still returns 200.
|
|
"""DB errors during K-profile persistence are best-effort — endpoint still returns 200.
|
|
|
|
|
|
|
@@ -2393,9 +2798,14 @@ class TestConfigureAmsSlotPersistsKProfile:
|
|
|
from backend.app.models.spoolman_slot_assignment import SpoolmanSlotAssignment
|
|
from backend.app.models.spoolman_slot_assignment import SpoolmanSlotAssignment
|
|
|
|
|
|
|
|
printer = await printer_factory(model="H2D")
|
|
printer = await printer_factory(model="H2D")
|
|
|
- db_session.add(SpoolmanSlotAssignment(
|
|
|
|
|
- printer_id=printer.id, ams_id=0, tray_id=3, spoolman_spool_id=216,
|
|
|
|
|
- ))
|
|
|
|
|
|
|
+ db_session.add(
|
|
|
|
|
+ SpoolmanSlotAssignment(
|
|
|
|
|
+ printer_id=printer.id,
|
|
|
|
|
+ ams_id=0,
|
|
|
|
|
+ tray_id=3,
|
|
|
|
|
+ spoolman_spool_id=216,
|
|
|
|
|
+ )
|
|
|
|
|
+ )
|
|
|
await db_session.commit()
|
|
await db_session.commit()
|
|
|
|
|
|
|
|
mock_client = MagicMock()
|
|
mock_client = MagicMock()
|
|
@@ -2424,10 +2834,14 @@ class TestConfigureAmsSlotPersistsKProfile:
|
|
|
response = await async_client.post(
|
|
response = await async_client.post(
|
|
|
f"/api/v1/printers/{printer.id}/slots/0/3/configure",
|
|
f"/api/v1/printers/{printer.id}/slots/0/3/configure",
|
|
|
params={
|
|
params={
|
|
|
- "tray_info_idx": "GFL05", "tray_type": "PLA",
|
|
|
|
|
- "tray_sub_brands": "PLA Basic", "tray_color": "FFFFFFFF",
|
|
|
|
|
- "nozzle_temp_min": 190, "nozzle_temp_max": 230,
|
|
|
|
|
- "cali_idx": 5, "k_value": 0.020,
|
|
|
|
|
|
|
+ "tray_info_idx": "GFL05",
|
|
|
|
|
+ "tray_type": "PLA",
|
|
|
|
|
+ "tray_sub_brands": "PLA Basic",
|
|
|
|
|
+ "tray_color": "FFFFFFFF",
|
|
|
|
|
+ "nozzle_temp_min": 190,
|
|
|
|
|
+ "nozzle_temp_max": 230,
|
|
|
|
|
+ "cali_idx": 5,
|
|
|
|
|
+ "k_value": 0.020,
|
|
|
},
|
|
},
|
|
|
)
|
|
)
|
|
|
|
|
|