slot_kprofile.py 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147
  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. @dataclass(frozen=True)
  22. class SlotKProfile:
  23. """A stored calibration profile, flattened across the two inventory backends."""
  24. cali_idx: int | None
  25. k_value: float | None
  26. name: str | None
  27. extruder: int
  28. # The preset the profile was calibrated under. ``extrusion_cali_sel`` must
  29. # carry this rather than the tray's RFID value, or the firmware mislinks it.
  30. filament_id: str | None
  31. async def find_slot_kprofile_for_extruder(
  32. db: AsyncSession,
  33. printer_id: int,
  34. ams_id: int,
  35. tray_id: int,
  36. extruder: int,
  37. nozzle_diameter: str,
  38. ) -> SlotKProfile | None:
  39. """Stored profile for whatever is in this slot, calibrated for ``extruder``.
  40. Returns None when the slot holds no known spool, or when that spool has no
  41. profile for this nozzle — an operator who calibrated only one side should
  42. keep the binding they set by hand rather than have it swapped for a guess.
  43. Only the table the current inventory mode uses is consulted. Before #2812
  44. the inactive one was emptied on every mode toggle, so reading the built-in
  45. table first and stopping on a hit was safe -- there could be nothing in it
  46. to stop on. Nothing is emptied now, and a leftover built-in row would
  47. otherwise shadow the Spoolman assignment for the slot, returning that
  48. spool's profile or, on the deliberate stop below, no profile at all. That
  49. is the symptom #1556 reported from the other direction.
  50. """
  51. spoolman_mode = await spoolman_owns_assignments(db)
  52. assignment = (
  53. None
  54. if spoolman_mode
  55. else (
  56. await db.execute(
  57. select(SpoolAssignment).where(
  58. SpoolAssignment.printer_id == printer_id,
  59. SpoolAssignment.ams_id == ams_id,
  60. SpoolAssignment.tray_id == tray_id,
  61. )
  62. )
  63. ).scalar_one_or_none()
  64. )
  65. if assignment is not None:
  66. profile = (
  67. (
  68. await db.execute(
  69. select(SpoolKProfile).where(
  70. SpoolKProfile.spool_id == assignment.spool_id,
  71. SpoolKProfile.printer_id == printer_id,
  72. SpoolKProfile.extruder == extruder,
  73. SpoolKProfile.nozzle_diameter == nozzle_diameter,
  74. )
  75. )
  76. )
  77. .scalars()
  78. .first()
  79. )
  80. if profile is not None:
  81. spool = (await db.execute(select(Spool).where(Spool.id == assignment.spool_id))).scalar_one_or_none()
  82. return SlotKProfile(
  83. cali_idx=profile.cali_idx,
  84. k_value=profile.k_value,
  85. name=profile.name,
  86. extruder=profile.extruder,
  87. filament_id=(spool.slicer_filament if spool else None),
  88. )
  89. # A known spool with no profile for this nozzle is a deliberate stop:
  90. # falling through to Spoolman would answer for a different spool.
  91. return None
  92. if not spoolman_mode:
  93. return None
  94. sm_assignment = (
  95. await db.execute(
  96. select(SpoolmanSlotAssignment).where(
  97. SpoolmanSlotAssignment.printer_id == printer_id,
  98. SpoolmanSlotAssignment.ams_id == ams_id,
  99. SpoolmanSlotAssignment.tray_id == tray_id,
  100. )
  101. )
  102. ).scalar_one_or_none()
  103. if sm_assignment is None:
  104. return None
  105. sm_profile = (
  106. (
  107. await db.execute(
  108. select(SpoolmanKProfile).where(
  109. SpoolmanKProfile.spoolman_spool_id == sm_assignment.spoolman_spool_id,
  110. SpoolmanKProfile.printer_id == printer_id,
  111. SpoolmanKProfile.extruder == extruder,
  112. SpoolmanKProfile.nozzle_diameter == nozzle_diameter,
  113. )
  114. )
  115. )
  116. .scalars()
  117. .first()
  118. )
  119. if sm_profile is None:
  120. return None
  121. # Spoolman rows carry no slicer preset; the caller falls back to the tray's
  122. # own tray_info_idx for filament_id.
  123. return SlotKProfile(
  124. cali_idx=sm_profile.cali_idx,
  125. k_value=sm_profile.k_value,
  126. name=sm_profile.name,
  127. extruder=sm_profile.extruder,
  128. filament_id=None,
  129. )