mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-08-11 00:30:12 -04:00
Round-3 review of the "Save AMS mapping" PR. The queue item's ams_mapping was set unconditionally, on the reasoning that honouring the slicer's own pick is a correctness fix rather than a feature. It is both. Storing a resolved mapping makes _ensure_ams_mapping return early, so _compute_ams_mapping_for_printer never runs — and that function is where prefer_lowest_filament lives, along with the AMS-filament-backup gate that qualifies it (#1766), the inventory-remain overrides, and the per-slot force-colour overrides. Every existing queue-mode VP pointed at a printer would have quietly lost all of it on upgrade, without a setting to turn it back on. So save_ams_mapping now gates the queue item too, not just the archive persistence. Off is exactly the old behaviour. The correctness case the PR was written for — two spools of the same red PLA, and the slot the user picked in the slicer thrown away — is still fixed, for anyone who asks for it. Force color match wins over it when both are on. Its only effect on a fixed-printer item is the filament_overrides written onto the queue item, and those are read inside the function a stored mapping skips, so the two toggles sitting next to each other on the same card silently cancelled. The dispatch now matches strictly, as asked, while the slicer's pick is still saved onto the archive — that is what the toggle's name promises, and a later reprint is a separate decision from this print. The queue-add fallback applies the same rule to a request that carries force-colour overrides. A mapping shorter than a plate's highest slot id cannot address that plate's own slots, and _ensure_ams_mapping would have kept it anyway, since it only rejects an all-unresolved one. Each plate now checks the length it needs and falls back to a computed mapping if the array does not reach. Bambu Studio sends a file-global array, so this normally never fires; it also means a multi-plate Send All degrades safely if that ever stops being true. The badges claimed more than they delivered. Both rendered whenever a saved mapping existed, ignoring which printer it belonged to, while the tooltips promised the reprint would reuse those exact spools — true only on the printer the trays were resolved against. The queue row's flag is now computed against that row's own printer, which is precisely when dispatch reuses the mapping, and the archive card names the printer instead of implying any of them will do. It hides itself when that printer no longer exists. Retranslated in all 13 locales. Frontend tests, which the PR had none of. The printer-scoping rule is now a pure function rather than an inline expression, covered for the mismatched printer, the no-printer-selected case that would otherwise compare undefined against undefined, and malformed extra_data. The toggle's undo bookkeeping is covered for unresolved slots, short mappings, and hand-made picks — preserved when the toggle never wrote that slot, replaced when it did, which is behaviour worth pinning either way. Also reverts all three queue-mode switches when a save fails, not just the new one; without it the card shows a setting the server rejected.
82 lines
4.3 KiB
Python
82 lines
4.3 KiB
Python
from datetime import datetime
|
|
|
|
from sqlalchemy import Boolean, DateTime, ForeignKey, Integer, String, func
|
|
from sqlalchemy.orm import Mapped, mapped_column
|
|
|
|
from backend.app.core.database import Base
|
|
|
|
# Canonical VP mode values. The legacy values `immediate` (→ archive) and
|
|
# `print_queue` (→ queue) shipped before the UI labels were aligned with the
|
|
# wire format. `normalize_vp_mode()` translates input from either form and
|
|
# the DB migration in `core/database.py` rewrites existing rows once at boot.
|
|
VP_MODE_ARCHIVE = "archive"
|
|
VP_MODE_REVIEW = "review"
|
|
VP_MODE_QUEUE = "queue"
|
|
VP_MODE_PROXY = "proxy"
|
|
VP_MODE_VALUES = (VP_MODE_ARCHIVE, VP_MODE_REVIEW, VP_MODE_QUEUE, VP_MODE_PROXY)
|
|
|
|
# Legacy → canonical map. Kept narrow on purpose so unrelated typos surface
|
|
# instead of getting silently re-pointed at a default.
|
|
_VP_MODE_ALIASES = {
|
|
"immediate": VP_MODE_ARCHIVE,
|
|
"print_queue": VP_MODE_QUEUE,
|
|
}
|
|
|
|
|
|
def normalize_vp_mode(value: str | None) -> str | None:
|
|
"""Map legacy wire values (`immediate`, `print_queue`) to canonical names.
|
|
|
|
Returns `None` unchanged so callers can decide whether to apply a default.
|
|
Returns unknown values unchanged so validators still see them and reject.
|
|
"""
|
|
if value is None:
|
|
return None
|
|
return _VP_MODE_ALIASES.get(value, value)
|
|
|
|
|
|
class VirtualPrinter(Base):
|
|
"""Virtual printer configuration for multi-instance support."""
|
|
|
|
__tablename__ = "virtual_printers"
|
|
|
|
id: Mapped[int] = mapped_column(primary_key=True)
|
|
name: Mapped[str] = mapped_column(String(100), default="Bambuddy")
|
|
enabled: Mapped[bool] = mapped_column(Boolean, default=False)
|
|
mode: Mapped[str] = mapped_column(String(20), default=VP_MODE_ARCHIVE) # archive|review|queue|proxy
|
|
auto_dispatch: Mapped[bool] = mapped_column(Boolean, server_default="true") # queue mode: auto-start or manual
|
|
queue_force_color_match: Mapped[bool] = mapped_column(
|
|
Boolean, server_default="false"
|
|
) # queue mode: pin per-slot type+color from the 3MF onto the queue
|
|
# item so the scheduler refuses to dispatch onto a printer with the wrong
|
|
# filament loaded (#1188).
|
|
save_ams_mapping: Mapped[bool] = mapped_column(
|
|
Boolean, server_default="false"
|
|
) # queue mode: keep the slicer's own live-resolved AMS-slot pick (the
|
|
# `ams_mapping` field on the MQTT `project_file` command) instead of
|
|
# re-deriving one from the file's static type/color. Stamps it on the queue
|
|
# item so THIS print dispatches to those trays, and onto the archive's
|
|
# `extra_data.slicer_ams_mapping` so a later reprint can reuse the same
|
|
# physical spools. Off by default: taking the slicer's pick makes the
|
|
# scheduler skip `_compute_ams_mapping_for_printer`, and with it
|
|
# `prefer_lowest_filament`, its AMS-backup gate (#1766) and the
|
|
# inventory-remain overrides — so it stays opt-in per virtual printer
|
|
# rather than changing behaviour for upgraders (#2700).
|
|
gcode_injection: Mapped[bool] = mapped_column(
|
|
Boolean, server_default="false"
|
|
) # queue mode: opt this VP's Send/Print jobs into per-model G-code snippet
|
|
# injection (#1516). Default off so existing gcode_snippets users don't
|
|
# silently start injecting; no-op when no snippets exist for the model.
|
|
model: Mapped[str | None] = mapped_column(String(50), nullable=True) # SSDP model code (server mode)
|
|
access_code: Mapped[str | None] = mapped_column(String(8), nullable=True) # 8 chars (server mode)
|
|
target_printer_id: Mapped[int | None] = mapped_column(
|
|
Integer, ForeignKey("printers.id", ondelete="SET NULL"), nullable=True
|
|
) # proxy mode
|
|
bind_ip: Mapped[str | None] = mapped_column(String(45), nullable=True) # dedicated IP (proxy mode)
|
|
remote_interface_ip: Mapped[str | None] = mapped_column(String(45), nullable=True) # SSDP advertise IP
|
|
tailscale_disabled: Mapped[bool] = mapped_column(
|
|
Boolean, server_default="true"
|
|
) # opt-in: user must explicitly enable; auto-detect only runs then
|
|
serial_suffix: Mapped[str] = mapped_column(String(9), default="391800001") # unique per printer
|
|
position: Mapped[int] = mapped_column(Integer, default=0)
|
|
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
|
|
updated_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now(), onupdate=func.now())
|