print_queue.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299
  1. from datetime import datetime
  2. from typing import Annotated, Literal
  3. from pydantic import BaseModel, Field, PlainSerializer, model_validator
  4. # Custom serializer to ensure UTC datetimes have Z suffix
  5. def serialize_utc_datetime(dt: datetime | None) -> str | None:
  6. if dt is None:
  7. return None
  8. # Add Z suffix to indicate UTC
  9. return dt.isoformat() + "Z"
  10. UTCDatetime = Annotated[datetime | None, PlainSerializer(serialize_utc_datetime)]
  11. class PrintQueueItemCreate(BaseModel):
  12. printer_id: int | None = None # None = unassigned, user assigns later
  13. target_model: str | None = None # Target printer model (mutually exclusive with printer_id)
  14. target_location: str | None = None # Target location filter (only used with target_model)
  15. required_filament_types: list[str] | None = None # Required filament types for model-based assignment
  16. filament_overrides: list[dict] | None = None # Filament overrides for model-based assignment
  17. # Either archive_id OR library_file_id must be provided
  18. archive_id: int | None = None
  19. library_file_id: int | None = None
  20. scheduled_time: datetime | None = None # None = ASAP (next when idle)
  21. require_previous_success: bool = False
  22. auto_off_after: bool = False # Power off printer after print completes
  23. manual_start: bool = False # Requires manual trigger to start (staged)
  24. insert_at_top: bool = False # Insert ahead of other pending items in the same queue scope
  25. insert_position: int | None = None # 1-indexed insertion position for priority queueing
  26. # Persistent "Print Anyway" acknowledgement (#1698-followup). When set,
  27. # PrintModal already showed the deficit warning and the user confirmed,
  28. # so the scheduler does not re-flag this item on the next tick.
  29. skip_filament_check: bool = False
  30. # AMS mapping: list of global tray IDs for each filament slot
  31. # Format: [5, -1, 2, -1] where position = slot_id-1, value = global tray ID (-1 = unused)
  32. ams_mapping: list[int] | None = None
  33. # Plate ID for multi-plate 3MF files (1-indexed, None = auto-detect/plate 1)
  34. plate_id: int | None = None
  35. # Print options
  36. bed_levelling: bool = True
  37. flow_cali: bool = False
  38. vibration_cali: bool = True
  39. layer_inspect: bool = False
  40. timelapse: bool = False
  41. use_ams: bool = True
  42. # Nozzle offset calibration — dual-nozzle printers only (#1682). Default True
  43. # matches BambuStudio's default; the MQTT layer ignores the flag on
  44. # single-nozzle printers so the wire value stays "skip" there.
  45. nozzle_offset_cali: bool = True
  46. # Preheat / heat-soak per-item override (#1468). 'inherit' uses the global
  47. # preheat_enabled setting; 'on' / 'off' force the decision. The chamber
  48. # target falls through: this override → max(filament-map[loaded tray]) → 0.
  49. preheat_override: Literal["inherit", "on", "off"] = "inherit"
  50. preheat_chamber_target_override: int | None = Field(default=None, ge=0, le=60)
  51. # Auto-print G-code injection
  52. gcode_injection: bool = False
  53. # Batch: create multiple copies (creates a batch if > 1)
  54. quantity: int = 1
  55. # Existing batch to add this item into. When set, the item's batch_id is
  56. # populated on insert so the queue UI groups it with its siblings. Used by
  57. # the multi-plate auto-batch flow and by the "Group as batch" action.
  58. batch_id: int | None = None
  59. # Project to associate the resulting archive with
  60. project_id: int | None = None
  61. # Direct printer-card uploads are temporary library files. The scheduler
  62. # deletes them after creating the durable archive copy.
  63. cleanup_library_after_dispatch: bool = False
  64. class PrintQueueItemUpdate(BaseModel):
  65. printer_id: int | None = None
  66. target_model: str | None = None # Target printer model (mutually exclusive with printer_id)
  67. target_location: str | None = None # Target location filter (only used with target_model)
  68. filament_overrides: list[dict] | None = None # Filament overrides for model-based assignment
  69. position: int | None = None
  70. scheduled_time: datetime | None = None
  71. require_previous_success: bool | None = None
  72. auto_off_after: bool | None = None
  73. manual_start: bool | None = None
  74. ams_mapping: list[int] | None = None
  75. plate_id: int | None = None
  76. # Print options
  77. bed_levelling: bool | None = None
  78. flow_cali: bool | None = None
  79. vibration_cali: bool | None = None
  80. layer_inspect: bool | None = None
  81. timelapse: bool | None = None
  82. use_ams: bool | None = None
  83. nozzle_offset_cali: bool | None = None
  84. preheat_override: Literal["inherit", "on", "off"] | None = None
  85. preheat_chamber_target_override: int | None = Field(default=None, ge=0, le=60)
  86. # Auto-print G-code injection
  87. gcode_injection: bool | None = None
  88. # H2C dual-nozzle-rack slicer pick (#1780). list[int] per-filament
  89. # physical nozzle position IDs from BambuStudio's project_file MQTT
  90. # body; sent back to the printer verbatim on dispatch.
  91. nozzle_mapping: list[int] | None = None
  92. class PrintQueueItemResponse(BaseModel):
  93. id: int
  94. printer_id: int | None # None = unassigned
  95. target_model: str | None = None # Target printer model for model-based assignment
  96. target_location: str | None = None # Target location filter for model-based assignment
  97. required_filament_types: list[str] | None = None # Required filament types for model-based assignment
  98. filament_overrides: list[dict] | None = None # Filament overrides for model-based assignment
  99. waiting_reason: str | None = None # Why a model-based job hasn't started yet
  100. archive_id: int | None # None if library_file_id is set (archive created at print start)
  101. library_file_id: int | None # For queue items from library files
  102. position: int
  103. scheduled_time: UTCDatetime
  104. require_previous_success: bool
  105. auto_off_after: bool
  106. manual_start: bool
  107. # True when the dispatch scheduler last evaluated this item and the
  108. # assigned spool could not satisfy at least one slot's required grams
  109. # (#1496). Display-only — the ▶ click recomputes deficit against live
  110. # spool state.
  111. filament_short: bool = False
  112. # User has acknowledged "Print Anyway" — scheduler skips the deficit check
  113. # for this item (#1698-followup).
  114. skip_filament_check: bool = False
  115. ams_mapping: list[int] | None = None
  116. plate_id: int | None = None # Plate ID for multi-plate 3MF files
  117. # Print options
  118. bed_levelling: bool = True
  119. flow_cali: bool = False
  120. vibration_cali: bool = True
  121. layer_inspect: bool = False
  122. timelapse: bool = False
  123. use_ams: bool = True
  124. nozzle_offset_cali: bool = True
  125. preheat_override: Literal["inherit", "on", "off"] = "inherit"
  126. preheat_chamber_target_override: int | None = None
  127. status: Literal["pending", "printing", "completed", "failed", "skipped", "cancelled"]
  128. started_at: UTCDatetime
  129. completed_at: UTCDatetime
  130. error_message: str | None
  131. created_at: UTCDatetime
  132. # Nested info for UI (populated in route)
  133. archive_name: str | None = None
  134. archive_thumbnail: str | None = None
  135. # True when the linked archive has been soft-deleted (its files are gone
  136. # from disk). In that case the *archive_name* / *archive_thumbnail* /
  137. # downstream metadata fields are intentionally left None so the frontend
  138. # doesn't 404-storm the now-missing thumbnail / plates / plate-thumbnail
  139. # endpoints (#1348 follow-up). Frontends can render a "source deleted"
  140. # badge based on this flag.
  141. archive_deleted: bool = False
  142. library_file_name: str | None = None # Name of library file (if library_file_id is set)
  143. library_file_thumbnail: str | None = None # Thumbnail of library file
  144. printer_name: str | None = None
  145. print_time_seconds: int | None = None # Estimated print time from archive or library file
  146. filament_used_grams: float | None = None # Estimated print weight from archive or library file
  147. filament_type: str | None = None # e.g. "PLA", "PETG" (from archive/library file)
  148. filament_color: str | None = None # e.g. "#FFFFFF" (from archive/library file)
  149. layer_height: float | None = None # e.g. 0.2 (from archive/library file)
  150. nozzle_diameter: float | None = None # e.g. 0.4 (from archive/library file)
  151. sliced_for_model: str | None = None # e.g. "P1S" (from archive/library file)
  152. # Build plate type (e.g. "Textured PEI Plate") so the user knows which
  153. # plate to mount on the printer (#1281). Per-plate accurate on multi-plate
  154. # 3MFs: when `plate_id` is set, the value is the matching plate's
  155. # `curr_bed_type` rather than the archive-level first-plate default.
  156. bed_type: str | None = None
  157. # User tracking (Issue #206)
  158. created_by_id: int | None = None
  159. created_by_username: str | None = None
  160. # Batch grouping
  161. batch_id: int | None = None
  162. batch_name: str | None = None
  163. # Shortest-job-first scheduling
  164. been_jumped: bool = False
  165. # Auto-print G-code injection
  166. gcode_injection: bool = False
  167. cleanup_library_after_dispatch: bool = False
  168. # H2C dual-nozzle-rack slicer pick (#1780). Surface for any future
  169. # "edit print → choose nozzle" UI; null on every model except O1C2
  170. # uploads from BambuStudio.
  171. nozzle_mapping: list[int] | None = None
  172. class Config:
  173. from_attributes = True
  174. class PrintQueueReorderItem(BaseModel):
  175. id: int
  176. position: int
  177. class PrintQueueReorder(BaseModel):
  178. items: list[PrintQueueReorderItem]
  179. @model_validator(mode="after")
  180. def _validate_positions_unique(self) -> "PrintQueueReorder":
  181. """Reject reorder requests with duplicate positions in the payload
  182. (#1625-followup).
  183. The /reorder route is the drag-drop renumber path on the queue UI;
  184. a well-behaved client sends a contiguous renumbering of a single
  185. printer's pending queue. A buggy client that sends two items at
  186. the same position would leave the queue in an inconsistent state
  187. (scheduler's ORDER BY (printer_id, position) ties get broken by
  188. physical row order). Fail closed at the schema boundary so the
  189. bug is caught before any DB mutation.
  190. Uniqueness is enforced WITHIN THE PAYLOAD only — cross-printer
  191. reorders that intentionally share positions across different
  192. printer queues are a non-goal of the drag-drop UI, so this is the
  193. right scope.
  194. """
  195. positions = [it.position for it in self.items]
  196. if len(positions) != len(set(positions)):
  197. duplicates = sorted({p for p in positions if positions.count(p) > 1})
  198. raise ValueError(f"Duplicate positions in reorder request: {duplicates}")
  199. return self
  200. class PrintQueueBulkUpdate(BaseModel):
  201. """Bulk update multiple queue items with the same values."""
  202. item_ids: list[int]
  203. # Fields to update (all optional - only set fields are applied)
  204. printer_id: int | None = None
  205. scheduled_time: datetime | None = None
  206. require_previous_success: bool | None = None
  207. auto_off_after: bool | None = None
  208. manual_start: bool | None = None
  209. # Print options
  210. bed_levelling: bool | None = None
  211. flow_cali: bool | None = None
  212. vibration_cali: bool | None = None
  213. layer_inspect: bool | None = None
  214. timelapse: bool | None = None
  215. use_ams: bool | None = None
  216. nozzle_offset_cali: bool | None = None
  217. preheat_override: Literal["inherit", "on", "off"] | None = None
  218. preheat_chamber_target_override: int | None = Field(default=None, ge=0, le=60)
  219. # Auto-print G-code injection
  220. gcode_injection: bool | None = None
  221. class PrintQueueBulkUpdateResponse(BaseModel):
  222. """Response for bulk update operation."""
  223. updated_count: int
  224. skipped_count: int # Items that were not pending
  225. message: str
  226. class PrintBatchCreate(BaseModel):
  227. """Create a batch, either empty (multi-plate pre-batch flow) or by
  228. assigning existing pending queue items into it (manual "Group as batch")."""
  229. name: str
  230. archive_id: int | None = None
  231. library_file_id: int | None = None
  232. # Existing pending queue items to assign to this batch. None / empty for
  233. # the empty-batch flow (client passes the returned id on subsequent
  234. # addToQueue calls).
  235. item_ids: list[int] | None = None
  236. class PrintBatchUngroupResponse(BaseModel):
  237. """Response after ungrouping a batch."""
  238. ungrouped_count: int
  239. message: str
  240. class PrintBatchResponse(BaseModel):
  241. """Response for a print batch with progress stats."""
  242. id: int
  243. name: str
  244. archive_id: int | None = None
  245. library_file_id: int | None = None
  246. quantity: int
  247. status: str
  248. created_at: UTCDatetime
  249. created_by_id: int | None = None
  250. created_by_username: str | None = None
  251. # Derived counts
  252. pending_count: int = 0
  253. printing_count: int = 0
  254. completed_count: int = 0
  255. failed_count: int = 0
  256. cancelled_count: int = 0
  257. class Config:
  258. from_attributes = True