Просмотр исходного кода

MQTT: a print paused at startup resumes as the same print (#3485)

- a power cut that also restarts Bambuddy brings it back while the
  printer is paused in power-loss recovery; the first state is PAUSE,
  so the printer's PAUSE -> RUNNING was taken for a new print and got
  a second archive, left printing
- a print found paused at startup now resumes through the restart
  recovery a print found running already gets (#1304), so the
  original archive and queue item complete
- also covers a print paused by hand before a restart
maziggy 1 день назад
Родитель
Сommit
f34f939215

+ 11 - 0
backend/app/services/bambu_mqtt.py

@@ -1637,6 +1637,10 @@ class BambuMQTTClient:
         self._previous_gcode_state: str | None = None
         self._previous_gcode_file: str | None = None
         self._was_running: bool = False  # Track if we've seen RUNNING state for current print
+        # #3485: the first state after startup was PAUSE with a print loaded,
+        # e.g. a printer in power-loss recovery when Bambuddy came back. Its
+        # PAUSE -> RUNNING is that print resuming, not a new one.
+        self._paused_at_startup: bool = False
         self._completion_triggered: bool = False  # Prevent duplicate completion triggers
         self._timelapse_during_print: bool = False  # Track if timelapse was active during this print
         # #1721: one-shot guard so the end-of-print stage-22 detector
@@ -5998,13 +6002,20 @@ class BambuMQTTClient:
 
         # Detect print start (state changes TO RUNNING with a file)
         current_file = self.state.gcode_file or self.state.current_print
+        if self._previous_gcode_state is None and self.state.state == "PAUSE" and current_file:
+            self._paused_at_startup = True
         is_new_print = (
             self.state.state == "RUNNING"
             and self._previous_gcode_state is not None  # #1304: skip on first push after Bambuddy startup
             and self._previous_gcode_state != "RUNNING"
             and current_file
             and not self._was_running  # Prevent duplicates when resuming from PAUSE
+            # #3485: a print paused when Bambuddy started resumes like one it
+            # found running (#1304): restart recovery below, not a new print.
+            and not (self._paused_at_startup and self._previous_gcode_state == "PAUSE")
         )
+        if self.state.state != "PAUSE":
+            self._paused_at_startup = False
         # Also detect if file changed while running (new print started)
         is_file_change = (
             self.state.state == "RUNNING"

+ 97 - 0
backend/tests/unit/services/test_resume_after_startup_pause_3485.py

@@ -0,0 +1,97 @@
+"""A print paused when Bambuddy starts resumes as the same print (#3485).
+
+A power cut takes down the printers and the Bambuddy host together. Bambuddy
+comes back while the printers sit paused in power-loss recovery, so the first
+state it sees is PAUSE, and the printer's PAUSE -> RUNNING was taken for a new
+print: a second archive, left "printing". It is now the restart recovery that
+a print found RUNNING at startup already gets (#1304, #1485).
+"""
+
+import pytest
+
+from backend.app.services.bambu_mqtt import BambuMQTTClient
+
+FILE = "chocolate-bite-buddy_plate_2.3mf"
+
+
+@pytest.fixture
+def client():
+    client = BambuMQTTClient(ip_address="192.168.1.100", serial_number="TEST123", access_code="12345678")
+    client.starts = []
+    client.recovered = []
+    client.completions = []
+    client.on_print_start = client.starts.append
+    client.on_print_running_observed = client.recovered.append
+    client.on_print_complete = client.completions.append
+    return client
+
+
+def _push(client, state: str, file: str | None = FILE) -> None:
+    payload = {"gcode_state": state}
+    if file is not None:
+        payload.update(gcode_file=file, subtask_name=file)
+    client._process_message({"print": payload})
+
+
+def test_the_resume_is_not_a_new_print(client):
+    _push(client, "PAUSE")
+    # The resume frame carries no file name, as on the reporter's A1 mini.
+    _push(client, "RUNNING", file=None)
+
+    assert client.starts == []
+    assert len(client.recovered) == 1
+    assert client.recovered[0]["filename"] == FILE
+
+
+def test_the_resumed_print_still_completes(client):
+    _push(client, "PAUSE")
+    _push(client, "RUNNING", file=None)
+    _push(client, "FINISH")
+
+    assert [c["status"] for c in client.completions] == ["completed"]
+
+
+def test_a_later_pause_and_resume_is_still_the_same_print(client):
+    _push(client, "PAUSE")
+    _push(client, "RUNNING", file=None)
+    _push(client, "PAUSE")
+    _push(client, "RUNNING")
+
+    assert client.starts == []
+
+
+def test_a_print_after_the_paused_one_ends_is_new(client):
+    _push(client, "PAUSE")
+    _push(client, "IDLE", file="")
+    _push(client, "RUNNING", file="next.3mf")
+
+    assert len(client.starts) == 1
+    assert client.starts[0]["filename"] == "next.3mf"
+
+
+def test_a_next_print_paused_before_it_runs_is_still_new(client):
+    _push(client, "PAUSE")
+    _push(client, "IDLE", file="")
+    _push(client, "PREPARE", file="next.3mf")
+    _push(client, "PAUSE", file="next.3mf")
+    _push(client, "RUNNING", file="next.3mf")
+
+    assert [s["filename"] for s in client.starts] == ["next.3mf"]
+
+
+def test_a_print_started_after_startup_is_new(client):
+    _push(client, "IDLE", file="")
+    _push(client, "RUNNING")
+
+    assert len(client.starts) == 1
+    assert client.recovered == []
+
+
+def test_a_pause_seen_while_running_resumes_quietly(client):
+    _push(client, "IDLE", file="")
+    _push(client, "RUNNING")
+    _push(client, "PAUSE")
+    _push(client, "RUNNING")
+
+    assert len(client.starts) == 1
+    assert client.recovered == []