mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-08-11 00:30:12 -04:00
usage_tracker's tray-switch split has never had a Spoolman peer. An AMS same-material runout switch mid-print charged the whole slot to the origin spool via the (via tag) path and double-credited the backup via remain-delta — origin exceeded initial_weight. Extract the segment-math into utils/tray_split.compute_tray_split_grams and call it from both writers so the two inventory backends attribute mid-print switches identically. spoolman_tracking gains _report_spool_usage_split_by_tray_changes; the Path 2 remain-delta fallback now skips trays the split path covered, killing the double-count.
94 lines
4.2 KiB
Python
94 lines
4.2 KiB
Python
"""Weight-split math for prints that traversed >1 AMS tray mid-print.
|
|
|
|
`state.tray_change_log` records `(global_tray_id, layer_num)` tuples every
|
|
time `tray_now` changes during a print (see `bambu_mqtt.py:1861`). At
|
|
completion, both the internal Spool inventory (`usage_tracker.py`) and the
|
|
Spoolman writer (`spoolman_tracking.py`) need to split a slot's total
|
|
weight across the segments those changes define — one call site per
|
|
inventory backend, one identical splitting algorithm.
|
|
|
|
The algorithm lives here so the two callers cannot drift: #1793 came from
|
|
`spoolman_tracking` never carrying the split at all, while `usage_tracker`
|
|
had shipped it since #957 and refined it in #1771. Sharing the helper is
|
|
the structural fix; each caller wraps its own "resolve segment tray →
|
|
charge N grams" side effect.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
# Qualified-name access (``threemf_tools.mm_to_grams(...)`` rather than
|
|
# ``from … import mm_to_grams``) so ``unittest.mock.patch`` on the
|
|
# threemf_tools module lands in the helper too — the pre-refactor
|
|
# ``usage_tracker`` call site imported at call-time inside a try block,
|
|
# which had the same testability property.
|
|
from backend.app.utils import threemf_tools
|
|
|
|
|
|
def compute_tray_split_grams(
|
|
tray_changes: list[tuple[int, int]],
|
|
total_weight: float,
|
|
slot_id: int,
|
|
layer_usage: dict[int, dict[int, float]] | None,
|
|
density: float,
|
|
diameter: float,
|
|
total_layers: int,
|
|
last_layer_num: int,
|
|
) -> list[tuple[int, int, float]]:
|
|
"""Split ``total_weight`` for a single slice slot across tray segments.
|
|
|
|
``tray_changes`` is the ordered list ``[(global_tray_id, seg_start_layer), ...]``
|
|
exactly as it appears in ``state.tray_change_log``. The last segment
|
|
runs to the end of the print; every other segment ends at the next
|
|
entry's ``seg_start_layer``.
|
|
|
|
Preference order for per-segment grams — matches ``usage_tracker`` so
|
|
both inventory backends split identically:
|
|
|
|
1. **G-code cumulative extrusion** (``layer_usage``, indexed by 0-based
|
|
filament id). Precise: uses the mm actually consumed between
|
|
``seg_start_layer`` and the next segment's start, then converts via
|
|
Spoolman-authoritative ``density`` / ``diameter``.
|
|
2. **Linear layer-ratio** — ``total_weight * segment_layers / denom``,
|
|
with ``denom = total_layers or last_layer_num``. Firmware on P1S
|
|
(observed) resets ``total_layer_num`` to 0 at print end, so the
|
|
captured ``last_layer_num`` is the durable denominator (see
|
|
``usage_tracker.py:1132``). #1771 addressed the pre-fix behaviour
|
|
of dumping everything onto the last segment.
|
|
3. **Equal-split** — when neither denominator is available (server
|
|
restart mid-print, missing state). Wrong but bounded — the last
|
|
segment absorbs any rounding drift via the ``is_last`` branch.
|
|
|
|
Returns ``[(seg_idx, global_tray_id, segment_grams)]``. Empty when
|
|
``tray_changes`` is empty; the caller decides whether to fall through
|
|
to single-tray attribution (``len(tray_changes) <= 1``).
|
|
"""
|
|
if not tray_changes:
|
|
return []
|
|
|
|
filament_id = slot_id - 1
|
|
n_segments = len(tray_changes)
|
|
denom = total_layers or last_layer_num
|
|
results: list[tuple[int, int, float]] = []
|
|
sum_previous = 0.0
|
|
|
|
for seg_idx, (tray_global, seg_start_layer) in enumerate(tray_changes):
|
|
is_last = seg_idx + 1 >= n_segments
|
|
|
|
if is_last:
|
|
segment_grams = total_weight - sum_previous
|
|
elif layer_usage:
|
|
seg_end_layer = tray_changes[seg_idx + 1][1]
|
|
mm_at_start = threemf_tools.get_cumulative_usage_at_layer(layer_usage, seg_start_layer).get(filament_id, 0)
|
|
mm_at_end = threemf_tools.get_cumulative_usage_at_layer(layer_usage, seg_end_layer).get(filament_id, 0)
|
|
segment_grams = threemf_tools.mm_to_grams(mm_at_end - mm_at_start, diameter, density)
|
|
else:
|
|
seg_end_layer = tray_changes[seg_idx + 1][1]
|
|
if denom > 0:
|
|
segment_grams = total_weight * (seg_end_layer - seg_start_layer) / denom
|
|
else:
|
|
segment_grams = total_weight / n_segments
|
|
|
|
sum_previous += segment_grams
|
|
results.append((seg_idx, tray_global, segment_grams))
|
|
|
|
return results
|