tray_split.py 4.2 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394
  1. """Weight-split math for prints that traversed >1 AMS tray mid-print.
  2. `state.tray_change_log` records `(global_tray_id, layer_num)` tuples every
  3. time `tray_now` changes during a print (see `bambu_mqtt.py:1861`). At
  4. completion, both the internal Spool inventory (`usage_tracker.py`) and the
  5. Spoolman writer (`spoolman_tracking.py`) need to split a slot's total
  6. weight across the segments those changes define — one call site per
  7. inventory backend, one identical splitting algorithm.
  8. The algorithm lives here so the two callers cannot drift: #1793 came from
  9. `spoolman_tracking` never carrying the split at all, while `usage_tracker`
  10. had shipped it since #957 and refined it in #1771. Sharing the helper is
  11. the structural fix; each caller wraps its own "resolve segment tray →
  12. charge N grams" side effect.
  13. """
  14. from __future__ import annotations
  15. # Qualified-name access (``threemf_tools.mm_to_grams(...)`` rather than
  16. # ``from … import mm_to_grams``) so ``unittest.mock.patch`` on the
  17. # threemf_tools module lands in the helper too — the pre-refactor
  18. # ``usage_tracker`` call site imported at call-time inside a try block,
  19. # which had the same testability property.
  20. from backend.app.utils import threemf_tools
  21. def compute_tray_split_grams(
  22. tray_changes: list[tuple[int, int]],
  23. total_weight: float,
  24. slot_id: int,
  25. layer_usage: dict[int, dict[int, float]] | None,
  26. density: float,
  27. diameter: float,
  28. total_layers: int,
  29. last_layer_num: int,
  30. ) -> list[tuple[int, int, float]]:
  31. """Split ``total_weight`` for a single slice slot across tray segments.
  32. ``tray_changes`` is the ordered list ``[(global_tray_id, seg_start_layer), ...]``
  33. exactly as it appears in ``state.tray_change_log``. The last segment
  34. runs to the end of the print; every other segment ends at the next
  35. entry's ``seg_start_layer``.
  36. Preference order for per-segment grams — matches ``usage_tracker`` so
  37. both inventory backends split identically:
  38. 1. **G-code cumulative extrusion** (``layer_usage``, indexed by 0-based
  39. filament id). Precise: uses the mm actually consumed between
  40. ``seg_start_layer`` and the next segment's start, then converts via
  41. Spoolman-authoritative ``density`` / ``diameter``.
  42. 2. **Linear layer-ratio** — ``total_weight * segment_layers / denom``,
  43. with ``denom = total_layers or last_layer_num``. Firmware on P1S
  44. (observed) resets ``total_layer_num`` to 0 at print end, so the
  45. captured ``last_layer_num`` is the durable denominator (see
  46. ``usage_tracker.py:1132``). #1771 addressed the pre-fix behaviour
  47. of dumping everything onto the last segment.
  48. 3. **Equal-split** — when neither denominator is available (server
  49. restart mid-print, missing state). Wrong but bounded — the last
  50. segment absorbs any rounding drift via the ``is_last`` branch.
  51. Returns ``[(seg_idx, global_tray_id, segment_grams)]``. Empty when
  52. ``tray_changes`` is empty; the caller decides whether to fall through
  53. to single-tray attribution (``len(tray_changes) <= 1``).
  54. """
  55. if not tray_changes:
  56. return []
  57. filament_id = slot_id - 1
  58. n_segments = len(tray_changes)
  59. denom = total_layers or last_layer_num
  60. results: list[tuple[int, int, float]] = []
  61. sum_previous = 0.0
  62. for seg_idx, (tray_global, seg_start_layer) in enumerate(tray_changes):
  63. is_last = seg_idx + 1 >= n_segments
  64. if is_last:
  65. segment_grams = total_weight - sum_previous
  66. elif layer_usage:
  67. seg_end_layer = tray_changes[seg_idx + 1][1]
  68. mm_at_start = threemf_tools.get_cumulative_usage_at_layer(layer_usage, seg_start_layer).get(filament_id, 0)
  69. mm_at_end = threemf_tools.get_cumulative_usage_at_layer(layer_usage, seg_end_layer).get(filament_id, 0)
  70. segment_grams = threemf_tools.mm_to_grams(mm_at_end - mm_at_start, diameter, density)
  71. else:
  72. seg_end_layer = tray_changes[seg_idx + 1][1]
  73. if denom > 0:
  74. segment_grams = total_weight * (seg_end_layer - seg_start_layer) / denom
  75. else:
  76. segment_grams = total_weight / n_segments
  77. sum_previous += segment_grams
  78. results.append((seg_idx, tray_global, segment_grams))
  79. return results