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

fix(vp): emit FINISH after FTP upload so Print-flow slicers unwedge (#1280)

  Bambuddy's VP supports two slicer flows: Send (file upload only — what
  queue/immediate/review modes are designed for) and Print (file upload
  + start-print, intended for proxy mode). When a user clicks Print
  against a non-proxy mode the VP must still respond gracefully — the
  file is fine to receive, just the start-print never happens. Instead
  the slicer wedged at "Downloading...(0%)" and blocked the next
  dispatch with "The printer is busy with another print job".

  Cause: on_file_received transitioned gcode_state PREPARE -> IDLE
  directly. Print-flow slicers watch the state cycle and only release
  their in-flight-job lock on PREPARE -> ... -> FINISH (or FAILED).
  PREPARE -> IDLE looks like "printer abandoned my job" and keeps the
  prior job pinned in the slicer's memory.

  Fix: transition PREPARE -> FINISH with prepare_percent=100. The 1-Hz
  periodic status push broadcasts the new state to every connected
  slicer within a second. Send-flow slicers don't watch this state so
  the change is a no-op for them; Print-flow slicers see the FINISH
  they were waiting for and unwedge.
maziggy 3 месяцев назад
Родитель
Сommit
0d6171dc9a

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
CHANGELOG.md


+ 11 - 2
backend/app/services/virtual_printer/manager.py

@@ -217,9 +217,18 @@ class VirtualPrinterInstance:
         else:
             await self._queue_file(file_path, source_ip)
 
-        # Reset MQTT status back to IDLE
+        # Signal job completion to the slicer. Send-flow slicers don't watch the
+        # post-upload state and would be happy with anything; the Print flow
+        # (intended for proxy-mode VPs, but users sometimes click it against
+        # queue/immediate/review modes too — #1280) watches the gcode_state
+        # cycle and only releases its in-flight-job lock when it sees FINISH.
+        # Going PREPARE → IDLE wedges the slicer's UI at "Downloading...(0%)"
+        # and blocks the next dispatch with "busy with another print job".
+        # PREPARE → FINISH satisfies both flows. prepare_percent=100 also
+        # unfreezes the slicer's "Downloading X%" progress bar which it ticks
+        # against the same field during the upload window.
         if self._mqtt and file_path.suffix.lower() == ".3mf":
-            self._mqtt.set_gcode_state("IDLE")
+            self._mqtt.set_gcode_state("FINISH", filename=file_path.name, prepare_percent="100")
 
     async def on_print_command(self, filename: str, data: dict) -> None:
         """Handle print command from MQTT."""

+ 35 - 0
backend/tests/unit/services/test_virtual_printer.py

@@ -171,6 +171,41 @@ class TestVirtualPrinterInstance:
 
             mock_archive.assert_called_once_with(file_path, "192.168.1.100")
 
+    @pytest.mark.asyncio
+    async def test_on_file_received_signals_FINISH_to_slicer(self, instance):
+        """Regression #1280: when a slicer's Print flow uploads to a non-proxy VP,
+        the VP must transition gcode_state PREPARE → FINISH so the slicer's
+        in-flight-job lock releases. Going PREPARE → IDLE wedges Orca at
+        "Downloading...(0%)" and blocks the next dispatch with "busy with
+        another print job".
+
+        Send-flow slicers don't watch the post-upload state, so this is a
+        no-op behavior change for them.
+        """
+        instance.mode = "immediate"
+        instance._mqtt = MagicMock()
+        instance._mqtt.set_gcode_state = MagicMock()
+        file_path = Path("/tmp/test.3mf")  # nosec B108
+
+        with patch.object(instance, "_archive_file", new_callable=AsyncMock):
+            await instance.on_file_received(file_path, "192.168.1.100")
+
+        instance._mqtt.set_gcode_state.assert_called_once_with("FINISH", filename="test.3mf", prepare_percent="100")
+
+    @pytest.mark.asyncio
+    async def test_on_file_received_non_3mf_does_not_touch_state(self, instance):
+        """Non-3MF uploads (e.g., a job's auxiliary files) must not transition
+        the visible state — the slicer is only tracking the .3mf upload."""
+        instance.mode = "immediate"
+        instance._mqtt = MagicMock()
+        instance._mqtt.set_gcode_state = MagicMock()
+        file_path = Path("/tmp/test.gcode")  # nosec B108
+
+        with patch.object(instance, "_archive_file", new_callable=AsyncMock):
+            await instance.on_file_received(file_path, "192.168.1.100")
+
+        instance._mqtt.set_gcode_state.assert_not_called()
+
     @pytest.mark.asyncio
     async def test_archive_file_skips_non_3mf(self, instance):
         """Verify non-3MF files are skipped and cleaned up."""

Некоторые файлы не были показаны из-за большого количества измененных файлов