printer.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304
  1. from datetime import datetime
  2. from pydantic import BaseModel, Field
  3. class PrinterBase(BaseModel):
  4. name: str = Field(..., min_length=1, max_length=100)
  5. serial_number: str = Field(..., min_length=1, max_length=50)
  6. ip_address: str = Field(
  7. ...,
  8. max_length=253,
  9. pattern=r"^(\d{1,3}(\.\d{1,3}){3}|[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?)*)$",
  10. )
  11. access_code: str = Field(..., min_length=1, max_length=20)
  12. model: str | None = None
  13. location: str | None = None # Group/location name
  14. auto_archive: bool = True
  15. external_camera_url: str | None = None
  16. external_camera_type: str | None = None # "mjpeg", "rtsp", "snapshot", "usb"
  17. external_camera_enabled: bool = False
  18. camera_rotation: int = 0 # 0, 90, 180, 270 degrees
  19. class PrinterCreate(PrinterBase):
  20. pass
  21. class PlateDetectionROI(BaseModel):
  22. """Region of interest for plate detection (percentages 0.0-1.0)."""
  23. x: float = Field(..., ge=0.0, le=1.0) # X start %
  24. y: float = Field(..., ge=0.0, le=1.0) # Y start %
  25. w: float = Field(..., ge=0.0, le=1.0) # Width %
  26. h: float = Field(..., ge=0.0, le=1.0) # Height %
  27. class PrinterUpdate(BaseModel):
  28. name: str | None = None
  29. ip_address: str | None = Field(
  30. default=None,
  31. max_length=253,
  32. pattern=r"^(\d{1,3}(\.\d{1,3}){3}|[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?)*)$",
  33. )
  34. access_code: str | None = None
  35. model: str | None = None
  36. location: str | None = None
  37. is_active: bool | None = None
  38. auto_archive: bool | None = None
  39. print_hours_offset: float | None = None
  40. external_camera_url: str | None = None
  41. external_camera_type: str | None = None
  42. external_camera_enabled: bool | None = None
  43. camera_rotation: int | None = None # 0, 90, 180, 270 degrees
  44. plate_detection_enabled: bool | None = None
  45. plate_detection_roi: PlateDetectionROI | None = None
  46. class PrinterResponse(PrinterBase):
  47. id: int
  48. is_active: bool
  49. nozzle_count: int = 1 # 1 or 2, auto-detected from MQTT
  50. print_hours_offset: float = 0.0
  51. external_camera_url: str | None = None
  52. external_camera_type: str | None = None
  53. external_camera_enabled: bool = False
  54. camera_rotation: int = 0 # 0, 90, 180, 270 degrees
  55. plate_detection_enabled: bool = False
  56. plate_detection_roi: PlateDetectionROI | None = None
  57. created_at: datetime
  58. updated_at: datetime
  59. class Config:
  60. from_attributes = True
  61. @classmethod
  62. def from_orm_with_roi(cls, printer) -> "PrinterResponse":
  63. """Create response from ORM model, converting ROI fields to nested object."""
  64. data = {
  65. "id": printer.id,
  66. "name": printer.name,
  67. "serial_number": printer.serial_number,
  68. "ip_address": printer.ip_address,
  69. "access_code": printer.access_code,
  70. "model": printer.model,
  71. "location": printer.location,
  72. "auto_archive": printer.auto_archive,
  73. "external_camera_url": printer.external_camera_url,
  74. "external_camera_type": printer.external_camera_type,
  75. "external_camera_enabled": printer.external_camera_enabled,
  76. "camera_rotation": printer.camera_rotation,
  77. "is_active": printer.is_active,
  78. "nozzle_count": printer.nozzle_count,
  79. "print_hours_offset": printer.print_hours_offset,
  80. "plate_detection_enabled": printer.plate_detection_enabled,
  81. "created_at": printer.created_at,
  82. "updated_at": printer.updated_at,
  83. }
  84. # Build ROI object if any ROI field is set
  85. if any(
  86. [
  87. printer.plate_detection_roi_x is not None,
  88. printer.plate_detection_roi_y is not None,
  89. printer.plate_detection_roi_w is not None,
  90. printer.plate_detection_roi_h is not None,
  91. ]
  92. ):
  93. data["plate_detection_roi"] = PlateDetectionROI(
  94. x=printer.plate_detection_roi_x or 0.15,
  95. y=printer.plate_detection_roi_y or 0.35,
  96. w=printer.plate_detection_roi_w or 0.70,
  97. h=printer.plate_detection_roi_h or 0.55,
  98. )
  99. return cls(**data)
  100. class HMSErrorResponse(BaseModel):
  101. code: str
  102. attr: int = 0 # Attribute value for constructing wiki URL
  103. module: int
  104. severity: int # 1=fatal, 2=serious, 3=common, 4=info
  105. class AMSTray(BaseModel):
  106. id: int
  107. tray_color: str | None = None
  108. tray_type: str | None = None
  109. tray_sub_brands: str | None = None # Full name like "PLA Basic", "PETG HF"
  110. tray_id_name: str | None = None # Bambu filament ID like "A00-Y2" (can decode to color)
  111. tray_info_idx: str | None = None # Filament preset ID like "GFA00"
  112. remain: int = 0
  113. k: float | None = None # Pressure advance value (from tray or K-profile lookup)
  114. cali_idx: int | None = None # Calibration index for K-profile lookup
  115. tag_uid: str | None = None # RFID tag UID (any tag)
  116. tray_uuid: str | None = None # Bambu Lab spool UUID (32-char hex)
  117. nozzle_temp_min: int | None = None # Min nozzle temperature
  118. nozzle_temp_max: int | None = None # Max nozzle temperature
  119. drying_temp: int | None = None # RFID-recommended drying temp
  120. drying_time: int | None = None # RFID-recommended drying time (hours)
  121. state: int | None = None # AMS tray state: 9=empty, 10=spool present not loaded, 11=loaded
  122. class AMSUnit(BaseModel):
  123. id: int
  124. humidity: int | None = None
  125. temp: float | None = None
  126. is_ams_ht: bool = False # True for AMS-HT (single spool), False for regular AMS (4 spools)
  127. tray: list[AMSTray] = []
  128. serial_number: str = "" # AMS unit serial number (sn from MQTT)
  129. sw_ver: str = "" # AMS firmware version (from get_version info.module)
  130. dry_time: int = 0 # Minutes remaining (0 = not drying, >0 = drying active)
  131. dry_status: int = 0 # 0=Off, 1=Checking, 2=Drying, 3=Cooling, 4=Stopping, 5=Error
  132. dry_sub_status: int = 0 # 0=Off, 1=Heating, 2=Dehumidify
  133. dry_sf_reason: list[int] = [] # Cannot-dry reasons from firmware (see CannotDryReason)
  134. module_type: str = "" # "ams", "n3f", "n3s"
  135. class NozzleInfoResponse(BaseModel):
  136. nozzle_type: str = "" # "stainless_steel" or "hardened_steel"
  137. nozzle_diameter: str = "" # e.g., "0.4"
  138. class NozzleRackSlot(BaseModel):
  139. """H2C nozzle rack slot (6-position tool-changer dock)."""
  140. id: int = 0
  141. nozzle_type: str = ""
  142. nozzle_diameter: str = ""
  143. wear: int | None = None
  144. stat: int | None = None # Nozzle status (e.g. mounted/docked)
  145. max_temp: int = 0 # Max temperature rating °C (0 = not set)
  146. serial_number: str = "" # Nozzle serial number
  147. filament_color: str = "" # RGBA hex ("00000000" = no filament)
  148. filament_id: str = "" # Bambu filament ID
  149. filament_type: str = "" # Material type (e.g. "PLA", "PETG")
  150. class AmsLabelBody(BaseModel):
  151. label: str = Field(..., min_length=1, max_length=100)
  152. ams_serial: str = Field(default="", max_length=50)
  153. class FilaSwitchResponse(BaseModel):
  154. """Filament Track Switch (FTS) state — accessory that mediates AMS-to-extruder routing.
  155. When installed, the AMS info field reports bits 8-11 = 0xE (uninitialized)
  156. because slots are dynamically routed via the FTS rather than tied to a
  157. specific extruder. Frontend uses `installed` to suppress the per-extruder
  158. slot filter in the print modal. See #1162.
  159. """
  160. installed: bool = False
  161. # in[track] = currently loaded slot for that track (-1 = empty)
  162. in_slots: list[int] = []
  163. # out[track] = extruder this track terminates at (0 = right, 1 = left)
  164. out_extruders: list[int] = []
  165. stat: int = 0
  166. info: int = 0
  167. class PrintOptionsResponse(BaseModel):
  168. """AI detection and print options from xcam data."""
  169. # Core AI detectors
  170. spaghetti_detector: bool = False
  171. print_halt: bool = False
  172. halt_print_sensitivity: str = "medium" # Spaghetti sensitivity
  173. first_layer_inspector: bool = False
  174. printing_monitor: bool = False
  175. buildplate_marker_detector: bool = False
  176. allow_skip_parts: bool = False
  177. # Additional AI detectors (decoded from cfg bitmask)
  178. nozzle_clumping_detector: bool = True
  179. nozzle_clumping_sensitivity: str = "medium"
  180. pileup_detector: bool = True
  181. pileup_sensitivity: str = "medium"
  182. airprint_detector: bool = True
  183. airprint_sensitivity: str = "medium"
  184. auto_recovery_step_loss: bool = True
  185. filament_tangle_detect: bool = False
  186. class PrinterStatus(BaseModel):
  187. id: int
  188. name: str
  189. connected: bool
  190. state: str | None = None
  191. current_print: str | None = None
  192. subtask_name: str | None = None
  193. gcode_file: str | None = None
  194. progress: float | None = None
  195. remaining_time: int | None = None
  196. layer_num: int | None = None
  197. total_layers: int | None = None
  198. temperatures: dict | None = None
  199. cover_url: str | None = None
  200. hms_errors: list[HMSErrorResponse] = []
  201. ams: list[AMSUnit] = []
  202. ams_exists: bool = False
  203. vt_tray: list[AMSTray] = [] # Virtual tray / external spool(s)
  204. sdcard: bool = False # SD card inserted
  205. store_to_sdcard: bool = False # Store sent files on SD card
  206. timelapse: bool = False # Timelapse recording active
  207. ipcam: bool = False # Live view enabled
  208. wifi_signal: int | None = None # WiFi signal strength in dBm
  209. wired_network: bool = False # Ethernet connection detected
  210. door_open: bool = False # Enclosure door open (X1/P1S/P2S/H2*)
  211. nozzles: list[NozzleInfoResponse] = [] # Nozzle hardware info (index 0=left/primary, 1=right)
  212. nozzle_rack: list[NozzleRackSlot] = [] # H2C 6-nozzle tool-changer rack
  213. print_options: PrintOptionsResponse | None = None # AI detection and print options
  214. # Calibration stage tracking
  215. stg_cur: int = -1 # Current stage number (-1 = not calibrating)
  216. stg_cur_name: str | None = None # Human-readable current stage name
  217. stg: list[int] = [] # List of stage numbers in calibration sequence
  218. # Air conditioning mode (0=cooling, 1=heating)
  219. airduct_mode: int = 0
  220. # Print speed level (1=silent, 2=standard, 3=sport, 4=ludicrous)
  221. speed_level: int = 2
  222. # Chamber light on/off
  223. chamber_light: bool = False
  224. # Active extruder for dual nozzle (0=right, 1=left)
  225. active_extruder: int = 0
  226. # AMS mapping for dual nozzle: which AMS is connected to which nozzle
  227. ams_mapping: list[int] = []
  228. # Per-AMS extruder map: {ams_id: extruder_id} where 0=right, 1=left
  229. ams_extruder_map: dict[str, int] = {}
  230. # Filament Track Switch (FTS) accessory — when installed, AMS reports
  231. # bits 8-11 = 0xE (uninitialized) and routing is dynamic via the FTS. See #1162.
  232. fila_switch: FilaSwitchResponse | None = None
  233. # Currently loaded tray (global ID): 254 = external spool, 255 = no filament
  234. tray_now: int = 255
  235. # AMS status for filament change tracking
  236. # Main status: 0=idle, 1=filament_change, 2=rfid_identifying, 3=assist, 4=calibration
  237. ams_status_main: int = 0
  238. # Sub status: specific step within filament change (when main=1)
  239. # Known values: 4=retraction, 6=load verification, 7=purge
  240. ams_status_sub: int = 0
  241. # mc_print_sub_stage - filament change step indicator used by OrcaSlicer/BambuStudio
  242. mc_print_sub_stage: int = 0
  243. # Timestamp of last AMS data update (for RFID refresh detection)
  244. last_ams_update: float = 0.0
  245. # Number of printable objects in current print (for skip objects feature)
  246. printable_objects_count: int = 0
  247. # Fan speeds (0-100 percentage, None if not available for this model)
  248. cooling_fan_speed: int | None = None # Part cooling fan
  249. big_fan1_speed: int | None = None # Auxiliary fan
  250. big_fan2_speed: int | None = None # Chamber/exhaust fan
  251. heatbreak_fan_speed: int | None = None # Hotend heatbreak fan
  252. # Firmware version (from info.module[name="ota"].sw_ver)
  253. firmware_version: str | None = None
  254. # Developer LAN mode: True = enabled, False = disabled (MQTT encryption), None = unknown
  255. developer_mode: bool | None = None
  256. # Queue: printer is awaiting the user to acknowledge the build plate is cleared
  257. # after a finished/failed print. Persisted across restarts (#961).
  258. awaiting_plate_clear: bool = False
  259. # AMS drying support
  260. supports_drying: bool = False
  261. # Linked archive for the active print (resolved via subtask_id). Frontend uses
  262. # this to fetch plate metadata and show the plate name when the source 3MF is
  263. # multi-plate (#881 follow-up).
  264. current_archive_id: int | None = None
  265. # 1-indexed plate number parsed from gcode_file (e.g. /Metadata/plate_2.gcode).
  266. # Set for every active print regardless of plate count; the frontend decides
  267. # whether to render it based on current_archive_id's is_multi_plate flag.
  268. current_plate_id: int | None = None