mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-18 21:47:39 +02:00
fix(vp): preserve AMS/vt_tray/net across incremental push_status updates (#1371)
The bridge cache replaced _latest_print_state wholesale on every push_status arrival. Bambu firmware sends full pushall responses (with AMS/vt_tray/net.info/lights_report) on reconnect / pushall requests, but ~1 Hz incremental updates with only the fields that changed. The first incremental push after a pushall therefore wiped AMS info from the bridge cache, and slicers reading the cache (via the VP's 1 Hz status push) saw a stripped-down state with no AMS visible until the next pushall — typically only on a manual printer power-cycle. Preserve a small set of slicer-visible sticky keys from the previous cache when the incoming push doesn't carry them: ams, vt_tray, ams_extruder_map, mapping, net, ipcam, lights_report. Mirrors the same pattern Bambuddy uses for its own internal state.raw_data.
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -48,6 +48,25 @@ 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",
|
||||
)
|
||||
|
||||
|
||||
def _ip_to_uint32_le(ip_str: str) -> int:
|
||||
"""Encode dotted-quad IPv4 as little-endian uint32 (Bambu MQTT's `net.info[].ip` shape)."""
|
||||
@@ -261,7 +280,25 @@ class MQTTBridge:
|
||||
entry["ip"] = self._vp_ip_uint32_le
|
||||
# Defensive deep copy on store so the cache is fully decoupled from
|
||||
# the freshly-parsed tree and from any reader's reference.
|
||||
self._latest_print_state = copy.deepcopy(print_data)
|
||||
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).
|
||||
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 and sticky_key in prev:
|
||||
new_state[sticky_key] = prev[sticky_key]
|
||||
self._latest_print_state = new_state
|
||||
return
|
||||
|
||||
# info.get_version responses → cache the module list so the synthetic
|
||||
|
||||
@@ -233,6 +233,124 @@ class TestPushStatusCache:
|
||||
assert bridge.get_latest_print_state() is None
|
||||
await bridge.stop()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_incremental_push_preserves_ams_from_previous_cache(self):
|
||||
"""Regression for #1371: Bambu firmware sends FULL push_status on
|
||||
pushall (with AMS/vt_tray/net/etc.) but typically OMITS those fields
|
||||
from 1 Hz incremental push_status updates. Without preserving the
|
||||
sticky keys across pushes, the cache forgets AMS info after the first
|
||||
incremental update, and BambuStudio (which reads the cache via the
|
||||
VP's 1 Hz status push) sees no AMS info until the user power-cycles
|
||||
the printer (forcing a fresh pushall).
|
||||
"""
|
||||
server = _make_server()
|
||||
bridge = _make_bridge(server)
|
||||
await bridge.start()
|
||||
|
||||
# 1. Initial pushall response with full state, AMS included.
|
||||
full_push = json.dumps(
|
||||
{
|
||||
"print": {
|
||||
"command": "push_status",
|
||||
"gcode_state": "IDLE",
|
||||
"wifi_signal": "-50dBm",
|
||||
"ams": {
|
||||
"ams": [
|
||||
{
|
||||
"id": "0",
|
||||
"tray": [
|
||||
{"id": "0", "tray_type": "PLA", "tray_color": "FF0000FF"},
|
||||
{"id": "1", "tray_type": "PETG", "tray_color": "00FF00FF"},
|
||||
],
|
||||
}
|
||||
],
|
||||
"tray_exist_bits": "3",
|
||||
},
|
||||
"vt_tray": {"id": "254", "tray_type": ""},
|
||||
"lights_report": [{"node": "chamber_light", "mode": "on"}],
|
||||
}
|
||||
}
|
||||
).encode()
|
||||
bridge._on_printer_raw(f"device/{H2D_SERIAL}/report", full_push)
|
||||
await asyncio.sleep(0.01)
|
||||
|
||||
cached = bridge.get_latest_print_state()
|
||||
assert cached["ams"]["ams"][0]["tray"][0]["tray_type"] == "PLA"
|
||||
assert cached["vt_tray"]["id"] == "254"
|
||||
assert cached["lights_report"][0]["mode"] == "on"
|
||||
|
||||
# 2. Incremental push with only temp/wifi changes — NO ams field.
|
||||
# This is what the printer sends every ~1 s between full pushalls.
|
||||
incremental_push = json.dumps(
|
||||
{
|
||||
"print": {
|
||||
"command": "push_status",
|
||||
"wifi_signal": "-55dBm",
|
||||
"chamber_temper": 26.0,
|
||||
}
|
||||
}
|
||||
).encode()
|
||||
bridge._on_printer_raw(f"device/{H2D_SERIAL}/report", incremental_push)
|
||||
await asyncio.sleep(0.01)
|
||||
|
||||
cached = bridge.get_latest_print_state()
|
||||
# New fields take effect.
|
||||
assert cached["wifi_signal"] == "-55dBm"
|
||||
assert cached["chamber_temper"] == 26.0
|
||||
# Sticky fields preserved from the previous cache (the #1371 fix).
|
||||
assert "ams" in cached, "AMS field must be preserved across incremental pushes (#1371)"
|
||||
assert cached["ams"]["ams"][0]["tray"][0]["tray_type"] == "PLA"
|
||||
assert cached["ams"]["tray_exist_bits"] == "3"
|
||||
assert cached["vt_tray"]["id"] == "254"
|
||||
assert cached["lights_report"][0]["mode"] == "on"
|
||||
|
||||
await bridge.stop()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_incoming_ams_update_replaces_cached_ams(self):
|
||||
"""Counterpart to the #1371 fix: preservation only kicks in when the
|
||||
incoming push OMITS a sticky key. When the printer DOES send a fresh
|
||||
`ams` value (e.g. on a pushall, or when AMS state genuinely changes),
|
||||
that value must take effect — the preservation must not shadow real
|
||||
updates.
|
||||
"""
|
||||
server = _make_server()
|
||||
bridge = _make_bridge(server)
|
||||
await bridge.start()
|
||||
|
||||
# 1. Initial state: PLA in tray 0.
|
||||
bridge._on_printer_raw(
|
||||
f"device/{H2D_SERIAL}/report",
|
||||
json.dumps(
|
||||
{
|
||||
"print": {
|
||||
"command": "push_status",
|
||||
"ams": {"ams": [{"id": "0", "tray": [{"id": "0", "tray_type": "PLA"}]}]},
|
||||
}
|
||||
}
|
||||
).encode(),
|
||||
)
|
||||
await asyncio.sleep(0.01)
|
||||
|
||||
# 2. Fresh push with PETG — must replace, not get shadowed by the old PLA.
|
||||
bridge._on_printer_raw(
|
||||
f"device/{H2D_SERIAL}/report",
|
||||
json.dumps(
|
||||
{
|
||||
"print": {
|
||||
"command": "push_status",
|
||||
"ams": {"ams": [{"id": "0", "tray": [{"id": "0", "tray_type": "PETG"}]}]},
|
||||
}
|
||||
}
|
||||
).encode(),
|
||||
)
|
||||
await asyncio.sleep(0.01)
|
||||
|
||||
cached = bridge.get_latest_print_state()
|
||||
assert cached["ams"]["ams"][0]["tray"][0]["tray_type"] == "PETG"
|
||||
|
||||
await bridge.stop()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Caching: get_version response
|
||||
|
||||
Reference in New Issue
Block a user