firmware.py 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298
  1. """
  2. Firmware Update API Routes
  3. Check for firmware updates from Bambu Lab.
  4. Also provides endpoints for uploading firmware to printers via SD card.
  5. """
  6. import logging
  7. from fastapi import APIRouter, Depends, HTTPException
  8. from pydantic import BaseModel, Field
  9. from sqlalchemy import select
  10. from sqlalchemy.ext.asyncio import AsyncSession
  11. from backend.app.core.database import get_db
  12. from backend.app.models.printer import Printer
  13. from backend.app.services.firmware_check import get_firmware_service
  14. from backend.app.services.firmware_update import (
  15. FirmwareUploadStatus,
  16. get_firmware_update_service,
  17. get_upload_state,
  18. )
  19. from backend.app.services.printer_manager import printer_manager
  20. logger = logging.getLogger(__name__)
  21. router = APIRouter(prefix="/firmware", tags=["firmware"])
  22. class FirmwareUpdateInfo(BaseModel):
  23. """Firmware update information for a printer."""
  24. printer_id: int
  25. printer_name: str
  26. model: str | None
  27. current_version: str | None
  28. latest_version: str | None
  29. update_available: bool
  30. download_url: str | None = None
  31. release_notes: str | None = None
  32. class FirmwareUpdatesResponse(BaseModel):
  33. """Response containing firmware updates for all printers."""
  34. updates: list[FirmwareUpdateInfo] = Field(default_factory=list)
  35. updates_available: int = Field(0, description="Number of printers with updates available")
  36. class LatestFirmwareInfo(BaseModel):
  37. """Latest firmware version info for a model."""
  38. model_key: str
  39. version: str
  40. download_url: str
  41. release_notes: str | None = None
  42. @router.get("/updates", response_model=FirmwareUpdatesResponse)
  43. async def check_firmware_updates(
  44. db: AsyncSession = Depends(get_db),
  45. ):
  46. """
  47. Check for firmware updates for all connected printers.
  48. Compares each printer's current firmware version against the latest
  49. available version from Bambu Lab's official firmware download page.
  50. Note: This does not require cloud authentication - it uses public
  51. firmware information from bambulab.com.
  52. """
  53. firmware_service = get_firmware_service()
  54. # Get all printers from database
  55. result = await db.execute(select(Printer).where(Printer.is_active.is_(True)))
  56. printers = result.scalars().all()
  57. updates = []
  58. updates_available = 0
  59. for printer in printers:
  60. # Get current firmware version from MQTT state
  61. current_version = None
  62. mqtt_client = printer_manager.get_client(printer.id)
  63. if mqtt_client and mqtt_client.state:
  64. current_version = mqtt_client.state.firmware_version
  65. # Check for update
  66. model = printer.model or "Unknown"
  67. update_info = await firmware_service.check_for_update(model, current_version or "")
  68. if update_info["update_available"]:
  69. updates_available += 1
  70. updates.append(
  71. FirmwareUpdateInfo(
  72. printer_id=printer.id,
  73. printer_name=printer.name,
  74. model=model,
  75. current_version=current_version,
  76. latest_version=update_info["latest_version"],
  77. update_available=update_info["update_available"],
  78. download_url=update_info["download_url"],
  79. release_notes=update_info["release_notes"],
  80. )
  81. )
  82. return FirmwareUpdatesResponse(updates=updates, updates_available=updates_available)
  83. @router.get("/updates/{printer_id}", response_model=FirmwareUpdateInfo)
  84. async def check_printer_firmware(
  85. printer_id: int,
  86. db: AsyncSession = Depends(get_db),
  87. ):
  88. """
  89. Check for firmware update for a specific printer.
  90. """
  91. firmware_service = get_firmware_service()
  92. # Get printer from database
  93. result = await db.execute(select(Printer).where(Printer.id == printer_id))
  94. printer = result.scalar_one_or_none()
  95. if not printer:
  96. raise HTTPException(status_code=404, detail="Printer not found")
  97. # Get current firmware version from MQTT state
  98. current_version = None
  99. mqtt_client = printer_manager.get_client(printer.id)
  100. if mqtt_client and mqtt_client.state:
  101. current_version = mqtt_client.state.firmware_version
  102. # Check for update
  103. model = printer.model or "Unknown"
  104. update_info = await firmware_service.check_for_update(model, current_version or "")
  105. return FirmwareUpdateInfo(
  106. printer_id=printer.id,
  107. printer_name=printer.name,
  108. model=model,
  109. current_version=current_version,
  110. latest_version=update_info["latest_version"],
  111. update_available=update_info["update_available"],
  112. download_url=update_info["download_url"],
  113. release_notes=update_info["release_notes"],
  114. )
  115. @router.get("/latest", response_model=list[LatestFirmwareInfo])
  116. async def get_all_latest_firmware():
  117. """
  118. Get the latest firmware versions for all Bambu Lab printer models.
  119. This endpoint fetches the latest available firmware versions from
  120. Bambu Lab's official firmware download page.
  121. """
  122. firmware_service = get_firmware_service()
  123. versions = await firmware_service.get_all_latest_versions()
  124. return [
  125. LatestFirmwareInfo(
  126. model_key=key,
  127. version=info.version,
  128. download_url=info.download_url,
  129. release_notes=info.release_notes,
  130. )
  131. for key, info in versions.items()
  132. ]
  133. # ============================================================================
  134. # Firmware Upload Endpoints (for LAN-only firmware updates)
  135. # ============================================================================
  136. class FirmwareUploadPrepareResponse(BaseModel):
  137. """Response from firmware upload preparation check."""
  138. can_proceed: bool
  139. sd_card_present: bool
  140. sd_card_free_space: int = Field(-1, description="Free space in bytes, -1 if unknown")
  141. firmware_size: int = Field(0, description="Estimated firmware size in bytes")
  142. space_sufficient: bool
  143. update_available: bool
  144. current_version: str | None = None
  145. latest_version: str | None = None
  146. firmware_filename: str | None = None
  147. errors: list[str] = Field(default_factory=list)
  148. class FirmwareUploadStatusResponse(BaseModel):
  149. """Response containing firmware upload status."""
  150. status: str # idle, preparing, downloading, uploading, complete, error
  151. progress: int = Field(0, ge=0, le=100)
  152. message: str = ""
  153. error: str | None = None
  154. firmware_filename: str | None = None
  155. firmware_version: str | None = None
  156. class FirmwareUploadStartResponse(BaseModel):
  157. """Response when starting a firmware upload."""
  158. started: bool
  159. message: str
  160. @router.get("/updates/{printer_id}/prepare", response_model=FirmwareUploadPrepareResponse)
  161. async def prepare_firmware_upload(
  162. printer_id: int,
  163. db: AsyncSession = Depends(get_db),
  164. ):
  165. """
  166. Check prerequisites for uploading firmware to a printer.
  167. This performs pre-flight checks including:
  168. - SD card presence
  169. - Available storage space
  170. - Update availability
  171. Call this before starting a firmware upload to ensure the operation
  172. can succeed.
  173. """
  174. update_service = get_firmware_update_service()
  175. result = await update_service.prepare_update(printer_id, db)
  176. return FirmwareUploadPrepareResponse(**result)
  177. @router.post("/updates/{printer_id}/upload", response_model=FirmwareUploadStartResponse)
  178. async def start_firmware_upload(
  179. printer_id: int,
  180. db: AsyncSession = Depends(get_db),
  181. ):
  182. """
  183. Start uploading firmware to a printer's SD card.
  184. This initiates a background process that:
  185. 1. Downloads the firmware from Bambu Lab
  186. 2. Uploads it to the printer's SD card via FTP
  187. Progress is broadcast via WebSocket with type "firmware_upload_progress".
  188. Use GET /firmware/updates/{printer_id}/upload/status for polling fallback.
  189. After upload completes, the user must trigger the update from the
  190. printer's screen (Settings > Firmware).
  191. """
  192. # First check prerequisites
  193. update_service = get_firmware_update_service()
  194. prepare_result = await update_service.prepare_update(printer_id, db)
  195. if not prepare_result["can_proceed"]:
  196. errors = prepare_result.get("errors", ["Cannot proceed with firmware upload"])
  197. raise HTTPException(
  198. status_code=400,
  199. detail="; ".join(errors),
  200. )
  201. # Start the upload
  202. started = await update_service.start_upload(printer_id, db)
  203. if not started:
  204. state = get_upload_state(printer_id)
  205. if state.status == FirmwareUploadStatus.DOWNLOADING:
  206. return FirmwareUploadStartResponse(
  207. started=False,
  208. message="Firmware upload already in progress",
  209. )
  210. raise HTTPException(
  211. status_code=500,
  212. detail=state.error or "Failed to start firmware upload",
  213. )
  214. return FirmwareUploadStartResponse(
  215. started=True,
  216. message="Firmware upload started. Progress will be broadcast via WebSocket.",
  217. )
  218. @router.get("/updates/{printer_id}/upload/status", response_model=FirmwareUploadStatusResponse)
  219. async def get_firmware_upload_status(printer_id: int):
  220. """
  221. Get the current status of a firmware upload operation.
  222. This is a polling fallback for clients that don't use WebSocket.
  223. For real-time updates, connect to WebSocket and listen for
  224. "firmware_upload_progress" messages.
  225. """
  226. state = get_upload_state(printer_id)
  227. return FirmwareUploadStatusResponse(
  228. status=state.status.value,
  229. progress=state.progress,
  230. message=state.message,
  231. error=state.error,
  232. firmware_filename=state.firmware_filename,
  233. firmware_version=state.firmware_version,
  234. )