| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112 |
- """Resolve an AMS slot's K value from the printer's calibration table.
- H2-series trays carry no ``k`` field of their own — only ``cali_idx`` — so the
- K value on the AMS slot card (#2854) is looked up from the printer's
- calibration table in ``state.kprofiles``.
- That table is not a clean per-nozzle numbering, and treating it as one is what
- blanked every slot on a second AMS (#3044). Both of these happen:
- * Two profiles can share a ``cali_idx`` and differ by extruder — measured on
- the maintainer's H2C, where one spool read 0.018 on the left nozzle and
- 0.020 on the right. Resolving on ``cali_idx`` alone showed the wrong one.
- * One profile can be what *both* extruders' slots point at. In the #3044
- capture an X2D's B1 and B3 carried exactly the K values of A4 and A1 — the
- same entries, tagged with one extruder. Demanding an extruder match left
- every slot on the right-hand AMS blank.
- The two are told apart by whether the table distinguishes extruders *at all*:
- 1. a profile filed under the slot's own extruder wins outright;
- 2. if the slot's extruder appears nowhere in the table, its tagging carries no
- information about this slot, so match on ``cali_idx`` alone — taking the
- answer only when the candidates agree on one K value, with the diameters
- currently installed as the tie-break (which separates a live table from one
- left behind by a nozzle that has since been swapped out).
- The condition on step 2 is what keeps the H2C case fixed. There extruder 0 does
- hold profiles, so a right-hand slot pointing at an index only the left hotend
- has is a real miss — the index means entry 16 *of the right nozzle's table*,
- and the left's entry 16 is a different profile. Falling back there is how the
- wrong K got shown in the first place.
- BambuStudio is looser still: ``AMSItem.cpp`` fills the same card through
- ``CalibUtils::get_pa_k_n_value_by_cali_idx``, which scans the whole history for
- a matching ``cali_idx`` and takes the first hit regardless of nozzle.
- If neither step singles out one value the answer is ``None``. A blank space on
- the card is a smaller error than confidently printing the other nozzle's
- number.
- """
- from collections.abc import Callable
- from backend.app.utils.fts_routing import slot_extruder
- def build_slot_k_resolver(state) -> Callable[[int | None, int, int], float | None]:
- """Return ``resolve(cali_idx, ams_id, tray_id) -> k value or None``.
- Built once per serialization pass and closed over the state, so the REST
- and WebSocket views of the same card cannot answer differently.
- """
- # (extruder, cali_idx) -> {nozzle_diameter: k}. The inner dict is what
- # detects the ambiguity: more than one entry means two nozzles' tables both
- # claim this index on this extruder.
- table: dict[tuple[int, int], dict[str, float]] = {}
- # cali_idx -> [(nozzle_diameter, k)], every extruder together. The fallback
- # for an index no profile claims on the slot's own extruder.
- shared: dict[int, list[tuple[str, float]]] = {}
- for kp in getattr(state, "kprofiles", None) or []:
- if kp.slot_id is None or not kp.k_value:
- continue
- try:
- k_value = float(kp.k_value)
- except (ValueError, TypeError):
- continue # Skip K-profile entries with unparseable values
- try:
- extruder = int(kp.extruder_id or 0)
- except (ValueError, TypeError):
- extruder = 0
- nozzle = str(kp.nozzle_diameter or "")
- table.setdefault((extruder, kp.slot_id), {})[nozzle] = k_value
- shared.setdefault(kp.slot_id, []).append((nozzle, k_value))
- # Which extruders the table names at all. An extruder missing from this is
- # one the printer is not filing profiles under, which is what makes the
- # cali_idx-only fallback safe for it.
- extruders_filed = {extruder for extruder, _ in table}
- installed = {str(n.nozzle_diameter) for n in (getattr(state, "nozzles", None) or []) if n.nozzle_diameter}
- def _agreed(candidates: list[tuple[str, float]]) -> float | None:
- """The one K these candidates describe, or None if they disagree.
- Values rather than entries: two nozzles listing the same number is not
- an ambiguity, it is the shared profile the fallback exists for.
- """
- values = {k for _, k in candidates}
- if len(values) == 1:
- return values.pop()
- live = {k for nozzle, k in candidates if nozzle in installed}
- return live.pop() if len(live) == 1 else None
- def resolve(cali_idx: int | None, ams_id: int, tray_id: int) -> float | None:
- if cali_idx is None:
- return None
- extruder = slot_extruder(ams_id, tray_id, state.ams_extruder_map, state.ams_switch_inlet)
- # Single-nozzle printers report everything under extruder 0, and that
- # is also the right default when the routing is simply unknown.
- own = extruder if extruder is not None else 0
- by_nozzle = table.get((own, cali_idx))
- if by_nozzle:
- if len(by_nozzle) == 1:
- return next(iter(by_nozzle.values()))
- live = [k for nozzle, k in by_nozzle.items() if nozzle in installed]
- return live[0] if len(live) == 1 else None
- if own in extruders_filed:
- return None
- candidates = shared.get(cali_idx)
- return _agreed(candidates) if candidates else None
- return resolve
|