printer.py 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  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(..., pattern=r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$")
  7. access_code: str = Field(..., min_length=1, max_length=20)
  8. model: str | None = None
  9. location: str | None = None # Group/location name
  10. auto_archive: bool = True
  11. external_camera_url: str | None = None
  12. external_camera_type: str | None = None # "mjpeg", "rtsp", "snapshot", "usb"
  13. external_camera_enabled: bool = False
  14. class PrinterCreate(PrinterBase):
  15. pass
  16. class PlateDetectionROI(BaseModel):
  17. """Region of interest for plate detection (percentages 0.0-1.0)."""
  18. x: float = Field(..., ge=0.0, le=1.0) # X start %
  19. y: float = Field(..., ge=0.0, le=1.0) # Y start %
  20. w: float = Field(..., ge=0.0, le=1.0) # Width %
  21. h: float = Field(..., ge=0.0, le=1.0) # Height %
  22. class PrinterUpdate(BaseModel):
  23. name: str | None = None
  24. ip_address: str | None = None
  25. access_code: str | None = None
  26. model: str | None = None
  27. location: str | None = None
  28. is_active: bool | None = None
  29. auto_archive: bool | None = None
  30. print_hours_offset: float | None = None
  31. external_camera_url: str | None = None
  32. external_camera_type: str | None = None
  33. external_camera_enabled: bool | None = None
  34. plate_detection_enabled: bool | None = None
  35. plate_detection_roi: PlateDetectionROI | None = None
  36. class PrinterResponse(PrinterBase):
  37. id: int
  38. is_active: bool
  39. nozzle_count: int = 1 # 1 or 2, auto-detected from MQTT
  40. print_hours_offset: float = 0.0
  41. external_camera_url: str | None = None
  42. external_camera_type: str | None = None
  43. external_camera_enabled: bool = False
  44. plate_detection_enabled: bool = False
  45. plate_detection_roi: PlateDetectionROI | None = None
  46. created_at: datetime
  47. updated_at: datetime
  48. class Config:
  49. from_attributes = True
  50. @classmethod
  51. def from_orm_with_roi(cls, printer) -> "PrinterResponse":
  52. """Create response from ORM model, converting ROI fields to nested object."""
  53. data = {
  54. "id": printer.id,
  55. "name": printer.name,
  56. "serial_number": printer.serial_number,
  57. "ip_address": printer.ip_address,
  58. "access_code": printer.access_code,
  59. "model": printer.model,
  60. "location": printer.location,
  61. "auto_archive": printer.auto_archive,
  62. "external_camera_url": printer.external_camera_url,
  63. "external_camera_type": printer.external_camera_type,
  64. "external_camera_enabled": printer.external_camera_enabled,
  65. "is_active": printer.is_active,
  66. "nozzle_count": printer.nozzle_count,
  67. "print_hours_offset": printer.print_hours_offset,
  68. "plate_detection_enabled": printer.plate_detection_enabled,
  69. "created_at": printer.created_at,
  70. "updated_at": printer.updated_at,
  71. }
  72. # Build ROI object if any ROI field is set
  73. if any(
  74. [
  75. printer.plate_detection_roi_x is not None,
  76. printer.plate_detection_roi_y is not None,
  77. printer.plate_detection_roi_w is not None,
  78. printer.plate_detection_roi_h is not None,
  79. ]
  80. ):
  81. data["plate_detection_roi"] = PlateDetectionROI(
  82. x=printer.plate_detection_roi_x or 0.15,
  83. y=printer.plate_detection_roi_y or 0.35,
  84. w=printer.plate_detection_roi_w or 0.70,
  85. h=printer.plate_detection_roi_h or 0.55,
  86. )
  87. return cls(**data)
  88. class HMSErrorResponse(BaseModel):
  89. code: str
  90. attr: int = 0 # Attribute value for constructing wiki URL
  91. module: int
  92. severity: int # 1=fatal, 2=serious, 3=common, 4=info
  93. class AMSTray(BaseModel):
  94. id: int
  95. tray_color: str | None = None
  96. tray_type: str | None = None
  97. tray_sub_brands: str | None = None # Full name like "PLA Basic", "PETG HF"
  98. tray_id_name: str | None = None # Bambu filament ID like "A00-Y2" (can decode to color)
  99. tray_info_idx: str | None = None # Filament preset ID like "GFA00"
  100. remain: int = 0
  101. k: float | None = None # Pressure advance value (from tray or K-profile lookup)
  102. cali_idx: int | None = None # Calibration index for K-profile lookup
  103. tag_uid: str | None = None # RFID tag UID (any tag)
  104. tray_uuid: str | None = None # Bambu Lab spool UUID (32-char hex)
  105. nozzle_temp_min: int | None = None # Min nozzle temperature
  106. nozzle_temp_max: int | None = None # Max nozzle temperature
  107. class AMSUnit(BaseModel):
  108. id: int
  109. humidity: int | None = None
  110. temp: float | None = None
  111. is_ams_ht: bool = False # True for AMS-HT (single spool), False for regular AMS (4 spools)
  112. tray: list[AMSTray] = []
  113. class NozzleInfoResponse(BaseModel):
  114. nozzle_type: str = "" # "stainless_steel" or "hardened_steel"
  115. nozzle_diameter: str = "" # e.g., "0.4"
  116. class PrintOptionsResponse(BaseModel):
  117. """AI detection and print options from xcam data."""
  118. # Core AI detectors
  119. spaghetti_detector: bool = False
  120. print_halt: bool = False
  121. halt_print_sensitivity: str = "medium" # Spaghetti sensitivity
  122. first_layer_inspector: bool = False
  123. printing_monitor: bool = False
  124. buildplate_marker_detector: bool = False
  125. allow_skip_parts: bool = False
  126. # Additional AI detectors (decoded from cfg bitmask)
  127. nozzle_clumping_detector: bool = True
  128. nozzle_clumping_sensitivity: str = "medium"
  129. pileup_detector: bool = True
  130. pileup_sensitivity: str = "medium"
  131. airprint_detector: bool = True
  132. airprint_sensitivity: str = "medium"
  133. auto_recovery_step_loss: bool = True
  134. filament_tangle_detect: bool = False
  135. class PrinterStatus(BaseModel):
  136. id: int
  137. name: str
  138. connected: bool
  139. state: str | None = None
  140. current_print: str | None = None
  141. subtask_name: str | None = None
  142. gcode_file: str | None = None
  143. progress: float | None = None
  144. remaining_time: int | None = None
  145. layer_num: int | None = None
  146. total_layers: int | None = None
  147. temperatures: dict | None = None
  148. cover_url: str | None = None
  149. hms_errors: list[HMSErrorResponse] = []
  150. ams: list[AMSUnit] = []
  151. ams_exists: bool = False
  152. vt_tray: AMSTray | None = None # Virtual tray / external spool
  153. sdcard: bool = False # SD card inserted
  154. store_to_sdcard: bool = False # Store sent files on SD card
  155. timelapse: bool = False # Timelapse recording active
  156. ipcam: bool = False # Live view enabled
  157. wifi_signal: int | None = None # WiFi signal strength in dBm
  158. nozzles: list[NozzleInfoResponse] = [] # Nozzle hardware info (index 0=left/primary, 1=right)
  159. print_options: PrintOptionsResponse | None = None # AI detection and print options
  160. # Calibration stage tracking
  161. stg_cur: int = -1 # Current stage number (-1 = not calibrating)
  162. stg_cur_name: str | None = None # Human-readable current stage name
  163. stg: list[int] = [] # List of stage numbers in calibration sequence
  164. # Air conditioning mode (0=cooling, 1=heating)
  165. airduct_mode: int = 0
  166. # Print speed level (1=silent, 2=standard, 3=sport, 4=ludicrous)
  167. speed_level: int = 2
  168. # Chamber light on/off
  169. chamber_light: bool = False
  170. # Active extruder for dual nozzle (0=right, 1=left)
  171. active_extruder: int = 0
  172. # AMS mapping for dual nozzle: which AMS is connected to which nozzle
  173. ams_mapping: list[int] = []
  174. # Per-AMS extruder map: {ams_id: extruder_id} where 0=right, 1=left
  175. ams_extruder_map: dict[str, int] = {}
  176. # Currently loaded tray (global ID): 254 = external spool, 255 = no filament
  177. tray_now: int = 255
  178. # AMS status for filament change tracking
  179. # Main status: 0=idle, 1=filament_change, 2=rfid_identifying, 3=assist, 4=calibration
  180. ams_status_main: int = 0
  181. # Sub status: specific step within filament change (when main=1)
  182. # Known values: 4=retraction, 6=load verification, 7=purge
  183. ams_status_sub: int = 0
  184. # mc_print_sub_stage - filament change step indicator used by OrcaSlicer/BambuStudio
  185. mc_print_sub_stage: int = 0
  186. # Timestamp of last AMS data update (for RFID refresh detection)
  187. last_ams_update: float = 0.0
  188. # Number of printable objects in current print (for skip objects feature)
  189. printable_objects_count: int = 0
  190. # Fan speeds (0-100 percentage, None if not available for this model)
  191. cooling_fan_speed: int | None = None # Part cooling fan
  192. big_fan1_speed: int | None = None # Auxiliary fan
  193. big_fan2_speed: int | None = None # Chamber/exhaust fan
  194. heatbreak_fan_speed: int | None = None # Hotend heatbreak fan
  195. # Firmware version (from info.module[name="ota"].sw_ver)
  196. firmware_version: str | None = None