| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131 |
- """The request-topic probe must not latch on a single unexplained drop (#2953).
- Bambuddy subscribes to the printer's own request topic to intercept the
- ``ams_mapping`` a slicer sends with a print. A1-class printers refuse: their
- broker kills the TCP connection instead of returning a SUBACK failure, so the
- only signal is "we subscribed and then got disconnected". That signal is
- circumstantial. Every other reason a connection can drop inside the same few
- seconds -- a network blip, the printer rebooting, the container being stopped
- mid-probe -- looks exactly the same.
- Latching on the first one costs ams_mapping capture for the rest of the
- process on a printer that supports the topic perfectly well, and every Studio
- print after that is charged to a spool picked by tray position. Requiring the
- drop to repeat costs a printer that genuinely refuses one extra reconnect.
- """
- from types import SimpleNamespace
- import pytest
- from backend.app.services.bambu_mqtt import BambuMQTTClient
- @pytest.fixture(autouse=True)
- def _clear_class_state():
- BambuMQTTClient._request_topic_cache.clear()
- BambuMQTTClient._request_topic_probe_failures.clear()
- yield
- BambuMQTTClient._request_topic_cache.clear()
- BambuMQTTClient._request_topic_probe_failures.clear()
- def _client(serial="SER2953"):
- client = BambuMQTTClient(ip_address="10.0.0.9", serial_number=serial, access_code="12345678")
- client._stale_reconnecting = False
- client._last_message_time = 0.0
- client.last_connect_error = None
- return client
- def _mid_probe(client):
- """Put the client where it is right after subscribing to the request topic."""
- import time
- client._request_topic_sub_mid = 7
- client._request_topic_sub_time = time.time()
- client._request_topic_confirmed = False
- def _drop(client):
- client._on_disconnect(None, None, disconnect_flags=None, rc=SimpleNamespace(is_failure=True))
- def test_one_drop_keeps_the_request_topic_enabled():
- """A blip, as far as we can tell. Try again on the next connection."""
- client = _client()
- _mid_probe(client)
- _drop(client)
- assert client._request_topic_supported is True
- assert BambuMQTTClient._request_topic_cache.get("SER2953") is None
- assert BambuMQTTClient._request_topic_probe_failures["SER2953"] == 1
- def test_a_second_drop_disables_it():
- """The A1's answer: same response every time. Stop asking, and stop
- causing a reconnect on every connection."""
- client = _client()
- _mid_probe(client)
- _drop(client)
- _mid_probe(client)
- _drop(client)
- assert client._request_topic_supported is False
- assert BambuMQTTClient._request_topic_cache["SER2953"] is False
- def test_a_new_client_for_a_disabled_printer_does_not_re_probe():
- """Unchanged: once disabled, later instances skip the subscription
- entirely rather than reopening the reconnect loop."""
- client = _client()
- _mid_probe(client)
- _drop(client)
- _mid_probe(client)
- _drop(client)
- assert _client()._request_topic_supported is False
- def test_a_successful_suback_clears_the_count():
- """A printer that answered once has answered. A later isolated drop must
- start from zero, not from a half-spent budget."""
- client = _client()
- _mid_probe(client)
- _drop(client)
- assert BambuMQTTClient._request_topic_probe_failures["SER2953"] == 1
- client._request_topic_sub_mid = 7
- client._on_subscribe(None, None, 7, [SimpleNamespace(is_failure=False, value=0, getName=lambda: "ok")])
- assert BambuMQTTClient._request_topic_cache["SER2953"] is True
- assert "SER2953" not in BambuMQTTClient._request_topic_probe_failures
- def test_a_suback_rejection_still_disables_immediately():
- """A SUBACK failure is the broker answering the question, not evidence
- about it. One is enough."""
- client = _client()
- client._request_topic_sub_mid = 7
- client._on_subscribe(None, None, 7, [SimpleNamespace(is_failure=True, value=135, getName=lambda: "Not authorized")])
- assert client._request_topic_supported is False
- assert BambuMQTTClient._request_topic_cache["SER2953"] is False
- def test_a_disconnect_we_asked_for_is_not_evidence():
- """Shutting the container down mid-probe used to count against the
- printer. ``disconnect()`` sets the event before closing the socket."""
- import threading
- client = _client()
- _mid_probe(client)
- client._disconnection_event = threading.Event()
- _drop(client)
- assert client._request_topic_supported is True
- assert "SER2953" not in BambuMQTTClient._request_topic_probe_failures
|