kprofile_lookup.py 3.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566
  1. """Resolve an AMS slot's K value from the printer's calibration table.
  2. H2-series trays carry no ``k`` field of their own — only ``cali_idx`` — so the
  3. K value on the AMS slot card (#2854) is looked up from the printer's
  4. calibration table in ``state.kprofiles``. That table is not flat: the printer
  5. numbers it **per nozzle**, so entry 16 exists under every nozzle it holds
  6. profiles for and means a different profile on each.
  7. ``state.kprofiles`` is the union across nozzle diameters (see
  8. ``BambuMQTTClient._store_kprofiles``), which is what the assign paths need but
  9. makes ``cali_idx`` alone ambiguous. Resolution here is therefore:
  10. 1. the slot's own extruder, which separates the two nozzles of a dual-nozzle
  11. machine outright;
  12. 2. failing that, the diameters currently installed, which separates a live
  13. table from one left behind by a nozzle that has since been swapped out.
  14. If both fail to single out one profile the answer is ``None``. A blank space on
  15. the card is a smaller error than confidently printing the other nozzle's number.
  16. """
  17. from collections.abc import Callable
  18. from backend.app.utils.fts_routing import slot_extruder
  19. def build_slot_k_resolver(state) -> Callable[[int | None, int, int], float | None]:
  20. """Return ``resolve(cali_idx, ams_id, tray_id) -> k value or None``.
  21. Built once per serialization pass and closed over the state, so the REST
  22. and WebSocket views of the same card cannot answer differently.
  23. """
  24. # (extruder, cali_idx) -> {nozzle_diameter: k}. The inner dict is what
  25. # detects the ambiguity: more than one entry means two nozzles' tables both
  26. # claim this index on this extruder.
  27. table: dict[tuple[int, int], dict[str, float]] = {}
  28. for kp in getattr(state, "kprofiles", None) or []:
  29. if kp.slot_id is None or not kp.k_value:
  30. continue
  31. try:
  32. k_value = float(kp.k_value)
  33. except (ValueError, TypeError):
  34. continue # Skip K-profile entries with unparseable values
  35. try:
  36. extruder = int(kp.extruder_id or 0)
  37. except (ValueError, TypeError):
  38. extruder = 0
  39. table.setdefault((extruder, kp.slot_id), {})[str(kp.nozzle_diameter or "")] = k_value
  40. installed = {str(n.nozzle_diameter) for n in (getattr(state, "nozzles", None) or []) if n.nozzle_diameter}
  41. def resolve(cali_idx: int | None, ams_id: int, tray_id: int) -> float | None:
  42. if cali_idx is None:
  43. return None
  44. extruder = slot_extruder(ams_id, tray_id, state.ams_extruder_map, state.ams_switch_inlet)
  45. # Single-nozzle printers report everything under extruder 0, and that
  46. # is also the right default when the routing is simply unknown.
  47. by_nozzle = table.get((extruder if extruder is not None else 0, cali_idx))
  48. if not by_nozzle:
  49. return None
  50. if len(by_nozzle) == 1:
  51. return next(iter(by_nozzle.values()))
  52. live = [k for nozzle, k in by_nozzle.items() if nozzle in installed]
  53. return live[0] if len(live) == 1 else None
  54. return resolve