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

Fix external spool print failing on printers without AMS (#854)

  Printers with no AMS hardware (P1S/P1P with only external spool)
  rejected print commands with "Failed to get AMS mapping table"
  because use_ams was always sent as true. Now auto-sets use_ams=false
  when all filament slots map to external spools or are unmapped.
  H2D-series excluded since they use use_ams for nozzle routing.
maziggy 5 месяцев назад
Родитель
Сommit
8b332f638f
3 измененных файлов с 48 добавлено и 0 удалено
  1. 1 0
      CHANGELOG.md
  2. 11 0
      backend/app/services/bambu_mqtt.py
  3. 36 0
      backend/tests/unit/services/test_bambu_mqtt.py

+ 1 - 0
CHANGELOG.md

@@ -13,6 +13,7 @@ All notable changes to Bambuddy will be documented in this file.
 - **Queue Page Visual Refresh** — Compact stats bar replaces the five summary cards (saves vertical space), color-coded left borders on all queue items for instant status scanning, collapsible history section (collapsed by default), and condensed single-line rows for history items showing more prints at a glance.
 - **Queue Page Visual Refresh** — Compact stats bar replaces the five summary cards (saves vertical space), color-coded left borders on all queue items for instant status scanning, collapsible history section (collapsed by default), and condensed single-line rows for history items showing more prints at a glance.
 
 
 ### Fixed
 ### Fixed
+- **External Spool Print Fails on P1S/P1P Without AMS** ([#854](https://github.com/maziggy/bambuddy/issues/854)) — Sending a print job to a printer with no AMS units and only an external spool (virtual tray 254) caused the printer to reject the command with "Failed to get AMS mapping table". The print command was sent with `use_ams: true` (the default), but firmware on printers without AMS hardware rejects that combination. Now automatically sets `use_ams: false` when all filament slots map to external spools or are unmapped. H2D-series printers are excluded since they use `use_ams` for nozzle routing. Reported by @UVCXanth.
 - **External Folder Scan 500 Error on 3MF Files** ([#846](https://github.com/maziggy/bambuddy/issues/846)) — Scanning an external folder containing .3mf files crashed with "Object of type bytes is not JSON serializable". The parsed 3MF metadata contained raw thumbnail bytes (`_thumbnail_data`) that were stored directly in the database JSON column without cleaning. Also removed a call to the non-existent `parser.extract_thumbnail()` method — thumbnail data is already available in the parsed metadata. Now uses the same `clean_metadata()` pattern as upload and zip extraction. Reported by @SMAW.
 - **External Folder Scan 500 Error on 3MF Files** ([#846](https://github.com/maziggy/bambuddy/issues/846)) — Scanning an external folder containing .3mf files crashed with "Object of type bytes is not JSON serializable". The parsed 3MF metadata contained raw thumbnail bytes (`_thumbnail_data`) that were stored directly in the database JSON column without cleaning. Also removed a call to the non-existent `parser.extract_thumbnail()` method — thumbnail data is already available in the parsed metadata. Now uses the same `clean_metadata()` pattern as upload and zip extraction. Reported by @SMAW.
 - **Archives Capped at 50 Items** ([#843](https://github.com/maziggy/bambuddy/issues/843)) — The archives page only showed the 50 most recent prints due to a hardcoded API limit. Users with more than 50 archives could not see or access older entries. Fixed by fetching all archives and adding client-side pagination with configurable page sizes (25, 50, 100, 200, or All). Page size preference is persisted. Reported by @dcbaldwin.
 - **Archives Capped at 50 Items** ([#843](https://github.com/maziggy/bambuddy/issues/843)) — The archives page only showed the 50 most recent prints due to a hardcoded API limit. Users with more than 50 archives could not see or access older entries. Fixed by fetching all archives and adding client-side pagination with configurable page sizes (25, 50, 100, 200, or All). Page size preference is persisted. Reported by @dcbaldwin.
 - **Filament Usage Not Recorded When Auto-Archive Disabled** — When a printer had "Auto-archive completed prints" turned off, filament consumption was silently lost. The `on_print_complete` callback returned early before reaching the usage tracking code, so neither the internal inventory (AMS remain% deltas) nor Spoolman received usage data. Moved filament tracking to run before the archive check so usage is always recorded regardless of the auto-archive setting.
 - **Filament Usage Not Recorded When Auto-Archive Disabled** — When a printer had "Auto-archive completed prints" turned off, filament consumption was silently lost. The `on_print_complete` callback returned early before reaching the usage tracking code, so neither the internal inventory (AMS remain% deltas) nor Spoolman received usage data. Moved filament tracking to run before the archive check so usage is always recorded regardless of the auto-archive setting.

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

@@ -2749,6 +2749,17 @@ class BambuMQTTClient:
             # Other printers (X1C, P1S, A1, etc.) require actual booleans for all fields
             # Other printers (X1C, P1S, A1, etc.) require actual booleans for all fields
             is_h2d = self.model and self.model.upper().strip() in ("H2D", "H2D PRO", "H2DPRO", "H2C", "H2S")
             is_h2d = self.model and self.model.upper().strip() in ("H2D", "H2D PRO", "H2DPRO", "H2C", "H2S")
 
 
+            # If all mapped slots are external spool (no real AMS trays), force use_ams=False.
+            # P1S/P1P with no AMS rejects use_ams=True with "Failed to get AMS mapping table".
+            # Skip for H2D series — use_ams controls nozzle routing on those printers.
+            if ams_mapping and use_ams and not is_h2d:
+                if all(t is None or int(t) < 0 or int(t) >= 254 for t in ams_mapping):
+                    use_ams = False
+                    logger.info(
+                        "[%s] All filament slots use external spool — setting use_ams=False",
+                        self.serial_number,
+                    )
+
             command = {
             command = {
                 "print": {
                 "print": {
                     "sequence_id": "20000",
                     "sequence_id": "20000",

+ 36 - 0
backend/tests/unit/services/test_bambu_mqtt.py

@@ -2967,6 +2967,42 @@ class TestStartPrintAmsMapping:
             {"ams_id": 255, "slot_id": 0},
             {"ams_id": 255, "slot_id": 0},
         ]
         ]
 
 
+    def test_external_spool_only_sets_use_ams_false(self, mqtt_client):
+        """Single external spool on non-H2D printer sets use_ams=False."""
+        mqtt_client.start_print("test.3mf", ams_mapping=[254], use_ams=True)
+
+        cmd = self._get_published_command(mqtt_client)
+        assert cmd["use_ams"] is False
+
+    def test_all_unmapped_sets_use_ams_false(self, mqtt_client):
+        """All unmapped slots on non-H2D printer sets use_ams=False."""
+        mqtt_client.start_print("test.3mf", ams_mapping=[-1, -1], use_ams=True)
+
+        cmd = self._get_published_command(mqtt_client)
+        assert cmd["use_ams"] is False
+
+    def test_mixed_ams_and_external_keeps_use_ams_true(self, mqtt_client):
+        """AMS tray + external spool keeps use_ams=True."""
+        mqtt_client.start_print("test.3mf", ams_mapping=[0, 254], use_ams=True)
+
+        cmd = self._get_published_command(mqtt_client)
+        assert cmd["use_ams"] is True
+
+    def test_h2d_both_external_keeps_use_ams_true(self, mqtt_client):
+        """H2D with both external spools keeps use_ams=True (nozzle routing)."""
+        mqtt_client.model = "H2D"
+        mqtt_client.start_print("test.3mf", ams_mapping=[254, 255], use_ams=True)
+
+        cmd = self._get_published_command(mqtt_client)
+        assert cmd["use_ams"] is True
+
+    def test_empty_ams_mapping_keeps_use_ams_true(self, mqtt_client):
+        """Empty ams_mapping list does not override use_ams."""
+        mqtt_client.start_print("test.3mf", ams_mapping=[], use_ams=True)
+
+        cmd = self._get_published_command(mqtt_client)
+        assert cmd["use_ams"] is True
+
     def test_no_ams_mapping_omits_fields(self, mqtt_client):
     def test_no_ams_mapping_omits_fields(self, mqtt_client):
         """When ams_mapping is None, neither field is in the command."""
         """When ams_mapping is None, neither field is in the command."""
         mqtt_client.start_print("test.3mf", ams_mapping=None)
         mqtt_client.start_print("test.3mf", ams_mapping=None)