fix(ams): stop reading the printer's command acks as status (issue #3040)

Every project_file carried "cfg": "0" — the device-config bitmask, which
Bambu Studio has never sent and the firmware ignores. The printer echoes a
command's fields back in its ack, and the ack was ingested as telemetry, so
bit 18 read as "AMS Filament Backup off" 25 ms after every dispatch.

Families that repeat cfg in their periodic status (P2S, H2C, X2D) corrected
themselves a second later; the P1S, A1, A1 Mini and A2L send it only in a
full status dump, so the wrong value stuck and silently disabled the
prefer-lowest-remaining gate. The A1 family, which reports no cfg at all and
is meant to stay "unknown", was pinned to a definite "off".

Acks are no longer read as status, for the backup bit or the per-job
timelapse flag they also echo, and cfg is gone from the print command.
This commit is contained in:
maziggy
2026-09-16 09:52:24 +02:00
parent 6017981e62
commit 0b830ac35c
3 changed files with 112 additions and 5 deletions
+79 -1
View File
@@ -4699,8 +4699,10 @@ class TestStartPrintUniqueIdentityFields:
assert cmd["url"] == "ftp://test.3mf"
assert cmd["file"] == "test.3mf"
assert cmd["profile_id"] == "0"
assert cmd["cfg"] == "0"
assert cmd["subtask_name"] == "test"
# The device-config bitmask is not a per-job field and is no longer
# sent; the printer echoed it back and we read it as telemetry (#3040).
assert "cfg" not in cmd
class TestDeleteKProfileDualNozzleDetection:
@@ -6951,6 +6953,82 @@ class TestAmsFilamentBackupHoldTimer:
assert mqtt_client._xcam_hold_start["print_option_auto_switch_filament"] == before_hold
class TestCommandAckIsNotTelemetry:
"""Regression (#3040): a printer's command acknowledgement echoes the
fields Bambuddy sent, so ingesting one as status reads our own request
back as the printer's state.
Bambuddy used to put ``"cfg": "0"`` in every project_file. The ack came
back carrying it, bit 18 read as "AMS Filament Backup OFF", and on the
families that don't repeat ``cfg`` in their periodic frames (P1S, A1,
A1 Mini, A2L) the wrong value stuck until the user toggled it — which
silently disabled the prefer-lowest-remaining gate for the rest of the day.
"""
@pytest.fixture
def mqtt_client(self):
from unittest.mock import MagicMock
from backend.app.services.bambu_mqtt import BambuMQTTClient
client = BambuMQTTClient(
ip_address="192.168.1.100",
serial_number="TEST123",
access_code="12345678",
)
client.state.connected = True
client._client = MagicMock()
return client
def test_project_file_ack_does_not_clear_backup_state(self, mqtt_client):
mqtt_client.state.ams_filament_backup = True
mqtt_client._process_message(
{"print": {"command": "project_file", "sequence_id": "20000", "cfg": "0", "result": "success"}}
)
assert mqtt_client.state.ams_filament_backup is True
def test_project_file_ack_leaves_unknown_backup_unknown(self, mqtt_client):
"""A1 / A1 Mini never report cfg, so the state must stay None ("unknown")
— the value the prefer-lowest gate reads as "preserve old behaviour"."""
assert mqtt_client.state.ams_filament_backup is None
mqtt_client._process_message({"print": {"command": "project_file", "cfg": "0"}})
assert mqtt_client.state.ams_filament_backup is None
def test_push_status_still_updates_backup_state(self, mqtt_client):
mqtt_client.state.ams_filament_backup = True
mqtt_client._process_message({"print": {"command": "push_status", "cfg": "C0340BC219"}}) # bit18=0
assert mqtt_client.state.ams_filament_backup is False
def test_status_frame_without_command_still_updates_backup_state(self, mqtt_client):
"""Some firmwares omit `command` on a status frame; those stay trusted."""
mqtt_client.state.ams_filament_backup = False
mqtt_client._process_message({"print": {"cfg": "C0340FC219"}}) # bit18=1
assert mqtt_client.state.ams_filament_backup is True
def test_project_file_ack_does_not_clear_timelapse_state(self, mqtt_client):
"""The ack echoes the per-job timelapse request, not the recorder."""
mqtt_client.state.timelapse = True
mqtt_client._process_message({"print": {"command": "project_file", "timelapse": False}})
assert mqtt_client.state.timelapse is True
def test_push_status_still_updates_timelapse_state(self, mqtt_client):
mqtt_client.state.timelapse = True
mqtt_client._process_message({"print": {"command": "push_status", "timelapse": False}})
assert mqtt_client.state.timelapse is False
# ---------------------------------------------------------------------------
# 2c. Single-nozzle H2S — external-spool tray_now override (#1822)
# ---------------------------------------------------------------------------