| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173 |
- """RFID refresh reports what the printer said, and falls back to M620 R (#3206).
- An X1C on X1Plus (base 01.08.02.00) answers ams_get_rfid with
- ``result: FAIL, reason: ERROR STATE``, yet the refresh returned success and
- scheduled the K-profile re-apply. The refresh now waits for the answer, sends
- the legacy ``M620 R<global tray>`` gcode Bambu Studio uses for printers without
- the new protocol when ams_get_rfid is refused, and reports a refusal of both.
- """
- import json
- from unittest.mock import MagicMock, patch
- import pytest
- from backend.app.services.bambu_mqtt import BambuMQTTClient
- from backend.app.utils.printer_models import uses_legacy_rfid_refresh
- def _client(answers: dict[str, dict | None], model: str = "X1C") -> tuple[BambuMQTTClient, list[dict]]:
- """A connected, idle client whose printer answers each command per ``answers``.
- ``answers`` maps a command name to the result fields the printer sends
- back, or None for no answer at all. Returns the client and the list of
- commands it published.
- """
- client = BambuMQTTClient(ip_address="10.0.0.1", serial_number="X1C", access_code="c", model=model)
- client._client = MagicMock()
- client.state.connected = True
- client.state.tray_now = 255
- sent: list[dict] = []
- def publish(_topic, body, **_kw):
- command = json.loads(body)["print"]
- sent.append(command)
- answer = answers.get(command["command"])
- if answer is not None:
- client._process_message({"print": {**command, **answer}})
- client._client.publish.side_effect = publish
- return client, sent
- ACCEPT = {"result": "SUCCESS", "reason": "SUCCESS"}
- REFUSE = {"result": "FAIL", "reason": "ERROR STATE"}
- class TestRfidRefreshAck:
- @pytest.mark.asyncio
- async def test_accepted_sends_only_ams_get_rfid(self):
- client, sent = _client({"ams_get_rfid": ACCEPT})
- ok, message = await client.ams_refresh_tray(0, 3)
- assert ok
- assert message == "Refreshing AMS 0 tray 3"
- assert [c["command"] for c in sent] == ["ams_get_rfid"]
- assert sent[0]["ams_id"] == 0 and sent[0]["slot_id"] == 3
- @pytest.mark.asyncio
- async def test_refused_falls_back_to_m620_with_the_global_tray(self):
- client, sent = _client({"ams_get_rfid": REFUSE, "gcode_line": ACCEPT})
- ok, message = await client.ams_refresh_tray(2, 1)
- assert ok
- assert "legacy" in message
- assert [c["command"] for c in sent] == ["ams_get_rfid", "gcode_line"]
- assert sent[1]["param"] == "M620 R9\n"
- @pytest.mark.asyncio
- async def test_both_refused_reports_the_printer_reason(self):
- client, _ = _client({"ams_get_rfid": REFUSE, "gcode_line": REFUSE})
- ok, message = await client.ams_refresh_tray(0, 3)
- assert not ok
- assert "ERROR STATE" in message
- @pytest.mark.asyncio
- async def test_no_fallback_for_units_without_a_legacy_index(self):
- # AMS-HT (128+) has no M620 R index; only firmware with ams_get_rfid has one.
- client, sent = _client({"ams_get_rfid": REFUSE})
- ok, message = await client.ams_refresh_tray(128, 0)
- assert not ok
- assert "ERROR STATE" in message
- assert [c["command"] for c in sent] == ["ams_get_rfid"]
- @pytest.mark.asyncio
- async def test_no_fallback_on_a_newer_protocol_model(self):
- # Bambu Studio never sends M620 R to these; a refusal there is reported.
- client, sent = _client({"ams_get_rfid": REFUSE}, model="H2D")
- ok, message = await client.ams_refresh_tray(0, 1)
- assert not ok
- assert "ERROR STATE" in message
- assert [c["command"] for c in sent] == ["ams_get_rfid"]
- @pytest.mark.asyncio
- @pytest.mark.parametrize("state", ["RUNNING", "PAUSE", "PREPARE", "SLICING"])
- async def test_no_fallback_during_a_job(self, state):
- # A gcode_line would be executed inside the running print.
- client, sent = _client({"ams_get_rfid": REFUSE})
- client.state.state = state
- ok, _ = await client.ams_refresh_tray(0, 1)
- assert not ok
- assert [c["command"] for c in sent] == ["ams_get_rfid"]
- @pytest.mark.asyncio
- async def test_no_answer_counts_as_accepted(self):
- client, sent = _client({})
- client._rfid_ack_timeout = 0.1
- ok, _ = await client.ams_refresh_tray(0, 0)
- assert ok
- assert [c["command"] for c in sent] == ["ams_get_rfid"]
- assert client._pending_rfid_acks == {}
- @pytest.mark.asyncio
- async def test_unrelated_gcode_line_ack_is_not_taken_for_ours(self):
- client, _ = _client({"ams_get_rfid": REFUSE})
- client._process_message({"print": {"command": "gcode_line", "sequence_id": "0", **REFUSE}})
- client._rfid_ack_timeout = 0.1
- ok, _ = await client.ams_refresh_tray(0, 0)
- # ams_get_rfid refused, M620 R sent but unanswered -> accepted.
- assert ok
- class TestRefreshRoute:
- @pytest.mark.asyncio
- async def test_refused_refresh_is_400_and_schedules_no_pa_reapply(self):
- from fastapi import HTTPException
- from backend.app.api.routes import printers as printers_routes
- client, _ = _client({"ams_get_rfid": REFUSE, "gcode_line": REFUSE})
- db = MagicMock()
- result = MagicMock()
- result.scalar_one_or_none.return_value = MagicMock(id=1)
- async def execute(*_a, **_kw):
- return result
- db.execute = execute
- with (
- patch.object(printers_routes, "printer_manager") as pm,
- patch.object(printers_routes, "spawn_background_task") as spawn,
- ):
- pm.get_client.return_value = client
- with pytest.raises(HTTPException) as exc:
- await printers_routes.refresh_ams_slot(1, 0, 3, None, db)
- assert exc.value.status_code == 400
- assert "ERROR STATE" in exc.value.detail
- spawn.assert_not_called()
- class TestLegacyRfidModels:
- @pytest.mark.parametrize("model", ["X1C", "X1", "X1E", "P1P", "P1S", "A1", "A1 Mini", "BL-P001", "C12", "N1"])
- def test_legacy(self, model):
- assert uses_legacy_rfid_refresh(model)
- @pytest.mark.parametrize("model", ["H2D", "H2C", "H2S", "P2S", "A2L", "X2D", "O1D", "N7", "", None])
- def test_not_legacy(self, model):
- assert not uses_legacy_rfid_refresh(model)
|