spoolbuddy.py 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  1. import json
  2. from datetime import datetime
  3. from typing import Literal
  4. from pydantic import BaseModel, Field, field_validator
  5. # --- Device schemas ---
  6. class DeviceRegisterRequest(BaseModel):
  7. device_id: str = Field(..., min_length=1, max_length=50)
  8. hostname: str = Field(..., min_length=1, max_length=100)
  9. ip_address: str = Field(..., min_length=1, max_length=45)
  10. firmware_version: str | None = Field(None, max_length=20)
  11. has_nfc: bool = True
  12. has_scale: bool = True
  13. tare_offset: int = 0
  14. calibration_factor: float = 1.0
  15. nfc_reader_type: str | None = Field(None, max_length=20)
  16. nfc_connection: str | None = Field(None, max_length=20)
  17. backend_url: str | None = Field(None, max_length=255)
  18. has_backlight: bool = False
  19. class DeviceResponse(BaseModel):
  20. id: int
  21. device_id: str
  22. hostname: str
  23. ip_address: str
  24. firmware_version: str | None = None
  25. has_nfc: bool
  26. has_scale: bool
  27. tare_offset: int
  28. calibration_factor: float
  29. nfc_reader_type: str | None = None
  30. nfc_connection: str | None = None
  31. backend_url: str | None = None
  32. display_brightness: int = 100
  33. display_blank_timeout: int = 0
  34. has_backlight: bool = False
  35. last_calibrated_at: datetime | None = None
  36. last_seen: datetime | None = None
  37. pending_command: str | None = None
  38. nfc_ok: bool
  39. scale_ok: bool
  40. uptime_s: int
  41. update_status: str | None = None
  42. update_message: str | None = None
  43. system_stats: dict | None = None
  44. online: bool = False
  45. ssh_public_key: str | None = None
  46. created_at: datetime
  47. updated_at: datetime
  48. class Config:
  49. from_attributes = True
  50. class HeartbeatRequest(BaseModel):
  51. nfc_ok: bool = False
  52. scale_ok: bool = False
  53. uptime_s: int = 0
  54. firmware_version: str | None = Field(None, max_length=20)
  55. ip_address: str | None = Field(None, max_length=45)
  56. nfc_reader_type: str | None = Field(None, max_length=20)
  57. nfc_connection: str | None = Field(None, max_length=20)
  58. backend_url: str | None = Field(None, max_length=255)
  59. system_stats: dict | None = None
  60. @field_validator("system_stats")
  61. @classmethod
  62. def _limit_system_stats_size(cls, v: dict | None) -> dict | None:
  63. if v is not None and len(json.dumps(v)) > 4096:
  64. raise ValueError("system_stats must not exceed 4096 bytes when JSON-encoded")
  65. return v
  66. class HeartbeatResponse(BaseModel):
  67. pending_command: str | None = None
  68. pending_write_payload: dict | None = None
  69. pending_system_payload: dict | None = None
  70. tare_offset: int
  71. calibration_factor: float
  72. display_brightness: int = 100
  73. display_blank_timeout: int = 0
  74. ssh_public_key: str | None = None
  75. # --- NFC schemas ---
  76. class TagScannedRequest(BaseModel):
  77. device_id: str = Field(..., max_length=50)
  78. tag_uid: str = Field(..., max_length=32)
  79. tray_uuid: str | None = Field(None, max_length=32, pattern=r"^[0-9A-Fa-f]*$")
  80. sak: int | None = None
  81. tag_type: str | None = Field(None, max_length=50)
  82. raw_blocks: dict | None = None
  83. class TagRemovedRequest(BaseModel):
  84. device_id: str = Field(..., max_length=50)
  85. tag_uid: str = Field(..., max_length=32)
  86. # --- Scale schemas ---
  87. class ScaleReadingRequest(BaseModel):
  88. device_id: str = Field(..., max_length=50)
  89. weight_grams: float = Field(..., allow_inf_nan=False)
  90. stable: bool = False
  91. raw_adc: int | None = None
  92. class UpdateSpoolWeightRequest(BaseModel):
  93. spool_id: int = Field(..., gt=0)
  94. weight_grams: float = Field(..., allow_inf_nan=False, ge=0.0, le=100_000.0)
  95. # --- Calibration schemas ---
  96. class SetTareRequest(BaseModel):
  97. tare_offset: int
  98. class SetCalibrationFactorRequest(BaseModel):
  99. known_weight_grams: float = Field(..., gt=0)
  100. raw_adc: int
  101. tare_raw_adc: int | None = None
  102. class CalibrationResponse(BaseModel):
  103. tare_offset: int
  104. calibration_factor: float
  105. # --- Display schemas ---
  106. class WriteTagRequest(BaseModel):
  107. device_id: str = Field(..., max_length=50)
  108. spool_id: int = Field(..., gt=0)
  109. class WriteTagResultRequest(BaseModel):
  110. device_id: str = Field(..., max_length=50)
  111. spool_id: int = Field(..., gt=0)
  112. tag_uid: str = Field(..., min_length=8, max_length=30, pattern=r"^[0-9A-Fa-f]+$")
  113. success: bool
  114. message: str | None = Field(None, max_length=500)
  115. class DisplaySettingsRequest(BaseModel):
  116. brightness: int = Field(ge=0, le=100)
  117. blank_timeout: int = Field(ge=0)
  118. class SystemConfigRequest(BaseModel):
  119. backend_url: str = Field(..., min_length=1, max_length=255)
  120. api_key: str | None = Field(default=None, max_length=255)
  121. class SystemCommandRequest(BaseModel):
  122. command: str = Field(
  123. ..., max_length=50, description="System command: reboot, shutdown, restart_daemon, restart_browser"
  124. )
  125. class SystemCommandResultRequest(BaseModel):
  126. command: str = Field(..., max_length=50)
  127. success: bool
  128. message: str | None = Field(None, max_length=500)
  129. class UpdateStatusRequest(BaseModel):
  130. status: Literal["updating", "complete", "error"]
  131. message: str | None = Field(None, max_length=255)
  132. # --- Diagnostics schemas ---
  133. class DiagnosticResultRequest(BaseModel):
  134. diagnostic: str = Field(..., max_length=50, description="Diagnostic type: 'nfc', 'scale', or 'read_tag'")
  135. success: bool
  136. output: str = Field(..., max_length=10_000)
  137. exit_code: int = Field(..., ge=-255, le=255)