fix(usage-tracker): split mid-print AMS-Backup spool switch correctly (#1771)

Reporter forcefully started a print needing ~260 g with 180 g on the
      first spool and a backup spool in the AMS. Printer correctly consumed
      spool 1, AMS Backup switched, spool 2 finished the print. Bambuddy
      attributed all 260 g to spool 2 -- spool 1 untouched in inventory.

      Two stacking bugs produced the exact "all to second spool" symptom for
      prints without per-layer 3MF gcode data:

      1. bambu_mqtt.py:2135 wrote state.total_layers = int(data["total_layer_num"])
         unconditionally. P1S firmware pushes total_layer_num=0 at print end
         (same reset pattern other models do for layer_num / progress). The
         unconditional write clobbered the slicer's actual total to 0 before
         the usage tracker read it.

      2. usage_tracker.py:1129-1137 linear-fallback dumped EVERYTHING onto the
         last segment when total_layers was 0:
           if total_layers > 0:
               segment_grams = total_weight * (seg_end_layer - seg_start_layer) / total_layers
           else:
               segment_grams = 0.0   # <- entire print weight ends up on last segment

         Path 2 (AMS remain% delta) couldn't recover because (a) the emptied
         spool reported remain=-1 and (b) Bug-A had already added the second
         spool's key to handled_trays, suppressing the Path 2 lookup.

      Fix:

      - bambu_mqtt.py: only overwrite state.total_layers when the incoming
        value is positive (mirror of the existing _last_valid_layer_num
        pattern at line 2127). Explicit reset on new print start at
        _handle_print_start so the previous print's total can't bleed in.

      - usage_tracker.py: cascade the linear-fallback denominator -
        state.total_layers, then last_layer_num (already threaded in for
        the last_progress fallback), then equal-split as a bounded fence.
        Equal-split is still wrong but never dumps the whole print on the
        last segment, which was strictly worse.
This commit is contained in:
maziggy
2026-06-28 12:37:26 +02:00
5 changed files with 229 additions and 7 deletions
+14 -1
View File
@@ -2133,7 +2133,14 @@ class BambuMQTTClient:
if new_layer > old_layer and self.on_layer_change:
self.on_layer_change(new_layer)
if "total_layer_num" in data:
self.state.total_layers = int(data["total_layer_num"])
# Some firmware (P1S observed) resets `total_layer_num` to 0 at
# print end — same shape as the `layer_num` reset guarded above.
# Preserve the last known good value so the usage-tracker split
# path (#1771) has a denominator that survives the reset frame.
# Explicit reset to 0 happens on print start (`_handle_print_start`).
new_total = int(data["total_layer_num"])
if new_total > 0:
self.state.total_layers = new_total
# Fan speeds (MQTT sends as string "0"-"15" representing speed levels, or percentage)
# Convert to 0-100 percentage for display
@@ -3120,6 +3127,12 @@ class BambuMQTTClient:
self.state.hms_errors = []
# Reset layer tracking for new print (needed for layer-based timelapse)
self.state.layer_num = 0
# Reset total_layers so the previous print's value can't bleed into
# this print's usage-tracker split before the new push_status arrives
# with the slicer's total (#1771 follow-on to the preservation guard
# above at line ~2135 — the guard now ignores firmware-reset 0s, so
# the explicit reset has to happen here instead).
self.state.total_layers = 0
# Reset completion tracking for new print
self._was_running = True
self._completion_triggered = False
+19 -6
View File
@@ -1127,14 +1127,27 @@ async def _track_from_3mf(
mm_at_end = get_cumulative_usage_at_layer(split_layer_usage, seg_end_layer).get(filament_id, 0)
segment_grams = mm_to_grams(mm_at_end - mm_at_start, diameter, density)
else:
# No per-layer data: linear fallback by layer ratio
# No per-layer data: linear fallback by layer ratio (#1771).
# Cascade denominators because firmware on some models (P1S
# observed) resets `total_layer_num` to 0 at print end —
# `last_layer_num` is the print's last-valid layer captured
# mid-print and survives that reset (same shape as the
# `last_progress` fallback at line 1040). Equal-split is the
# last-resort fence: still wrong, but bounded — never dumps
# the entire print onto the last segment, which was the
# original #1771 symptom for the reporter (P1S, AMS Backup
# fed from spool 1 then spool 2, all 260 g credited to
# spool 2 even though spool 1 had given up its 180 g).
seg_end_layer = tray_changes[seg_idx + 1][1]
total_layers = state.total_layers if state else 0
if total_layers > 0:
segment_grams = total_weight * (seg_end_layer - seg_start_layer) / total_layers
denom = (state.total_layers if state else 0) or last_layer_num
if denom > 0:
segment_grams = total_weight * (seg_end_layer - seg_start_layer) / denom
else:
# Can't compute ratio — assign all to last segment
segment_grams = 0.0
# No layer information available from any source —
# spread evenly across segments. The last segment will
# get the rounding remainder via the `is_last` branch
# above on its own iteration.
segment_grams = total_weight / len(tray_changes)
sum_previous += segment_grams
if segment_grams <= 0: