slot_kprofile.py 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  1. """Find the stored K-profile for an AMS slot on a particular nozzle.
  2. K-profiles are per-nozzle: Bambuddy already keeps one row per
  3. ``(spool, printer, extruder)`` in ``spool_k_profile`` (and the Spoolman mirror
  4. in ``spoolman_k_profile``), each with its own ``cali_idx`` and K value. On the
  5. maintainer's H2C, one black PLA reads 0.018 on the left hotend and 0.020 on the
  6. right, stored as calibration indices 16 and 15.
  7. A tray, by contrast, holds exactly **one** ``cali_idx``. So whenever a slot's
  8. nozzle changes — which on a Filament Track Switch machine happens every time an
  9. AMS is moved between the switch's two inlets — the stored counterpart for the
  10. new nozzle has to be looked up and re-selected. This module is that lookup.
  11. """
  12. from dataclasses import dataclass
  13. from sqlalchemy import select
  14. from sqlalchemy.ext.asyncio import AsyncSession
  15. from backend.app.models.spool import Spool
  16. from backend.app.models.spool_assignment import SpoolAssignment
  17. from backend.app.models.spool_k_profile import SpoolKProfile
  18. from backend.app.models.spoolman_k_profile import SpoolmanKProfile
  19. from backend.app.models.spoolman_slot_assignment import SpoolmanSlotAssignment
  20. from backend.app.services.inventory_mode import spoolman_owns_assignments
  21. from backend.app.services.spool_filament_preset import resolve_spool_preset, resolve_spoolman_preset_with_own
  22. from backend.app.utils.filament_ids import normalize_slicer_filament
  23. @dataclass(frozen=True)
  24. class SlotKProfile:
  25. """A stored calibration profile, flattened across the two inventory backends."""
  26. cali_idx: int | None
  27. k_value: float | None
  28. name: str | None
  29. extruder: int
  30. # The preset the profile was calibrated under. ``extrusion_cali_sel`` must
  31. # carry this rather than the tray's RFID value, or the firmware mislinks it.
  32. filament_id: str | None
  33. def _flow_applies(stored_flow: str | None, fitted_flow: str | None) -> bool:
  34. """``SlotNozzle.flow_matches`` for callers that hold only the two strings."""
  35. from backend.app.services.slot_nozzle import SlotNozzle
  36. return SlotNozzle(extruder=None, diameter="", flow=fitted_flow).flow_matches(stored_flow)
  37. async def find_slot_kprofile_for_extruder(
  38. db: AsyncSession,
  39. printer_id: int,
  40. ams_id: int,
  41. tray_id: int,
  42. extruder: int,
  43. nozzle_diameter: str,
  44. printer_model: str | None = None,
  45. flow: str | None = None,
  46. ) -> SlotKProfile | None:
  47. """Stored profile for whatever is in this slot, calibrated for ``extruder``.
  48. Returns None when the slot holds no known spool, or when that spool has no
  49. profile for this nozzle — an operator who calibrated only one side should
  50. keep the binding they set by hand rather than have it swapped for a guess.
  51. Only the table the current inventory mode uses is consulted. Before #2812
  52. the inactive one was emptied on every mode toggle, so reading the built-in
  53. table first and stopping on a hit was safe -- there could be nothing in it
  54. to stop on. Nothing is emptied now, and a leftover built-in row would
  55. otherwise shadow the Spoolman assignment for the slot, returning that
  56. spool's profile or, on the deliberate stop below, no profile at all. That
  57. is the symptom #1556 reported from the other direction.
  58. """
  59. spoolman_mode = await spoolman_owns_assignments(db)
  60. assignment = (
  61. None
  62. if spoolman_mode
  63. else (
  64. await db.execute(
  65. select(SpoolAssignment).where(
  66. SpoolAssignment.printer_id == printer_id,
  67. SpoolAssignment.ams_id == ams_id,
  68. SpoolAssignment.tray_id == tray_id,
  69. )
  70. )
  71. ).scalar_one_or_none()
  72. )
  73. if assignment is not None:
  74. profile = (
  75. (
  76. await db.execute(
  77. select(SpoolKProfile).where(
  78. SpoolKProfile.spool_id == assignment.spool_id,
  79. SpoolKProfile.printer_id == printer_id,
  80. SpoolKProfile.extruder == extruder,
  81. SpoolKProfile.nozzle_diameter == nozzle_diameter,
  82. )
  83. )
  84. )
  85. .scalars()
  86. .all()
  87. )
  88. # Flow is filtered here rather than in SQL: a stored NULL matches any
  89. # fitted nozzle (see SlotNozzle.flow_matches), which is not an equality
  90. # test and would need an OR IS NULL that reads worse than this.
  91. profile = next((p for p in profile if _flow_applies(p.nozzle_type, flow)), None)
  92. if profile is not None:
  93. spool = (await db.execute(select(Spool).where(Spool.id == assignment.spool_id))).scalar_one_or_none()
  94. # The preset this profile was calibrated under, through the
  95. # per-printer-model cascade: a spool can carry a different preset
  96. # per model, and extrusion_cali_sel has to name the one the printer
  97. # will actually see in the slot. Falls back to the spool's own
  98. # value when the caller cannot say which model this is.
  99. filament_id = spool.slicer_filament if spool else None
  100. if spool is not None and printer_model:
  101. filament_id, _ = await resolve_spool_preset(
  102. db,
  103. spool_id=spool.id,
  104. printer_model=printer_model,
  105. nozzle_diameter=nozzle_diameter,
  106. fallback_filament=spool.slicer_filament,
  107. fallback_name=spool.slicer_filament_name,
  108. )
  109. return SlotKProfile(
  110. cali_idx=profile.cali_idx,
  111. k_value=profile.k_value,
  112. name=profile.name,
  113. extruder=profile.extruder,
  114. filament_id=filament_id,
  115. )
  116. # A known spool with no profile for this nozzle is a deliberate stop:
  117. # falling through to Spoolman would answer for a different spool.
  118. return None
  119. if not spoolman_mode:
  120. return None
  121. sm_assignment = (
  122. await db.execute(
  123. select(SpoolmanSlotAssignment).where(
  124. SpoolmanSlotAssignment.printer_id == printer_id,
  125. SpoolmanSlotAssignment.ams_id == ams_id,
  126. SpoolmanSlotAssignment.tray_id == tray_id,
  127. )
  128. )
  129. ).scalar_one_or_none()
  130. if sm_assignment is None:
  131. return None
  132. sm_profile = (
  133. (
  134. await db.execute(
  135. select(SpoolmanKProfile).where(
  136. SpoolmanKProfile.spoolman_spool_id == sm_assignment.spoolman_spool_id,
  137. SpoolmanKProfile.printer_id == printer_id,
  138. SpoolmanKProfile.extruder == extruder,
  139. SpoolmanKProfile.nozzle_diameter == nozzle_diameter,
  140. )
  141. )
  142. )
  143. .scalars()
  144. .all()
  145. )
  146. sm_profile = next((p for p in sm_profile if _flow_applies(p.nozzle_type, flow)), None)
  147. if sm_profile is None:
  148. return None
  149. # A Spoolman K row carries no preset of its own: the spool's per-model
  150. # override, stored locally (the table the Spoolman assign path writes),
  151. # else the preset set on the spool in Spoolman. With neither the caller
  152. # falls back to the tray's own tray_info_idx.
  153. sm_preset, _ = await resolve_spoolman_preset_with_own(
  154. db,
  155. spoolman_spool_id=sm_assignment.spoolman_spool_id,
  156. printer_model=printer_model,
  157. nozzle_diameter=nozzle_diameter,
  158. )
  159. # As the assign path sends it: a filament id, not the stored setting-id
  160. # form ("GFSL05_07"). Anything that needs the slicer's resolver (a local
  161. # preset number, an Orca UUID, a cloud user preset) is left to the tray's
  162. # own id, as before.
  163. sm_filament_id = normalize_slicer_filament(sm_preset)[0] or None
  164. if sm_filament_id and not (
  165. sm_filament_id.startswith("GF")
  166. or (sm_filament_id.startswith("P") and not sm_filament_id.startswith(("PFUS", "PFCN")))
  167. ):
  168. sm_filament_id = None
  169. return SlotKProfile(
  170. cali_idx=sm_profile.cali_idx,
  171. k_value=sm_profile.k_value,
  172. name=sm_profile.name,
  173. extruder=sm_profile.extruder,
  174. filament_id=sm_filament_id,
  175. )