print_queue.py 12 KB

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