mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-08 07:01:40 +02:00
Every field that takes a chamber target stopped at 60: the per-filament chamber map and per-print override in Preheat & Heat Soak, the chamber quick-select presets, and the printer-card chamber control. 60 is the X1E's ceiling and the X1E was the only heated-chamber model when that limit was written; the H2 series and X2D heat to 65, so the top of their range was unreachable. The ceiling now lives in one constant per side (MAX_CHAMBER_TEMP_C in backend/app/utils/printer_models.py and frontend/src/utils/printer.ts) rather than as a literal at each call site. X1E firmware clamps a higher request to its own maximum, so a shared ceiling is safe. Also fixes a live bug at PrintersPage.tsx:7985: parsePresetTriple was bounded to 60 there, and it rejects the whole triple on any out-of-range entry, so a saved 65 preset would have silently reverted the printer card to the defaults while Settings still showed 65.
336 lines
14 KiB
Python
336 lines
14 KiB
Python
from datetime import datetime
|
|
from typing import Annotated, Literal
|
|
|
|
from pydantic import BaseModel, BeforeValidator, Field, PlainSerializer, model_validator
|
|
|
|
from backend.app.utils.printer_models import MAX_CHAMBER_TEMP_C
|
|
|
|
|
|
# Custom serializer to ensure UTC datetimes have Z suffix
|
|
def serialize_utc_datetime(dt: datetime | None) -> str | None:
|
|
if dt is None:
|
|
return None
|
|
# Add Z suffix to indicate UTC
|
|
return dt.isoformat() + "Z"
|
|
|
|
|
|
UTCDatetime = Annotated[datetime | None, PlainSerializer(serialize_utc_datetime)]
|
|
|
|
|
|
def _coerce_tristate(v: object) -> object:
|
|
"""Map legacy on/off booleans onto the tri-state calibration options.
|
|
|
|
bed_levelling / flow_cali / nozzle_offset_cali were plain booleans before we
|
|
added BambuStudio's third "auto" state (skip if recently done). Rows and API
|
|
payloads created under the old scheme carry bool / 0-1 int / "true"/"false";
|
|
coerce them so old clients and un-migrated rows still validate. getValueInt
|
|
parity: off=0, on=1, auto=2.
|
|
"""
|
|
if isinstance(v, bool):
|
|
return "on" if v else "off"
|
|
if isinstance(v, int):
|
|
return {0: "off", 1: "on", 2: "auto"}.get(v, "auto")
|
|
if isinstance(v, str):
|
|
low = v.strip().lower()
|
|
if low in ("true", "1"):
|
|
return "on"
|
|
if low in ("false", "0"):
|
|
return "off"
|
|
return v
|
|
|
|
|
|
# Tri-state calibration option: "auto" (printer decides / skip if recent),
|
|
# "on" (force every print), "off" (never). Mirrors BambuStudio's ops_auto.
|
|
TriState = Annotated[Literal["off", "on", "auto"], BeforeValidator(_coerce_tristate)]
|
|
|
|
|
|
class PrintQueueItemCreate(BaseModel):
|
|
printer_id: int | None = None # None = unassigned, user assigns later
|
|
target_model: str | None = None # Target printer model (mutually exclusive with printer_id)
|
|
target_location: str | None = None # Target location filter (only used with target_model)
|
|
required_filament_types: list[str] | None = None # Required filament types for model-based assignment
|
|
filament_overrides: list[dict] | None = None # Filament overrides for model-based assignment
|
|
# Either archive_id OR library_file_id must be provided
|
|
archive_id: int | None = None
|
|
library_file_id: int | None = None
|
|
scheduled_time: datetime | None = None # None = ASAP (next when idle)
|
|
require_previous_success: bool = False
|
|
auto_off_after: bool = False # Power off printer after print completes
|
|
manual_start: bool = False # Requires manual trigger to start (staged)
|
|
insert_at_top: bool = False # Insert ahead of other pending items in the same queue scope
|
|
insert_position: int | None = None # 1-indexed insertion position for priority queueing
|
|
# Persistent "Print Anyway" acknowledgement (#1698-followup). When set,
|
|
# PrintModal already showed the deficit warning and the user confirmed,
|
|
# so the scheduler does not re-flag this item on the next tick.
|
|
skip_filament_check: bool = False
|
|
# AMS mapping: list of global tray IDs for each filament slot
|
|
# Format: [5, -1, 2, -1] where position = slot_id-1, value = global tray ID (-1 = unused)
|
|
ams_mapping: list[int] | None = None
|
|
# Plate ID for multi-plate 3MF files (1-indexed, None = auto-detect/plate 1)
|
|
plate_id: int | None = None
|
|
# Print options. bed_levelling / flow_cali / nozzle_offset_cali are tri-state
|
|
# (off/on/auto), defaulting to "auto" to match BambuStudio. vibration_cali /
|
|
# layer_inspect / timelapse stay on/off (BambuStudio exposes no auto for them).
|
|
bed_levelling: TriState = "auto"
|
|
flow_cali: TriState = "auto"
|
|
vibration_cali: bool = True
|
|
layer_inspect: bool = False
|
|
timelapse: bool = False
|
|
use_ams: bool = True
|
|
# Nozzle offset calibration — dual-nozzle printers only (#1682). The MQTT
|
|
# layer ignores the value on single-nozzle printers so the wire stays "skip".
|
|
nozzle_offset_cali: TriState = "auto"
|
|
# Preheat / heat-soak per-item override (#1468). 'inherit' uses the global
|
|
# preheat_enabled setting; 'on' / 'off' force the decision. The chamber
|
|
# target falls through: this override → max(filament-map[loaded tray]) → 0.
|
|
preheat_override: Literal["inherit", "on", "off"] = "inherit"
|
|
preheat_chamber_target_override: int | None = Field(default=None, ge=0, le=MAX_CHAMBER_TEMP_C)
|
|
# Auto-print G-code injection
|
|
gcode_injection: bool = False
|
|
# Batch: create multiple copies (creates a batch if > 1)
|
|
quantity: int = 1
|
|
# Existing batch to add this item into. When set, the item's batch_id is
|
|
# populated on insert so the queue UI groups it with its siblings. Used by
|
|
# the multi-plate auto-batch flow and by the "Group as batch" action.
|
|
batch_id: int | None = None
|
|
# Project to associate the resulting archive with
|
|
project_id: int | None = None
|
|
# Direct printer-card uploads are temporary library files. The scheduler
|
|
# deletes them after creating the durable archive copy.
|
|
cleanup_library_after_dispatch: bool = False
|
|
|
|
|
|
class PrintQueueItemUpdate(BaseModel):
|
|
printer_id: int | None = None
|
|
target_model: str | None = None # Target printer model (mutually exclusive with printer_id)
|
|
target_location: str | None = None # Target location filter (only used with target_model)
|
|
filament_overrides: list[dict] | None = None # Filament overrides for model-based assignment
|
|
position: int | None = None
|
|
scheduled_time: datetime | None = None
|
|
require_previous_success: bool | None = None
|
|
auto_off_after: bool | None = None
|
|
manual_start: bool | None = None
|
|
ams_mapping: list[int] | None = None
|
|
plate_id: int | None = None
|
|
# Print options
|
|
bed_levelling: TriState | None = None
|
|
flow_cali: TriState | None = None
|
|
vibration_cali: bool | None = None
|
|
layer_inspect: bool | None = None
|
|
timelapse: bool | None = None
|
|
use_ams: bool | None = None
|
|
nozzle_offset_cali: TriState | None = None
|
|
preheat_override: Literal["inherit", "on", "off"] | None = None
|
|
preheat_chamber_target_override: int | None = Field(default=None, ge=0, le=MAX_CHAMBER_TEMP_C)
|
|
# Auto-print G-code injection
|
|
gcode_injection: bool | None = None
|
|
# H2C dual-nozzle-rack slicer pick (#1780). list[int] per-filament
|
|
# physical nozzle position IDs from BambuStudio's project_file MQTT
|
|
# body; sent back to the printer verbatim on dispatch.
|
|
nozzle_mapping: list[int] | None = None
|
|
|
|
|
|
class PrintQueueItemResponse(BaseModel):
|
|
id: int
|
|
printer_id: int | None # None = unassigned
|
|
target_model: str | None = None # Target printer model for model-based assignment
|
|
target_location: str | None = None # Target location filter for model-based assignment
|
|
required_filament_types: list[str] | None = None # Required filament types for model-based assignment
|
|
filament_overrides: list[dict] | None = None # Filament overrides for model-based assignment
|
|
waiting_reason: str | None = None # Why a model-based job hasn't started yet
|
|
archive_id: int | None # None if library_file_id is set (archive created at print start)
|
|
library_file_id: int | None # For queue items from library files
|
|
position: int
|
|
scheduled_time: UTCDatetime
|
|
require_previous_success: bool
|
|
auto_off_after: bool
|
|
manual_start: bool
|
|
# True when the dispatch scheduler last evaluated this item and the
|
|
# assigned spool could not satisfy at least one slot's required grams
|
|
# (#1496). Display-only — the ▶ click recomputes deficit against live
|
|
# spool state.
|
|
filament_short: bool = False
|
|
# User has acknowledged "Print Anyway" — scheduler skips the deficit check
|
|
# for this item (#1698-followup).
|
|
skip_filament_check: bool = False
|
|
ams_mapping: list[int] | None = None
|
|
plate_id: int | None = None # Plate ID for multi-plate 3MF files
|
|
# Print options
|
|
bed_levelling: TriState = "auto"
|
|
flow_cali: TriState = "auto"
|
|
vibration_cali: bool = True
|
|
layer_inspect: bool = False
|
|
timelapse: bool = False
|
|
use_ams: bool = True
|
|
nozzle_offset_cali: TriState = "auto"
|
|
preheat_override: Literal["inherit", "on", "off"] = "inherit"
|
|
preheat_chamber_target_override: int | None = None
|
|
status: Literal["pending", "printing", "completed", "failed", "skipped", "cancelled"]
|
|
started_at: UTCDatetime
|
|
completed_at: UTCDatetime
|
|
error_message: str | None
|
|
created_at: UTCDatetime
|
|
|
|
# Nested info for UI (populated in route)
|
|
archive_name: str | None = None
|
|
archive_thumbnail: str | None = None
|
|
# True when the linked archive has been soft-deleted (its files are gone
|
|
# from disk). In that case the *archive_name* / *archive_thumbnail* /
|
|
# downstream metadata fields are intentionally left None so the frontend
|
|
# doesn't 404-storm the now-missing thumbnail / plates / plate-thumbnail
|
|
# endpoints (#1348 follow-up). Frontends can render a "source deleted"
|
|
# badge based on this flag.
|
|
archive_deleted: bool = False
|
|
library_file_name: str | None = None # Name of library file (if library_file_id is set)
|
|
library_file_thumbnail: str | None = None # Thumbnail of library file
|
|
printer_name: str | None = None
|
|
print_time_seconds: int | None = None # Estimated print time from archive or library file
|
|
filament_used_grams: float | None = None # Estimated print weight from archive or library file
|
|
filament_type: str | None = None # e.g. "PLA", "PETG" (from archive/library file)
|
|
filament_color: str | None = None # e.g. "#FFFFFF" (from archive/library file)
|
|
layer_height: float | None = None # e.g. 0.2 (from archive/library file)
|
|
nozzle_diameter: float | None = None # e.g. 0.4 (from archive/library file)
|
|
sliced_for_model: str | None = None # e.g. "P1S" (from archive/library file)
|
|
# Build plate type (e.g. "Textured PEI Plate") so the user knows which
|
|
# plate to mount on the printer (#1281). Per-plate accurate on multi-plate
|
|
# 3MFs: when `plate_id` is set, the value is the matching plate's
|
|
# `curr_bed_type` rather than the archive-level first-plate default.
|
|
bed_type: str | None = None
|
|
# True when the source archive carries the slicer's own live-resolved
|
|
# AMS-slot pick (extra_data.slicer_ams_mapping) *and* it was resolved
|
|
# against this row's own printer — the only case where dispatch actually
|
|
# reuses that exact physical spool instead of the scheduler re-deriving one
|
|
# from the file's static type/color.
|
|
archive_has_slicer_ams_mapping: bool = False
|
|
|
|
# User tracking (Issue #206)
|
|
created_by_id: int | None = None
|
|
created_by_username: str | None = None
|
|
|
|
# Batch grouping
|
|
batch_id: int | None = None
|
|
batch_name: str | None = None
|
|
|
|
# Shortest-job-first scheduling
|
|
been_jumped: bool = False
|
|
|
|
# Auto-print G-code injection
|
|
gcode_injection: bool = False
|
|
cleanup_library_after_dispatch: bool = False
|
|
|
|
# H2C dual-nozzle-rack slicer pick (#1780). Surface for any future
|
|
# "edit print → choose nozzle" UI; null on every model except O1C2
|
|
# uploads from BambuStudio.
|
|
nozzle_mapping: list[int] | None = None
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
|
|
class PrintQueueReorderItem(BaseModel):
|
|
id: int
|
|
position: int
|
|
|
|
|
|
class PrintQueueReorder(BaseModel):
|
|
items: list[PrintQueueReorderItem]
|
|
|
|
@model_validator(mode="after")
|
|
def _validate_positions_unique(self) -> "PrintQueueReorder":
|
|
"""Reject reorder requests with duplicate positions in the payload
|
|
(#1625-followup).
|
|
|
|
The /reorder route is the drag-drop renumber path on the queue UI;
|
|
a well-behaved client sends a contiguous renumbering of a single
|
|
printer's pending queue. A buggy client that sends two items at
|
|
the same position would leave the queue in an inconsistent state
|
|
(scheduler's ORDER BY (printer_id, position) ties get broken by
|
|
physical row order). Fail closed at the schema boundary so the
|
|
bug is caught before any DB mutation.
|
|
|
|
Uniqueness is enforced WITHIN THE PAYLOAD only — cross-printer
|
|
reorders that intentionally share positions across different
|
|
printer queues are a non-goal of the drag-drop UI, so this is the
|
|
right scope.
|
|
"""
|
|
positions = [it.position for it in self.items]
|
|
if len(positions) != len(set(positions)):
|
|
duplicates = sorted({p for p in positions if positions.count(p) > 1})
|
|
raise ValueError(f"Duplicate positions in reorder request: {duplicates}")
|
|
return self
|
|
|
|
|
|
class PrintQueueBulkUpdate(BaseModel):
|
|
"""Bulk update multiple queue items with the same values."""
|
|
|
|
item_ids: list[int]
|
|
# Fields to update (all optional - only set fields are applied)
|
|
printer_id: int | None = None
|
|
scheduled_time: datetime | None = None
|
|
require_previous_success: bool | None = None
|
|
auto_off_after: bool | None = None
|
|
manual_start: bool | None = None
|
|
# Print options
|
|
bed_levelling: TriState | None = None
|
|
flow_cali: TriState | None = None
|
|
vibration_cali: bool | None = None
|
|
layer_inspect: bool | None = None
|
|
timelapse: bool | None = None
|
|
use_ams: bool | None = None
|
|
nozzle_offset_cali: TriState | None = None
|
|
preheat_override: Literal["inherit", "on", "off"] | None = None
|
|
preheat_chamber_target_override: int | None = Field(default=None, ge=0, le=MAX_CHAMBER_TEMP_C)
|
|
# Auto-print G-code injection
|
|
gcode_injection: bool | None = None
|
|
|
|
|
|
class PrintQueueBulkUpdateResponse(BaseModel):
|
|
"""Response for bulk update operation."""
|
|
|
|
updated_count: int
|
|
skipped_count: int # Items that were not pending
|
|
message: str
|
|
|
|
|
|
class PrintBatchCreate(BaseModel):
|
|
"""Create a batch, either empty (multi-plate pre-batch flow) or by
|
|
assigning existing pending queue items into it (manual "Group as batch")."""
|
|
|
|
name: str
|
|
archive_id: int | None = None
|
|
library_file_id: int | None = None
|
|
# Existing pending queue items to assign to this batch. None / empty for
|
|
# the empty-batch flow (client passes the returned id on subsequent
|
|
# addToQueue calls).
|
|
item_ids: list[int] | None = None
|
|
|
|
|
|
class PrintBatchUngroupResponse(BaseModel):
|
|
"""Response after ungrouping a batch."""
|
|
|
|
ungrouped_count: int
|
|
message: str
|
|
|
|
|
|
class PrintBatchResponse(BaseModel):
|
|
"""Response for a print batch with progress stats."""
|
|
|
|
id: int
|
|
name: str
|
|
archive_id: int | None = None
|
|
library_file_id: int | None = None
|
|
quantity: int
|
|
status: str
|
|
created_at: UTCDatetime
|
|
created_by_id: int | None = None
|
|
created_by_username: str | None = None
|
|
# Derived counts
|
|
pending_count: int = 0
|
|
printing_count: int = 0
|
|
completed_count: int = 0
|
|
failed_count: int = 0
|
|
cancelled_count: int = 0
|
|
|
|
class Config:
|
|
from_attributes = True
|