Browse Source

fix(vp): overlay storage indicators on cached push so slicer pre-flight passes for P1S/A1 targets (issue #1228)

  Slicer "Send to printer" worked on 0.2.3.2 with a queue-mode VP and
  started failing on 0.2.4b3 with BambuStudio's generic "storage needs
  to be inserted before send to printer" error. Multiple users
  reported it across P1S, P2S, Docker bridge, macvlan, and host
  networking. @rtadams89's debug-level support archive showed the
  smoking gun: slicer establishes MQTT TLS, gets pushall +
  get_version, then never opens an FTP connection — pre-flight
  rejects before any data transfer.

  The 0.2.3.2 synthetic stub baked in three SD/storage indicators
  that BambuStudio's "Send" pre-flight reads: home_flag with bit 8
  (HAS_SDCARD_NORMAL, 0x100), sdcard=True, and a storage:{free,total}
  block. The 0.2.4b3 cached-as-base slicer-mirror (7dea33d0) passes
  the live target's push_status through with only an IP rewrite — if
  the real firmware doesn't report those fields (P1S/A1 with no SD
  card, older field shapes, confirmed on P1S firmware 01.10.00.00),
  the slicer sees "no storage" and aborts. H2D and X1C reproductions
  worked because those firmwares do report the indicators.

  In _send_status_report's cached-as-base branch, after copying the
  cache and applying the existing protocol/upload-state overrides:

  - home_flag |= 0x100 (preserves any other bits the real printer set)
  - sdcard = True (force-set even when real says False)
  - storage = setdefault(...) (only fills in if missing — real values
    pass through unchanged when the printer reports them)

  For VP usage the slicer uploads via FTPS to Bambuddy's filesystem
  at /app/data/virtual_printer/uploads/<vpid>/; the printer's actual
  SD card is irrelevant on that path, so forcing "storage available"
  is correct for the queue / immediate / review modes the
  cached-as-base path covers.
maziggy 2 weeks ago
parent
commit
ceffcfaef6

File diff suppressed because it is too large
+ 0 - 0
CHANGELOG.md


+ 14 - 0
backend/app/services/virtual_printer/mqtt_server.py

@@ -659,6 +659,20 @@ class SimpleMQTTServer:
                 else:
                 else:
                     # Don't override real subtask_name with empty if no upload pending.
                     # Don't override real subtask_name with empty if no upload pending.
                     print_block.setdefault("subtask_name", "")
                     print_block.setdefault("subtask_name", "")
+                # Storage-availability indicators the slicer's "Send" pre-flight reads
+                # (#1228). P1S/A1-class firmware doesn't always include these in
+                # push_status (no SD card inserted, older field shapes), and BambuStudio
+                # rejects the send pre-flight with the generic "storage needs to be
+                # inserted before send to printer" error before even attempting FTP.
+                # For VP usage the slicer uploads via FTPS to Bambuddy's filesystem —
+                # the printer's actual SD/storage state is irrelevant on that path.
+                # Force "available" indicators so the pre-flight passes regardless of
+                # what the real printer reports. Restores the 0.2.3.2 synthetic-stub
+                # behaviour for these fields without losing the live AMS / k-profile /
+                # camera mirror cached-as-base provides.
+                print_block["home_flag"] = print_block.get("home_flag", 0) | 0x100  # bit 8 = HAS_SDCARD_NORMAL
+                print_block["sdcard"] = True
+                print_block.setdefault("storage", {"free": 1_000_000_000, "total": 32_000_000_000})
                 status = {"print": print_block}
                 status = {"print": print_block}
                 await self._publish_to_report(writer, status, serial or self.serial)
                 await self._publish_to_report(writer, status, serial or self.serial)
                 return
                 return

+ 59 - 0
backend/tests/unit/test_vp_mqtt_bridge.py

@@ -393,6 +393,65 @@ class TestStatusReportCachedAsBase:
         assert payload["print"]["nozzle_type"] == "hardened_steel"
         assert payload["print"]["nozzle_type"] == "hardened_steel"
         assert "storage" in payload["print"]
         assert "storage" in payload["print"]
 
 
+    @pytest.mark.asyncio
+    async def test_storage_indicators_overlaid_for_send_preflight(self):
+        """#1228: P1S/A1-class firmware doesn't always include the SD/storage
+        fields BambuStudio's "Send" pre-flight reads. Without these the
+        slicer rejects with 'storage needs to be inserted' before even
+        attempting FTP. The cached-as-base path now overlays them so the
+        pre-flight passes regardless of what the real printer reports.
+        """
+        server = _make_server()
+        bridge = MagicMock()
+        # Real P1S push without SD card inserted: home_flag has other bits set
+        # but the SD bit (0x100) is clear; sdcard is False; no storage field.
+        bridge.get_latest_print_state.return_value = {
+            "command": "push_status",
+            "msg": 0,
+            "home_flag": 0x42,
+            "sdcard": False,
+        }
+        server.set_bridge(bridge)
+        published = self._capture_published(server)
+
+        await server._send_status_report(MagicMock())
+        _serial, payload = published[0]
+        # SD bit ORed onto whatever was there — other bits preserved.
+        assert payload["print"]["home_flag"] & 0x100 == 0x100
+        assert payload["print"]["home_flag"] & 0x42 == 0x42
+        # Force-set so a False from the printer doesn't trip the pre-flight.
+        assert payload["print"]["sdcard"] is True
+        # storage was missing — the overlay must inject a non-empty default.
+        assert "storage" in payload["print"]
+        assert payload["print"]["storage"]["free"] > 0
+        assert payload["print"]["storage"]["total"] > 0
+
+    @pytest.mark.asyncio
+    async def test_storage_indicators_preserve_real_storage_when_present(self):
+        """When the real printer DOES report a storage block, pass it through
+        unchanged (the overlay only fills in the missing field, not overrides).
+        """
+        server = _make_server()
+        bridge = MagicMock()
+        real_storage = {"free": 12345, "total": 67890}
+        bridge.get_latest_print_state.return_value = {
+            "command": "push_status",
+            "msg": 0,
+            "home_flag": 0x100,  # SD bit already set on the real printer
+            "sdcard": True,
+            "storage": real_storage,
+        }
+        server.set_bridge(bridge)
+        published = self._capture_published(server)
+
+        await server._send_status_report(MagicMock())
+        _serial, payload = published[0]
+        # SD bit OR is idempotent — already-set bit stays set.
+        assert payload["print"]["home_flag"] == 0x100
+        assert payload["print"]["sdcard"] is True
+        # Real values pass through, NOT the synthetic defaults.
+        assert payload["print"]["storage"] == real_storage
+
     @pytest.mark.asyncio
     @pytest.mark.asyncio
     async def test_overrides_protocol_fields_even_when_cache_present(self):
     async def test_overrides_protocol_fields_even_when_cache_present(self):
         """Cached value's gcode_state must NOT win over our local upload-state-machine value."""
         """Cached value's gcode_state must NOT win over our local upload-state-machine value."""

Some files were not shown because too many files changed in this diff