Procházet zdrojové kódy

fix(vp): overlay incoming dict-shaped push_status fields onto cache instead of replacing (#1622 round 5)

  Right after the slicer picks a filament for the external spool (vt_tray, ams_id=255),
  Bambu firmware pushes a partial vt_tray carrying just {tray_info_idx, tray_color} -
  ~18 fields shorter than the pushall shape the slicer expects. The #1622 round-4
  per-field accumulate (da799447) only carried over prev keys NOT in new, so the
  cached vt_tray was replaced wholesale with the 2-field partial. The next 1 Hz
  cached-as-base push delivered the stripped dict and BambuStudio rendered the
  external slot as invalid (color only, no tray_type / state / k / n / cali_idx /
  nozzle_temp_*). Reload restored it because the reconnect-triggered pushall
  re-seeded vt_tray, then the cycle repeated. AMS slots didn't suffer because
  _merge_ams_dict deep-merged them.

  Fix: for every top-level push_status key whose prev AND new are both dicts,
  overlay incoming keys onto prev rather than replace. ams is excluded (already
  deep-merged). The same shape protects device / online / upgrade_state / ipcam /
  upload / net against future firmware partials. net.info IP rewrite is unaffected -
  _rewrite_net_info_ips runs before caching and overlay lets the freshly-rewritten
  list win over prev when present.
maziggy před 2 měsíci
rodič
revize
9c4252911b

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 0 - 0
CHANGELOG.md


+ 23 - 0
backend/app/services/virtual_printer/mqtt_bridge.py

@@ -608,6 +608,29 @@ class MQTTBridge:
                 # internally in `_handle_ams_data`.
                 if isinstance(new_state.get("ams"), dict) and isinstance(prev.get("ams"), dict):
                     new_state["ams"] = _merge_ams_dict(prev["ams"], new_state["ams"])
+                # Same per-field accumulate rule applied one level deeper for
+                # other top-level dict-shaped fields. Firmware sends partial
+                # `vt_tray` (external spool) updates right after a slicer
+                # `ams_filament_setting` pick — typically just `{tray_info_idx,
+                # tray_color}`, dropping the ~18 other fields (`tray_type`,
+                # `state`, `remain`, `k`, `n`, `cali_idx`, `nozzle_temp_min/max`,
+                # `tray_uuid`, `xcam_info`, ...) the slicer needs to render the
+                # slot. Without overlay the next 1 Hz cached-as-base push
+                # delivered the stripped dict and the slicer rendered the
+                # external slot as "invalid" until a reload triggered a fresh
+                # pushall (#1622 round 5, reported by @shaddowlink). AMS slots
+                # didn't suffer because `_merge_ams_dict` deep-merges per tray.
+                # Same shape covers `device`, `online`, `upgrade_state`, `ipcam`,
+                # `upload`, `net`, ... against future firmware partials too.
+                # `ams` is excluded — already deep-merged above.
+                for key, new_value in list(new_state.items()):
+                    if key == "ams":
+                        continue
+                    prev_value = prev.get(key)
+                    if isinstance(prev_value, dict) and isinstance(new_value, dict):
+                        merged = dict(prev_value)
+                        merged.update(new_value)
+                        new_state[key] = merged
             self._latest_print_state = new_state
             dump_wire(self.vp_name, "in", new_state)
             return

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

@@ -469,6 +469,100 @@ class TestPushStatusCache:
 
         await bridge.stop()
 
+    @pytest.mark.asyncio
+    async def test_partial_vt_tray_update_overlays_onto_cached_full_dict(self):
+        """Regression for #1622 round 5 (reported by @shaddowlink): right after
+        the slicer picks a filament for the external spool (vt_tray, ams_id=255),
+        Bambu firmware pushes a partial vt_tray carrying just the changed
+        fields — typically ``{tray_info_idx, tray_color}`` — and omits the
+        ~18 other keys (tray_type, state, k, n, cali_idx, nozzle_temp_min/max,
+        tray_uuid, xcam_info, ...) the slicer needs to render the slot.
+        Before this fix the per-field accumulate replaced the cached vt_tray
+        wholesale (it only carried over prev keys NOT present in new), so the
+        next 1 Hz cached-as-base push handed the slicer a stripped vt_tray and
+        BambuStudio rendered the external slot as "invalid" until a reload
+        triggered a fresh pushall. AMS slots didn't suffer because
+        `_merge_ams_dict` already deep-merged them. The fix overlays incoming
+        keys onto the previous dict for every top-level dict-shaped field
+        (excluding ams, which keeps its own deep merge).
+        """
+        server = _make_server()
+        bridge = _make_bridge(server)
+        await bridge.start()
+
+        # 1. Pushall response with the full ~20-field vt_tray dict a real
+        # P1S sends to bootstrap the slot.
+        full_push = json.dumps(
+            {
+                "print": {
+                    "command": "push_status",
+                    "vt_tray": {
+                        "id": "254",
+                        "tray_info_idx": "Pea5f68f",
+                        "tray_type": "PLA",
+                        "tray_sub_brands": "",
+                        "tray_color": "F72323FF",
+                        "tray_weight": "0",
+                        "tray_diameter": "0.00",
+                        "tray_temp": "0",
+                        "tray_time": "0",
+                        "bed_temp_type": "0",
+                        "bed_temp": "0",
+                        "nozzle_temp_max": "240",
+                        "nozzle_temp_min": "190",
+                        "xcam_info": "000000000000000000000000",
+                        "tray_uuid": "00000000000000000000000000000000",
+                        "ctype": 0,
+                        "remain": -1,
+                        "k": 0.01999999955296,
+                        "n": 1,
+                        "cali_idx": -1,
+                        "state": 3,
+                    },
+                }
+            }
+        ).encode()
+        bridge._on_printer_raw(f"device/{H2D_SERIAL}/report", full_push)
+        await asyncio.sleep(0.01)
+
+        # 2. Incremental push carrying just the two fields the slicer's pick
+        # changed — exactly the shape the P1S firmware sends after an
+        # ams_filament_setting ack. This is what shaddowlink's wire dump
+        # captured for the failing case.
+        incremental_push = json.dumps(
+            {
+                "print": {
+                    "command": "push_status",
+                    "vt_tray": {
+                        "tray_info_idx": "Pea5f68f",
+                        "tray_color": "76D9F4FF",
+                    },
+                }
+            }
+        ).encode()
+        bridge._on_printer_raw(f"device/{H2D_SERIAL}/report", incremental_push)
+        await asyncio.sleep(0.01)
+
+        cached = bridge.get_latest_print_state()
+        vt = cached["vt_tray"]
+        # Incoming fields applied.
+        assert vt["tray_info_idx"] == "Pea5f68f"
+        assert vt["tray_color"] == "76D9F4FF"
+        # All other fields preserved from the prior pushall — without these
+        # the slicer rendered the slot as invalid.
+        assert vt["tray_type"] == "PLA"
+        assert vt["state"] == 3
+        assert vt["remain"] == -1
+        assert vt["k"] == 0.01999999955296
+        assert vt["n"] == 1
+        assert vt["cali_idx"] == -1
+        assert vt["nozzle_temp_min"] == "190"
+        assert vt["nozzle_temp_max"] == "240"
+        assert vt["tray_uuid"] == "00000000000000000000000000000000"
+        assert vt["id"] == "254"
+
+        await bridge.stop()
+
     @pytest.mark.asyncio
     async def test_partial_ams_status_update_preserves_unit_list(self):
         """#1387: Bambu firmware also sends `ams` updates where the key is

Některé soubory nejsou zobrazeny, neboť je v těchto rozdílových datech změněno mnoho souborů