mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-09-30 03:01:21 +02:00
Two faults behind the same kind of print: one that arrives without a
retrievable 3MF, which on an H2S is any job started from the printer's
own internal library.
Such an archive has no file_path, and Path("").parent is Path("."), so
every site that derived the archive's folder from it landed on the data
directory itself. The finish-photo capture spotted that and wrote to
<archive_dir>/<id>/photos instead. Nothing else did. The photo was
written in one place and looked for in another: reads 404'd, deletes
dropped the name and left the file, and the notification attachment
never found the image. Hand-uploaded photos worked only because upload
and read agreed with each other rather than with the capture. Give the
question one owner in utils/archive_paths and have all four sites ask
it. Lookups check the old shared location too, so photos already
uploaded there stay reachable; uploads now go where captures go.
Separately, the remain%-delta fallback that stands in for a missing 3MF
can charge nothing for several reasons, and did so without a word. The
AMS reading is coarse and, on the reporter's printer, noisy: it rises
mid-print, swings five points over a job, sits at 100% through a
36-minute print on a fresh spool, and goes negative on a nearly empty
one -- which the start-of-print gate rejects, dropping the only slot
that was printing. Two of their prints went uncounted for two different
reasons and both read as "no spools updated", which is also what a print
with nothing to charge prints. Name the slot and the two readings in
each case, on the Spoolman path and on the internal-inventory path,
which has carried the same gates since #1119.
The Spoolman path also had no notion of which slots the print used, so a
spool swapped into an idle slot mid-print reads as consumption and is
billed to whoever that slot is assigned to -- the fault #1269 fixed for
the internal tracker, still open here, and likeliest on exactly the
prints this fallback serves, where nothing else narrows the field. Use
the same three pieces of evidence it does: the print's mapping, its
mid-print tray changes, and the tray it started on. The last needs
storing, because the internal tracker's row is deleted before this runs
and a screen-started print has no mapping to fall back on -- hence a new
nullable column, and no backfill, since a row from before it existed has
nothing to say. Where no evidence exists at all, every slot is still
considered.
Both paths also treated tray_now == 255 as naming a slot. It does not:
it is the field's initial value, the fallback for an unparseable
reading, and what it reports with nothing loaded. Mapped as a tray id it
becomes (255, 1), so as the only evidence it excluded every real slot
and charged nothing at all -- this issue's own bug, arriving by a new
route. On the internal path that is live today; on the Spoolman path it
would have shipped with the guard above. The external holder reports 254
when it is genuinely in use.
The arithmetic is untouched: at one percent per step this cannot resolve
a small print, and pretending otherwise would be worse than saying so.
65 lines
3.3 KiB
Python
65 lines
3.3 KiB
Python
"""Track Spoolman data for active prints."""
|
|
|
|
from sqlalchemy import JSON, ForeignKey, UniqueConstraint
|
|
from sqlalchemy.orm import Mapped, mapped_column
|
|
|
|
from backend.app.core.database import Base
|
|
|
|
|
|
class ActivePrintSpoolman(Base):
|
|
"""Stores Spoolman tracking data for active prints.
|
|
|
|
This data is captured at print start and used at print completion
|
|
to report per-filament usage to the correct Spoolman spools.
|
|
Rows are deleted after print completes.
|
|
|
|
Key: (printer_id, archive_id) - allows same archive on different printers
|
|
"""
|
|
|
|
__tablename__ = "active_print_spoolman"
|
|
__table_args__ = (UniqueConstraint("printer_id", "archive_id", name="uq_printer_archive"),)
|
|
|
|
id: Mapped[int] = mapped_column(primary_key=True)
|
|
printer_id: Mapped[int] = mapped_column(ForeignKey("printers.id", ondelete="CASCADE"))
|
|
archive_id: Mapped[int] = mapped_column(ForeignKey("print_archives.id", ondelete="CASCADE"))
|
|
|
|
# Per-filament usage from 3MF: [{"slot_id": 1, "used_g": 50.5, "type": "PLA"}, ...]
|
|
# Nullable for the no-3MF case ("Untitled" prints where Bambu didn't keep a
|
|
# .gcode.3mf on the printer): the row still gets created so the completion
|
|
# path can use ``tray_remain_start`` for an AMS remain%-delta write,
|
|
# mirroring the internal-inventory Path 2 fallback in usage_tracker (#1820).
|
|
filament_usage: Mapped[list | None] = mapped_column(JSON, nullable=True)
|
|
|
|
# AMS tray state at print start: {0: {"tray_uuid": "...", "tag_uid": "..."}, ...}
|
|
ams_trays: Mapped[dict] = mapped_column(JSON)
|
|
|
|
# Custom slot-to-tray mapping from queue (optional): [5, -1, 2, -1]
|
|
slot_to_tray: Mapped[list | None] = mapped_column(JSON, nullable=True)
|
|
|
|
# Per-layer cumulative usage from G-code parsing (for accurate partial usage)
|
|
# Format: {"0": {0: 125.5}, "1": {0: 250.0, 1: 50.0}, ...}
|
|
# Keys are layer numbers (as strings for JSON), values are filament_id -> mm
|
|
layer_usage: Mapped[dict | None] = mapped_column(JSON, nullable=True)
|
|
|
|
# Filament properties (density, diameter per filament slot)
|
|
# Format: {1: {"density": 1.24, "diameter": 1.75, "type": "PLA"}, ...}
|
|
filament_properties: Mapped[dict | None] = mapped_column(JSON, nullable=True)
|
|
|
|
# AMS tray remain% per slot at print start, captured so the completion
|
|
# path can compute a remain-delta when the 3MF didn't cover a slot (or
|
|
# there was no 3MF at all — #1820). Matches the internal-inventory
|
|
# ``tray_remain_start`` snapshot at usage_tracker.py:301.
|
|
# Format: {"<ams_id>-<tray_id>": {"remain": int, "tray_uuid": str}, ...}
|
|
tray_remain_start: Mapped[dict | None] = mapped_column(JSON, nullable=True)
|
|
|
|
# Global tray id the printer was drawing from when the print started.
|
|
# Evidence of which slot the print actually used, for the remain%-delta
|
|
# fallback (#1269 on the internal side, #1820 here): without it, a spool
|
|
# swapped in an untouched slot mid-print reads as consumption and is
|
|
# charged to whatever spool that slot was assigned. Often the only
|
|
# evidence there is — a print started from the printer's own screen
|
|
# carries no ams_mapping and may change tray never. Nullable: rows written
|
|
# before this column existed, and printers that report no tray_now, simply
|
|
# provide no evidence and are handled as before.
|
|
tray_now_at_start: Mapped[int | None] = mapped_column(nullable=True)
|