kprofile_lookup.py 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112
  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``.
  5. That table is not a clean per-nozzle numbering, and treating it as one is what
  6. blanked every slot on a second AMS (#3044). Both of these happen:
  7. * Two profiles can share a ``cali_idx`` and differ by extruder — measured on
  8. the maintainer's H2C, where one spool read 0.018 on the left nozzle and
  9. 0.020 on the right. Resolving on ``cali_idx`` alone showed the wrong one.
  10. * One profile can be what *both* extruders' slots point at. In the #3044
  11. capture an X2D's B1 and B3 carried exactly the K values of A4 and A1 — the
  12. same entries, tagged with one extruder. Demanding an extruder match left
  13. every slot on the right-hand AMS blank.
  14. The two are told apart by whether the table distinguishes extruders *at all*:
  15. 1. a profile filed under the slot's own extruder wins outright;
  16. 2. if the slot's extruder appears nowhere in the table, its tagging carries no
  17. information about this slot, so match on ``cali_idx`` alone — taking the
  18. answer only when the candidates agree on one K value, with the diameters
  19. currently installed as the tie-break (which separates a live table from one
  20. left behind by a nozzle that has since been swapped out).
  21. The condition on step 2 is what keeps the H2C case fixed. There extruder 0 does
  22. hold profiles, so a right-hand slot pointing at an index only the left hotend
  23. has is a real miss — the index means entry 16 *of the right nozzle's table*,
  24. and the left's entry 16 is a different profile. Falling back there is how the
  25. wrong K got shown in the first place.
  26. BambuStudio is looser still: ``AMSItem.cpp`` fills the same card through
  27. ``CalibUtils::get_pa_k_n_value_by_cali_idx``, which scans the whole history for
  28. a matching ``cali_idx`` and takes the first hit regardless of nozzle.
  29. If neither step singles out one value the answer is ``None``. A blank space on
  30. the card is a smaller error than confidently printing the other nozzle's
  31. number.
  32. """
  33. from collections.abc import Callable
  34. from backend.app.utils.fts_routing import slot_extruder
  35. def build_slot_k_resolver(state) -> Callable[[int | None, int, int], float | None]:
  36. """Return ``resolve(cali_idx, ams_id, tray_id) -> k value or None``.
  37. Built once per serialization pass and closed over the state, so the REST
  38. and WebSocket views of the same card cannot answer differently.
  39. """
  40. # (extruder, cali_idx) -> {nozzle_diameter: k}. The inner dict is what
  41. # detects the ambiguity: more than one entry means two nozzles' tables both
  42. # claim this index on this extruder.
  43. table: dict[tuple[int, int], dict[str, float]] = {}
  44. # cali_idx -> [(nozzle_diameter, k)], every extruder together. The fallback
  45. # for an index no profile claims on the slot's own extruder.
  46. shared: dict[int, list[tuple[str, float]]] = {}
  47. for kp in getattr(state, "kprofiles", None) or []:
  48. if kp.slot_id is None or not kp.k_value:
  49. continue
  50. try:
  51. k_value = float(kp.k_value)
  52. except (ValueError, TypeError):
  53. continue # Skip K-profile entries with unparseable values
  54. try:
  55. extruder = int(kp.extruder_id or 0)
  56. except (ValueError, TypeError):
  57. extruder = 0
  58. nozzle = str(kp.nozzle_diameter or "")
  59. table.setdefault((extruder, kp.slot_id), {})[nozzle] = k_value
  60. shared.setdefault(kp.slot_id, []).append((nozzle, k_value))
  61. # Which extruders the table names at all. An extruder missing from this is
  62. # one the printer is not filing profiles under, which is what makes the
  63. # cali_idx-only fallback safe for it.
  64. extruders_filed = {extruder for extruder, _ in table}
  65. installed = {str(n.nozzle_diameter) for n in (getattr(state, "nozzles", None) or []) if n.nozzle_diameter}
  66. def _agreed(candidates: list[tuple[str, float]]) -> float | None:
  67. """The one K these candidates describe, or None if they disagree.
  68. Values rather than entries: two nozzles listing the same number is not
  69. an ambiguity, it is the shared profile the fallback exists for.
  70. """
  71. values = {k for _, k in candidates}
  72. if len(values) == 1:
  73. return values.pop()
  74. live = {k for nozzle, k in candidates if nozzle in installed}
  75. return live.pop() if len(live) == 1 else None
  76. def resolve(cali_idx: int | None, ams_id: int, tray_id: int) -> float | None:
  77. if cali_idx is None:
  78. return None
  79. extruder = slot_extruder(ams_id, tray_id, state.ams_extruder_map, state.ams_switch_inlet)
  80. # Single-nozzle printers report everything under extruder 0, and that
  81. # is also the right default when the routing is simply unknown.
  82. own = extruder if extruder is not None else 0
  83. by_nozzle = table.get((own, cali_idx))
  84. if by_nozzle:
  85. if len(by_nozzle) == 1:
  86. return next(iter(by_nozzle.values()))
  87. live = [k for nozzle, k in by_nozzle.items() if nozzle in installed]
  88. return live[0] if len(live) == 1 else None
  89. if own in extruders_filed:
  90. return None
  91. candidates = shared.get(cali_idx)
  92. return _agreed(candidates) if candidates else None
  93. return resolve