Files
bambuddy/backend/app/models/active_print_session.py
T
maziggy 454457a0af Attribute filament correctly when AMS backup swaps spools mid-print
Everything the completion path needs to split a print's filament across
the trays it fed from lived only in memory: the dispatched plate and
slot-to-tray mapping, the spool-assignment snapshot, and the tray-change
log. A print that outlived a restart lost all of it and fell back to
what the printer reports at completion -- which, with AMS Filament
Backup on, is the substitute tray. The whole print was charged to the
spool that only finished it while the spool that ran dry was charged
nothing.

Persist that context in a new active_print_sessions row, append tray
changes as they happen, and restore both the session and the printer's
tray-change log at restart recovery. Seed the log from the current tray
when there is nothing to restore, since last_loaded_tray advances even
when no change is logged.

Rank the queue item's stored ams_mapping above the printer's live
mapping field, which is what backup rewrites. Recover plate_id from the
archive or queue item, and give extract_layer_filament_usage_from_3mf a
plate_id instead of taking the first .gcode member -- a Bambu Studio
export stores plate 2 first, so per-layer figures were measured against
the wrong plate for both inventory backends.

Stop auto-unlinking a spool assignment when its slot reports empty
during a running print. At a runout the spool is still in the AMS, and
dropping the link leaves the completion path nothing to charge.

Capture the print-start context for both inventory backends. Spoolman's
own durable row (#1820) carries its plate-scoped figures and dispatched
mapping but not the tray-change log, and its slot assignments -- the
way. Registration in _active_sessions stays gated, since on_ams_change
reads it to decide whether to skip the remain%-based weight sync (#880).
2026-08-13 08:37:16 +02:00

58 lines
2.6 KiB
Python

"""Durable copy of the filament-attribution context for an in-flight print."""
from datetime import datetime
from sqlalchemy import JSON, DateTime, ForeignKey
from sqlalchemy.orm import Mapped, mapped_column
from backend.app.core.database import Base
class ActivePrintSession(Base):
"""Print-start context the completion path needs, persisted per printer.
``usage_tracker._active_sessions`` holds the same data in memory, and the
tray-change log lives on ``PrinterState``. Both are lost when Bambuddy
restarts mid-print, which on a long print silently destroys filament
attribution: without the plate the 3MF parser sums every plate, without the
assignment snapshot a spool unlinked at runout can't be resolved, and
without the tray-change log an AMS-backup switch charges the whole print to
whichever tray happened to finish it.
One row per printer — a printer runs one print at a time. Written at print
start, appended to on every tray change, deleted at completion. A leaked
row (completion missed entirely) is harmless: print start overwrites it,
and the completion path ignores a row whose ``started_at`` doesn't line up
with the print it is closing.
The Spoolman writer has had an equivalent durable row since #1820
(``active_print_spoolman``); this is the internal-inventory counterpart.
"""
__tablename__ = "active_print_sessions"
printer_id: Mapped[int] = mapped_column(ForeignKey("printers.id", ondelete="CASCADE"), primary_key=True)
print_name: Mapped[str] = mapped_column(default="")
started_at: Mapped[datetime] = mapped_column(DateTime)
# tray_now at print start — reliable, unlike at completion where the
# printer has usually retracted and reports 255.
tray_now_at_start: Mapped[int] = mapped_column(default=-1)
# Queue item's plate for multi-plate 3MFs dispatched one plate at a time.
plate_id: Mapped[int | None] = mapped_column(nullable=True)
# Slicer slot -> global tray, as dispatched: [2]
ams_mapping: Mapped[list | None] = mapped_column(JSON, nullable=True)
# {"<ams_id>-<tray_id>": spool_id} — the assignment map as it stood before
# the print could disturb it.
spool_assignments: Mapped[dict | None] = mapped_column(JSON, nullable=True)
# {"<ams_id>-<tray_id>": remain%} for the remain-delta fallback path.
tray_remain_start: Mapped[dict | None] = mapped_column(JSON, nullable=True)
# [[global_tray_id, layer_num], ...] mirroring PrinterState.tray_change_log.
tray_change_log: Mapped[list | None] = mapped_column(JSON, nullable=True)