Follow AMS Filament Backup toggles without a Force Refresh on printers without cfg (#3259)

The P1S reports a slicer toggle in a small 4-6 key update frame, not a full
status report, so the badge only changed after a Force Refresh. The probe
capture showed home_flag bit 10 flipping with each toggle and no stray flips
while idle. Once a full report has arrived without cfg, bit 10 is now read
from every status frame that carries home_flag. Small frames are still
ignored before that, and printers that send cfg keep reading cfg alone.

Removes the home_flag probe log.
This commit is contained in:
maziggy
2026-10-09 08:40:10 +02:00
parent 2bceb1f6b4
commit d9759be90c
3 changed files with 44 additions and 53 deletions
+1 -1
View File
@@ -162,7 +162,7 @@ All notable changes to Bambuddy will be documented in this file.
- **The File Manager and Archives page loaded every thumbnail at once** — With a few hundred files every thumbnail was requested on page load, and the queue of requests slowed down everything else on the server for several seconds. Thumbnails now load as they scroll into view.
- **Statistics counted a multi-material print written from spools as one material (#3262, reported by @ojimpo)** — A print's materials are stored comma-joined, as `PLA, PLA-S` from the sliced file but as `PLA Basic,PLA` once they are taken from the spools that fed it. The Statistics page split on comma plus space only, so those prints showed as one `PLA Basic,PLA` row in the material weight, print count, time and success-rate charts, and the Archives material filter offered the joined string as a material of its own and missed those prints when filtering by `PLA`. Both now split on the comma alone, as the statistics API already did, so existing archives are counted under each of their materials.
- **A running print's cost used the default rate instead of its spools, then changed without notice at completion (#3261, reported by @ojimpo)** — The archive was priced at creation from the Filament catalogue or **Default Filament Cost**, and the spools that fed the print only priced it once it finished, in both Spoolman mode and the built-in inventory. Anyone noting the cost while a long print ran recorded a placeholder. A print is now priced from its spools when it starts, from the sliced file's per-slot estimate and the spool in each mapped tray, with the same default-rate cover for grams no spool prices; a reprint still keeps its first run's cost. The archive card marks a running print's cost with `~` and a tooltip, since the final figure comes from what was actually used. A print whose slots can't be matched to trays at the start keeps the placeholder until it finishes.
- **AMS Filament Backup showed "?" on the P1S, P1P, A1 and A1 Mini (#3259, reported by @HopNotch)** — These printers never send the `cfg` field Bambuddy read the setting from; they carry it in `home_flag` bit 10, where Bambu Studio reads it for every printer. Bambuddy now falls back to that bit, so the badge shows the printer's real setting and the prefer-lowest-remaining gate follows it. Before 1.2.5.6 the badge seemed to work on these printers only because it read Bambuddy's own print command echoed back, which always said off (#3040).
- **AMS Filament Backup showed "?" on the P1S, P1P, A1 and A1 Mini (#3259, reported by @HopNotch)** — These printers never send the `cfg` field Bambuddy read the setting from; they carry it in `home_flag` bit 10, where Bambu Studio reads it for every printer. Bambuddy now falls back to that bit, so the badge shows the printer's real setting and the prefer-lowest-remaining gate follows it. Switching the setting in the slicer now shows up without a Force Refresh. Before 1.2.5.6 the badge seemed to work on these printers only because it read Bambuddy's own print command echoed back, which always said off (#3040).
- **Slicing failed with a printer preset saved under a name of its own (#3250, reported by @bobobo1618)** — A printer preset copied from a stock one and renamed, such as "Bambu Lab H2D 0.4 nozzle - Apply Skew Calibration", failed every slice with "The selected printer is not compatible with the process preset in the 3mf", even with a stock process the stock printer slices fine.
- **Why:** the slicer checks a process against the stock preset a printer preset was saved from. The slicer sidecar handed it the renamed preset as a stock preset in its own right, and no process lists that name. It now names the stock preset the printer was saved from, while still slicing with the renamed preset's own settings, start G-code included. A process or filament preset that lists the renamed printer itself keeps fitting it, as it does in the slicer. Needs the updated slicer sidecar image (OrcaSlicer and Bambu Studio).
- **The Slice dialog** read the printer model out of the preset's name, which a name of your own may not contain, so it misjudged which process and filament presets fit. It now also checks against the stock preset the printer was saved from, for imported and OrcaSlicer Cloud printer presets.
+16 -29
View File
@@ -1494,8 +1494,9 @@ class BambuMQTTClient:
# True once the printer has sent `cfg`; from then on AMS Filament Backup
# is read from cfg only, never from home_flag (#3259).
self._backup_cfg_seen: bool = False
# Last home_flag written by the #3259 probe log below.
self._backup_home_flag_logged: int | None = None
# True once a full status report arrived without `cfg`; from then on
# home_flag bit 10 is read from every status frame (#3259).
self._backup_home_flag_trusted: bool = False
# Track last requested tray ID for H2D dual-nozzle printers
# H2D only reports slot number (0-3) in tray_now, not global tray ID
@@ -2319,12 +2320,14 @@ class BambuMQTTClient:
# and live H2D ON/OFF capture 2026-06-20.
#
# Families without cfg (P1S, P1P, A1, A1 Mini) carry it in home_flag
# bit 10 (#3259). That's read only from a full status report (the
# same >30-key test as the developer-mode probe) and only while the
# printer has never sent cfg: H2D firmware also sends small
# heartbeat frames with a partial home_flag (bits 8-9 clear with a
# card inserted, which is why the SD-card badge was removed), and
# printers that send cfg must keep reading it alone.
# bit 10 (#3259). Trusted once a full status report (the same
# >30-key test as the developer-mode probe) arrived without cfg;
# from then on every status frame that carries home_flag is read,
# so a toggle in the slicer shows up without a Force Refresh (P1S
# capture: each toggle came in a 4-6 key update frame). Before
# that nothing is read from small frames, so a cfg printer can't
# be misread from a frame that arrives ahead of its first cfg.
# Printers that send cfg keep reading it alone.
#
# Hold-timer guard: when the user just toggled via the badge, the
# next 1-2 push_status frames may still carry the printer's OLD cfg
@@ -2339,27 +2342,11 @@ class BambuMQTTClient:
if "cfg" in print_data:
self._backup_cfg_seen = True
new_backup = parse_ams_filament_backup_from_cfg(print_data["cfg"])
elif not self._backup_cfg_seen and len(print_data) > 30:
new_backup = parse_ams_filament_backup_from_home_flag(print_data.get("home_flag"))
# Probe for #3259: does bit 10 stay right in the small update
# frames these printers send between full reports? Logged on
# every change, so a heartbeat that clears bit 10 shows up as
# a flip. Remove once a P1S capture answers it.
home_flag = print_data.get("home_flag")
if (
not self._backup_cfg_seen
and isinstance(home_flag, int)
and not isinstance(home_flag, bool)
and home_flag != self._backup_home_flag_logged
):
self._backup_home_flag_logged = home_flag
logger.debug(
"[%s] home_flag probe: 0x%08X bit10=%d keys=%d",
self.serial_number,
home_flag & 0xFFFFFFFF,
(home_flag >> 10) & 1,
len(print_data),
)
elif not self._backup_cfg_seen:
if len(print_data) > 30:
self._backup_home_flag_trusted = True
if self._backup_home_flag_trusted:
new_backup = parse_ams_filament_backup_from_home_flag(print_data.get("home_flag"))
if new_backup is not None and new_backup != self.state.ams_filament_backup:
hold_start = self._xcam_hold_start.get("print_option_auto_switch_filament")
if hold_start is not None and (time.time() - hold_start) <= self._xcam_hold_time:
+27 -23
View File
@@ -7231,14 +7231,39 @@ class TestCommandAckIsNotTelemetry:
assert mqtt_client.state.ams_filament_backup is False
def test_small_frame_home_flag_is_ignored(self, mqtt_client):
"""Heartbeat-style frames carry a partial home_flag (H2D, SD-card badge)."""
def test_small_frame_home_flag_ignored_before_full_report(self, mqtt_client):
"""Until a full report without cfg arrives, the printer may still be a cfg family."""
mqtt_client.state.ams_filament_backup = True
mqtt_client._process_message({"print": {"command": "push_status", "home_flag": 0}})
assert mqtt_client.state.ams_filament_backup is True
def test_small_frame_home_flag_read_after_full_report_without_cfg(self, mqtt_client):
"""P1S sends a slicer toggle in a 4-6 key update frame, not a full report (#3259)."""
mqtt_client._process_message(self._full_status(home_flag=0x00634518)) # bit10=1
assert mqtt_client.state.ams_filament_backup is True
mqtt_client._process_message({"print": {"command": "push_status", "home_flag": 0x00634118}})
assert mqtt_client.state.ams_filament_backup is False
mqtt_client._process_message({"print": {"command": "push_status", "home_flag": 0x00634518}})
assert mqtt_client.state.ams_filament_backup is True
def test_small_frame_without_home_flag_keeps_backup_state(self, mqtt_client):
mqtt_client._process_message(self._full_status(home_flag=0x00634518)) # bit10=1
mqtt_client._process_message({"print": {"command": "push_status", "wifi_signal": "-38dBm"}})
assert mqtt_client.state.ams_filament_backup is True
def test_small_frame_home_flag_ignored_on_cfg_printer(self, mqtt_client):
mqtt_client._process_message(self._full_status(cfg="C0340FC219")) # bit18=1
mqtt_client._process_message({"print": {"command": "push_status", "home_flag": 0}})
assert mqtt_client.state.ams_filament_backup is True
def test_home_flag_ignored_once_printer_sent_cfg(self, mqtt_client):
"""Printers that send cfg keep reading it alone, even in full reports without it."""
mqtt_client._process_message(self._full_status(cfg="C0340FC219", home_flag=1 << 10)) # bit18=1
@@ -7269,27 +7294,6 @@ class TestCommandAckIsNotTelemetry:
assert mqtt_client.state.ams_filament_backup is True
def test_home_flag_probe_logs_each_change_with_frame_size(self, mqtt_client, caplog):
"""#3259 probe: small frames are logged too, but only when home_flag changes."""
caplog.set_level(logging.DEBUG, logger="backend.app.services.bambu_mqtt")
mqtt_client._process_message(self._full_status(home_flag=7554719)) # bit10=1
mqtt_client._process_message({"print": {"command": "push_status", "home_flag": 7554719}})
mqtt_client._process_message({"print": {"command": "push_status", "home_flag": -1024}})
probes = [r.getMessage() for r in caplog.records if "home_flag probe" in r.getMessage()]
assert len(probes) == 2
assert "0x0073469F bit10=1 keys=32" in probes[0]
assert "0xFFFFFC00 bit10=1 keys=2" in probes[1]
def test_home_flag_probe_silent_once_printer_sent_cfg(self, mqtt_client, caplog):
caplog.set_level(logging.DEBUG, logger="backend.app.services.bambu_mqtt")
mqtt_client._process_message(self._full_status(cfg="C0340FC219"))
mqtt_client._process_message({"print": {"command": "push_status", "home_flag": 0}})
assert not [r for r in caplog.records if "home_flag probe" in r.getMessage()]
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