mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-08-11 00:30:12 -04:00
fix(vp): accumulate cached push_status per-field instead of allowlist (#1622)
Bridge cache replaced prev state wholesale on each incremental, re-merging only a 14-key allowlist. Capability/lifecycle fields (cali_version, print_type, mc_print_stage, device, ...) drained out within one 1Hz tick, greying out BambuStudio's Device-tab UIs (manage-calibration, AMS-slot dropdown) once the cache thinned. Most P1S users miss it by timing — they click Device tab while the cache is still fat from the connect pushall. Switch to per-field accumulate matching bambu_mqtt.py's internal state handler: prev keys carry over verbatim when not present in the incoming push, new values overwrite when present. _merge_ams_dict for partial AMS blobs unchanged (#1387 / #1371 regression guards stay green). _SLICER_VISIBLE_STICKY_KEYS removed — new logic is a strict superset.
This commit is contained in:
parent
282aefc564
commit
da799447f6
3 changed files with 106 additions and 64 deletions
File diff suppressed because one or more lines are too long
|
|
@ -52,38 +52,18 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
REFRESH_INTERVAL_SECONDS = 30.0
|
||||
|
||||
# Top-level push_status fields that Bambu firmware sends in FULL pushall
|
||||
# responses (on `pushall` request / printer reconnect) but typically OMITS
|
||||
# from 1 Hz incremental push_status updates. Without preserving these
|
||||
# fields across incremental updates, the bridge cache would lose AMS info
|
||||
# (and friends) between pushalls — slicers reading the cache would see a
|
||||
# stripped-down state and the fix would only re-appear on a manual printer
|
||||
# power-cycle (#1371). Mirrors the same set Bambuddy itself preserves in
|
||||
# bambu_mqtt.py:2686-2711 for its own internal raw_data, with a few more
|
||||
# entries that the slicer cares about (net, ipcam, lights_report).
|
||||
_SLICER_VISIBLE_STICKY_KEYS: tuple[str, ...] = (
|
||||
"ams",
|
||||
"vt_tray",
|
||||
"ams_extruder_map",
|
||||
"mapping",
|
||||
"net",
|
||||
"ipcam",
|
||||
"lights_report",
|
||||
# Pre-flight / Prepare-tab fields that BambuStudio reads off cached
|
||||
# push_status. Bambu firmware emits them in full pushall but typically
|
||||
# OMITS them from 1 Hz incremental updates, so without sticky-preservation
|
||||
# the cache drops them after the very next tick and the slicer's
|
||||
# "block Send while busy / unknown firmware" branch kicks in. Same shape
|
||||
# as #1228 (storage indicators) and #1558 (live-progress fields) —
|
||||
# cached-branch field-shape parity, not a new mechanism.
|
||||
"upgrade_state", # Send pre-flight reads dis_state / force_upgrade
|
||||
"xcam", # Prepare-tab reads spaghetti / first-layer / halt sensitivity
|
||||
"hw_switch_state", # Hardware switch state (Prepare tab)
|
||||
"nozzle_diameter",
|
||||
"nozzle_type",
|
||||
"online", # Module online map (ahb / rfid / version)
|
||||
"ams_status", # AMS overall status; can be ams_status-only incremental
|
||||
)
|
||||
# Bambuddy's internal printer state in bambu_mqtt.py (around line 2686+) is
|
||||
# updated per-field — each `if "X" in data: self.state.X = ...` block leaves
|
||||
# every other field untouched, so the state accumulates everything the
|
||||
# printer has ever sent. The bridge cache below mirrors that pattern: when
|
||||
# the incoming push_status omits a field, the previous value is preserved
|
||||
# verbatim; only fields actually present in the new push overwrite. This
|
||||
# stops capability/lifecycle fields (cali_version, print_type, mc_print_stage,
|
||||
# device, ...) draining out of the cache between pushalls, which surfaced
|
||||
# as #1622 (BambuStudio's Device-tab UIs greying out on P1S after the
|
||||
# cache drained to a thin incremental snapshot). The `ams` field still
|
||||
# gets unit-/tray-level deep merge via `_merge_ams_dict` because firmware
|
||||
# sends partial `ams` blobs under the same key (#1387).
|
||||
|
||||
|
||||
def _ip_to_uint32_le(ip_str: str) -> int:
|
||||
|
|
@ -602,39 +582,32 @@ class MQTTBridge:
|
|||
new_state = copy.deepcopy(print_data)
|
||||
# Bambu firmware sends two kinds of push_status: full pushall
|
||||
# responses (on `pushall` requests / printer reconnect) which
|
||||
# include AMS, vt_tray, net, etc. — and ~1 Hz incremental
|
||||
# updates with just the fields that changed (typically temps,
|
||||
# fan, wifi). Without preserving sticky fields from the previous
|
||||
# cache, the first incremental push after a pushall would wipe
|
||||
# AMS info from the bridge cache, and slicers reading the cache
|
||||
# between pushalls would see a stripped-down printer state with
|
||||
# no AMS visible until the next pushall — typically only when
|
||||
# the user power-cycles the printer (#1371). Mirror the same
|
||||
# preservation pattern Bambuddy uses for its own internal state
|
||||
# in bambu_mqtt.py (see _SLICER_VISIBLE_STICKY_KEYS below).
|
||||
# include the full top-level field set (AMS, vt_tray, net,
|
||||
# cali_version, print_type, mc_print_stage, device, ...) — and
|
||||
# ~1 Hz incrementals with just the fields that changed (temps,
|
||||
# fan, wifi, status). Carry over every prev field the incoming
|
||||
# push doesn't overwrite, mirroring the per-field accumulate
|
||||
# pattern in bambu_mqtt.py's internal state handler — without
|
||||
# this the cache thins out to whatever the latest incremental
|
||||
# carried (~17 keys on P1S in #1622), and the slicer's Device-
|
||||
# tab capability gates (manage-calibration, AMS-assign dropdown,
|
||||
# …) flip off because their gating fields drained from the
|
||||
# cache. The deep-copy is defensive: without it the carried-
|
||||
# over nested dicts/lists are shared with the previous cache,
|
||||
# so any in-place mutation later would corrupt both.
|
||||
prev = self._latest_print_state
|
||||
if prev is not None:
|
||||
for sticky_key in _SLICER_VISIBLE_STICKY_KEYS:
|
||||
if sticky_key not in new_state:
|
||||
if sticky_key in prev:
|
||||
# Defensive deep copy — without this the carried-over
|
||||
# nested dicts/lists are shared between new_state and
|
||||
# the previous cache, so any in-place mutation later
|
||||
# (current or future code paths) would corrupt both.
|
||||
new_state[sticky_key] = copy.deepcopy(prev[sticky_key])
|
||||
continue
|
||||
# Key IS in new_state — but firmware sends partial blobs
|
||||
# (status-only / tray-targeted) under the same key on
|
||||
# incremental updates, which would overwrite the cached
|
||||
# full blob and break the slicer's AMS render (#1387).
|
||||
# For `ams` specifically the deep-merge mirrors what
|
||||
# Bambuddy already does internally in `_handle_ams_data`.
|
||||
if (
|
||||
sticky_key == "ams"
|
||||
and isinstance(new_state.get("ams"), dict)
|
||||
and isinstance(prev.get("ams"), dict)
|
||||
):
|
||||
new_state["ams"] = _merge_ams_dict(prev["ams"], new_state["ams"])
|
||||
for prev_key, prev_value in prev.items():
|
||||
if prev_key not in new_state:
|
||||
new_state[prev_key] = copy.deepcopy(prev_value)
|
||||
# Firmware sends partial `ams` blobs (status-only / unit-
|
||||
# targeted / tray-targeted) under the same key on
|
||||
# incremental updates, which would overwrite the cached
|
||||
# full blob and break the slicer's AMS render (#1387 /
|
||||
# #1371). Deep-merge mirrors what bambu_mqtt.py does
|
||||
# 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"])
|
||||
self._latest_print_state = new_state
|
||||
dump_wire(self.vp_name, "in", new_state)
|
||||
return
|
||||
|
|
|
|||
|
|
@ -402,6 +402,73 @@ class TestPushStatusCache:
|
|||
|
||||
await bridge.stop()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_incremental_push_preserves_non_allowlisted_capability_fields(self):
|
||||
"""Regression for #1622: BambuStudio gates Device-tab UIs (manage
|
||||
calibration, AMS-slot filament dropdown, ...) on capability /
|
||||
lifecycle fields (cali_version, print_type, mc_print_stage,
|
||||
device, ...) it reads off the cached push_status. Before the fix
|
||||
these fields were not in the allowlist and drained out of the
|
||||
bridge cache on the first 1 Hz incremental tick, so the slicer's
|
||||
Device tab would grey out the gated UIs once the cache thinned.
|
||||
After the fix the cache accumulates everything the printer has
|
||||
ever sent, dropped only when explicitly overwritten.
|
||||
"""
|
||||
server = _make_server()
|
||||
bridge = _make_bridge(server)
|
||||
await bridge.start()
|
||||
|
||||
full_push = json.dumps(
|
||||
{
|
||||
"print": {
|
||||
"command": "push_status",
|
||||
"cali_version": 2,
|
||||
"print_type": "idle",
|
||||
"gcode_state": "IDLE",
|
||||
"mc_print_stage": "0",
|
||||
"mc_stage": 0,
|
||||
"device": {"ext_tool": {"info": []}},
|
||||
"cfg": "",
|
||||
"home_flag": 256,
|
||||
"wifi_signal": "-50dBm",
|
||||
}
|
||||
}
|
||||
).encode()
|
||||
bridge._on_printer_raw(f"device/{H2D_SERIAL}/report", full_push)
|
||||
await asyncio.sleep(0.01)
|
||||
|
||||
# Incremental push carrying only temps + wifi — none of the
|
||||
# capability/lifecycle fields above are mentioned.
|
||||
incremental_push = json.dumps(
|
||||
{
|
||||
"print": {
|
||||
"command": "push_status",
|
||||
"wifi_signal": "-55dBm",
|
||||
"nozzle_temper": 24.5,
|
||||
}
|
||||
}
|
||||
).encode()
|
||||
bridge._on_printer_raw(f"device/{H2D_SERIAL}/report", incremental_push)
|
||||
await asyncio.sleep(0.01)
|
||||
|
||||
cached = bridge.get_latest_print_state()
|
||||
# Incremental values applied.
|
||||
assert cached["wifi_signal"] == "-55dBm"
|
||||
assert cached["nozzle_temper"] == 24.5
|
||||
# Capability / lifecycle fields preserved from the prior pushall
|
||||
# — the symptoms in #1622 (Device-tab UIs disabled) trace to these
|
||||
# exact keys missing.
|
||||
assert cached["cali_version"] == 2
|
||||
assert cached["print_type"] == "idle"
|
||||
assert cached["gcode_state"] == "IDLE"
|
||||
assert cached["mc_print_stage"] == "0"
|
||||
assert cached["mc_stage"] == 0
|
||||
assert cached["device"] == {"ext_tool": {"info": []}}
|
||||
assert cached["cfg"] == ""
|
||||
assert cached["home_flag"] == 256
|
||||
|
||||
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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue