kprofiles.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382
  1. """API routes for K-profile (pressure advance) management."""
  2. import asyncio
  3. import logging
  4. from fastapi import APIRouter, Depends, HTTPException
  5. from sqlalchemy import select
  6. from sqlalchemy.ext.asyncio import AsyncSession
  7. from backend.app.core.database import get_db
  8. from backend.app.models.kprofile_note import KProfileNote as KProfileNoteModel
  9. from backend.app.models.printer import Printer
  10. from backend.app.schemas.kprofile import (
  11. KProfile,
  12. KProfileCreate,
  13. KProfileDelete,
  14. KProfileNote,
  15. KProfileNoteResponse,
  16. KProfilesResponse,
  17. )
  18. from backend.app.services.printer_manager import printer_manager
  19. logger = logging.getLogger(__name__)
  20. router = APIRouter(prefix="/printers/{printer_id}/kprofiles", tags=["kprofiles"])
  21. @router.get("/", response_model=KProfilesResponse)
  22. async def get_kprofiles(
  23. printer_id: int,
  24. nozzle_diameter: str = "0.4",
  25. db: AsyncSession = Depends(get_db),
  26. ):
  27. """Get K-profiles from a printer.
  28. Args:
  29. printer_id: ID of the printer
  30. nozzle_diameter: Filter by nozzle diameter (default: "0.4")
  31. """
  32. # Check printer exists
  33. result = await db.execute(select(Printer).where(Printer.id == printer_id))
  34. printer = result.scalar_one_or_none()
  35. if not printer:
  36. raise HTTPException(404, "Printer not found")
  37. # Get MQTT client for printer
  38. client = printer_manager.get_client(printer_id)
  39. if not client or not client.state.connected:
  40. raise HTTPException(400, "Printer not connected")
  41. # Request K-profiles from printer
  42. profiles = await client.get_kprofiles(nozzle_diameter=nozzle_diameter)
  43. # Convert from MQTT dataclass to Pydantic schema
  44. return KProfilesResponse(
  45. profiles=[
  46. KProfile(
  47. slot_id=p.slot_id,
  48. extruder_id=p.extruder_id,
  49. nozzle_id=p.nozzle_id,
  50. nozzle_diameter=p.nozzle_diameter,
  51. filament_id=p.filament_id,
  52. name=p.name,
  53. k_value=p.k_value,
  54. n_coef=p.n_coef,
  55. ams_id=p.ams_id,
  56. tray_id=p.tray_id,
  57. setting_id=p.setting_id,
  58. )
  59. for p in profiles
  60. ],
  61. nozzle_diameter=nozzle_diameter,
  62. )
  63. @router.post("/", response_model=dict)
  64. async def set_kprofile(
  65. printer_id: int,
  66. profile: KProfileCreate,
  67. db: AsyncSession = Depends(get_db),
  68. ):
  69. """Create or update a K-profile on the printer.
  70. For H2D edits (slot_id > 0), this performs an in-place edit using cali_idx.
  71. For other printers or new profiles, this adds a new profile.
  72. Args:
  73. printer_id: ID of the printer
  74. profile: K-profile data to set
  75. """
  76. is_edit = profile.slot_id > 0
  77. operation = "edit" if is_edit else "add"
  78. logger.info(
  79. f"[API] set_kprofile ({operation}): printer={printer_id}, slot_id={profile.slot_id}, "
  80. f"extruder_id={profile.extruder_id}, nozzle_id={profile.nozzle_id}, "
  81. f"name={profile.name}, filament_id={profile.filament_id}, k_value={profile.k_value}"
  82. )
  83. # Check printer exists
  84. result = await db.execute(select(Printer).where(Printer.id == printer_id))
  85. printer = result.scalar_one_or_none()
  86. if not printer:
  87. raise HTTPException(404, "Printer not found")
  88. # Get MQTT client for printer
  89. client = printer_manager.get_client(printer_id)
  90. if not client or not client.state.connected:
  91. raise HTTPException(400, "Printer not connected")
  92. # Detect H2D by serial number prefix
  93. is_h2d = printer.serial_number.startswith("094")
  94. if is_edit and is_h2d:
  95. # H2D in-place edit: use cali_idx with slot_id=0 and empty setting_id
  96. logger.info(f"[API] H2D in-place edit: cali_idx={profile.slot_id}")
  97. success = client.set_kprofile(
  98. filament_id=profile.filament_id,
  99. name=profile.name,
  100. k_value=profile.k_value,
  101. nozzle_diameter=profile.nozzle_diameter,
  102. nozzle_id=profile.nozzle_id,
  103. extruder_id=profile.extruder_id,
  104. setting_id=None,
  105. slot_id=0,
  106. cali_idx=profile.slot_id, # Pass the original slot for in-place edit
  107. )
  108. elif is_edit:
  109. # Non-H2D edit: use delete + add approach
  110. logger.info(f"[API] Edit: deleting existing profile slot_id={profile.slot_id}")
  111. delete_success = client.delete_kprofile(
  112. cali_idx=profile.slot_id,
  113. filament_id=profile.filament_id,
  114. nozzle_id=profile.nozzle_id,
  115. nozzle_diameter=profile.nozzle_diameter,
  116. extruder_id=profile.extruder_id,
  117. setting_id=profile.setting_id,
  118. )
  119. if not delete_success:
  120. raise HTTPException(500, "Failed to delete existing K-profile for edit")
  121. # Wait for printer to process the delete before adding
  122. await asyncio.sleep(0.5)
  123. logger.info("[API] Edit: delete complete, now adding updated profile")
  124. success = client.set_kprofile(
  125. filament_id=profile.filament_id,
  126. name=profile.name,
  127. k_value=profile.k_value,
  128. nozzle_diameter=profile.nozzle_diameter,
  129. nozzle_id=profile.nozzle_id,
  130. extruder_id=profile.extruder_id,
  131. setting_id=None, # Generate new setting_id for add
  132. slot_id=0, # Always 0 for add (new profile)
  133. )
  134. else:
  135. # New profile: add with slot_id=0
  136. success = client.set_kprofile(
  137. filament_id=profile.filament_id,
  138. name=profile.name,
  139. k_value=profile.k_value,
  140. nozzle_diameter=profile.nozzle_diameter,
  141. nozzle_id=profile.nozzle_id,
  142. extruder_id=profile.extruder_id,
  143. setting_id=None, # Generate new setting_id for add
  144. slot_id=0, # Always 0 for add (new profile)
  145. )
  146. if not success:
  147. raise HTTPException(500, "Failed to send K-profile command")
  148. message = "K-profile updated successfully" if is_edit else "K-profile added successfully"
  149. return {"success": True, "message": message}
  150. @router.post("/batch", response_model=dict)
  151. async def set_kprofiles_batch(
  152. printer_id: int,
  153. profiles: list[KProfileCreate],
  154. db: AsyncSession = Depends(get_db),
  155. ):
  156. """Create multiple K-profiles in a single command (for dual-nozzle).
  157. This sends all profiles in one MQTT command, which is more reliable
  158. for dual-nozzle printers that may not handle sequential commands well.
  159. Args:
  160. printer_id: ID of the printer
  161. profiles: List of K-profiles to set
  162. """
  163. if not profiles:
  164. raise HTTPException(400, "No profiles provided")
  165. logger.info(f"[API] set_kprofiles_batch: printer={printer_id}, {len(profiles)} profiles")
  166. for p in profiles:
  167. logger.info(f" - extruder_id={p.extruder_id}, name={p.name}, k_value={p.k_value}")
  168. # Check printer exists
  169. result = await db.execute(select(Printer).where(Printer.id == printer_id))
  170. printer = result.scalar_one_or_none()
  171. if not printer:
  172. raise HTTPException(404, "Printer not found")
  173. # Get MQTT client for printer
  174. client = printer_manager.get_client(printer_id)
  175. if not client or not client.state.connected:
  176. raise HTTPException(400, "Printer not connected")
  177. # Build list of profile dicts for batch command
  178. profile_dicts = [
  179. {
  180. "filament_id": p.filament_id,
  181. "name": p.name,
  182. "k_value": p.k_value,
  183. "nozzle_id": p.nozzle_id,
  184. "extruder_id": p.extruder_id,
  185. "setting_id": p.setting_id,
  186. "slot_id": p.slot_id,
  187. }
  188. for p in profiles
  189. ]
  190. # Get nozzle_diameter from first profile (all should have same)
  191. nozzle_diameter = profiles[0].nozzle_diameter
  192. success = client.set_kprofiles_batch(profile_dicts, nozzle_diameter)
  193. if not success:
  194. raise HTTPException(500, "Failed to send K-profiles batch command")
  195. return {"success": True, "message": f"Added {len(profiles)} K-profiles"}
  196. @router.delete("/", response_model=dict)
  197. async def delete_kprofile(
  198. printer_id: int,
  199. profile: KProfileDelete,
  200. db: AsyncSession = Depends(get_db),
  201. ):
  202. """Delete a K-profile from the printer.
  203. Args:
  204. printer_id: ID of the printer
  205. profile: K-profile identification data for deletion
  206. """
  207. # Check printer exists
  208. result = await db.execute(select(Printer).where(Printer.id == printer_id))
  209. printer = result.scalar_one_or_none()
  210. if not printer:
  211. raise HTTPException(404, "Printer not found")
  212. # Get MQTT client for printer
  213. client = printer_manager.get_client(printer_id)
  214. if not client or not client.state.connected:
  215. raise HTTPException(400, "Printer not connected")
  216. # Send the delete command to printer
  217. logger.info(
  218. f"[API] delete_kprofile: printer={printer_id}, slot_id={profile.slot_id}, "
  219. f"setting_id={profile.setting_id}, filament_id={profile.filament_id}"
  220. )
  221. success = client.delete_kprofile(
  222. cali_idx=profile.slot_id,
  223. filament_id=profile.filament_id,
  224. nozzle_id=profile.nozzle_id,
  225. nozzle_diameter=profile.nozzle_diameter,
  226. extruder_id=profile.extruder_id,
  227. setting_id=profile.setting_id,
  228. )
  229. if not success:
  230. raise HTTPException(500, "Failed to send K-profile delete command")
  231. return {"success": True, "message": "K-profile deleted successfully"}
  232. @router.get("/notes", response_model=KProfileNoteResponse)
  233. async def get_kprofile_notes(
  234. printer_id: int,
  235. db: AsyncSession = Depends(get_db),
  236. ):
  237. """Get all K-profile notes for a printer.
  238. Notes are stored locally since printers don't support notes.
  239. Args:
  240. printer_id: ID of the printer
  241. """
  242. # Check printer exists
  243. result = await db.execute(select(Printer).where(Printer.id == printer_id))
  244. printer = result.scalar_one_or_none()
  245. if not printer:
  246. raise HTTPException(404, "Printer not found")
  247. # Get all notes for this printer
  248. result = await db.execute(select(KProfileNoteModel).where(KProfileNoteModel.printer_id == printer_id))
  249. notes = result.scalars().all()
  250. # Return as a dictionary mapping setting_id -> note
  251. return KProfileNoteResponse(notes={note.setting_id: note.note for note in notes})
  252. @router.put("/notes", response_model=dict)
  253. async def set_kprofile_note(
  254. printer_id: int,
  255. note_data: KProfileNote,
  256. db: AsyncSession = Depends(get_db),
  257. ):
  258. """Set or update a note for a K-profile.
  259. Args:
  260. printer_id: ID of the printer
  261. note_data: The note data (setting_id and note content)
  262. """
  263. # Check printer exists
  264. result = await db.execute(select(Printer).where(Printer.id == printer_id))
  265. printer = result.scalar_one_or_none()
  266. if not printer:
  267. raise HTTPException(404, "Printer not found")
  268. # Find existing note or create new one
  269. result = await db.execute(
  270. select(KProfileNoteModel).where(
  271. KProfileNoteModel.printer_id == printer_id,
  272. KProfileNoteModel.setting_id == note_data.setting_id,
  273. )
  274. )
  275. existing_note = result.scalar_one_or_none()
  276. if note_data.note.strip():
  277. # Save or update note
  278. if existing_note:
  279. existing_note.note = note_data.note
  280. else:
  281. new_note = KProfileNoteModel(
  282. printer_id=printer_id,
  283. setting_id=note_data.setting_id,
  284. note=note_data.note,
  285. )
  286. db.add(new_note)
  287. await db.commit()
  288. return {"success": True, "message": "Note saved"}
  289. else:
  290. # Delete note if empty
  291. if existing_note:
  292. await db.delete(existing_note)
  293. await db.commit()
  294. return {"success": True, "message": "Note deleted"}
  295. @router.delete("/notes/{setting_id}", response_model=dict)
  296. async def delete_kprofile_note(
  297. printer_id: int,
  298. setting_id: str,
  299. db: AsyncSession = Depends(get_db),
  300. ):
  301. """Delete a note for a K-profile.
  302. Args:
  303. printer_id: ID of the printer
  304. setting_id: The setting_id of the K-profile
  305. """
  306. # Check printer exists
  307. result = await db.execute(select(Printer).where(Printer.id == printer_id))
  308. printer = result.scalar_one_or_none()
  309. if not printer:
  310. raise HTTPException(404, "Printer not found")
  311. # Find and delete the note
  312. result = await db.execute(
  313. select(KProfileNoteModel).where(
  314. KProfileNoteModel.printer_id == printer_id,
  315. KProfileNoteModel.setting_id == setting_id,
  316. )
  317. )
  318. existing_note = result.scalar_one_or_none()
  319. if existing_note:
  320. await db.delete(existing_note)
  321. await db.commit()
  322. return {"success": True, "message": "Note deleted"}