mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-18 11:56:01 +02:00
fix(ams): resolve a slot's K profile by index when the printer does not file per hotend (issue #3044)
An X2D with two AMS 2 Pro, one per hotend, showed a K value on every slot
of the first and nothing on any slot of the second. Configure Slot was
worse than blank there: the picker offered no matching profile, the slot
read as though nothing were bound, and choosing one changed nothing the
user could see. Both symptoms are one rule.
A calibration index can mean two different profiles on a dual-nozzle
machine -- on the maintainer's H2C, index 16 is the left hotend's black
PLA at K=0.018 and 15 is the right's at K=0.020 -- so the index is
resolved against the slot's own hotend, and a miss shows nothing rather
than the other nozzle's number. That is right whenever the printer files
its calibrations per hotend. This one files them per filament: the second
AMS's slots point at the same entries as the first, every entry tagged
with one extruder, and requiring a match found nothing at all.
The hotend now has to appear in the table the printer actually sent
before it is used to narrow anything. Where it does not, the index stands
on its own, which is what BambuStudio does for this same card --
AMSItem.cpp resolves it through get_pa_k_n_value_by_cali_idx, matching
cali_idx and nothing else. Where it does, nothing changes: the H2C case
still blanks rather than borrowing, and the other hotend's profiles stay
reachable under Other K profiles. The relaxed path still refuses an
answer when the candidates disagree on a value.
The premise that the table is always numbered per nozzle had been written
into three comments and two layers of code; it is corrected where it
appears.
Alongside it, in the same picker: the K-profile options rendered the
hotend suffix twice in the matching group and three times under Other, so
every option on a dual-nozzle printer read "... . Left . Left".
This commit is contained in:
@@ -528,13 +528,14 @@ async def get_printer_status(
|
||||
ams_exists = False
|
||||
raw_data = state.raw_data or {}
|
||||
|
||||
# K value for a slot's bound profile, resolved against its own nozzle.
|
||||
# K value for a slot's bound profile, preferring the slot's own nozzle.
|
||||
#
|
||||
# Keyed on more than cali_idx: the printer numbers its calibration table
|
||||
# per nozzle, so entry 16 exists on each and means a different profile on
|
||||
# each. A cali_idx-only map let whichever profile the printer happened to
|
||||
# list last overwrite the other, and the slot then displayed the wrong
|
||||
# nozzle's K — on the maintainer's H2C, 0.018 and 0.020 for the same spool.
|
||||
# cali_idx alone is not enough: two profiles can share an index and differ
|
||||
# by extruder, and a cali_idx-only map let whichever the printer listed
|
||||
# last overwrite the other — on the maintainer's H2C, 0.018 and 0.020 for
|
||||
# the same spool. Nor is the extruder a requirement: one profile can be
|
||||
# what both extruders' slots point at, and demanding a match blanked every
|
||||
# slot on a second AMS (#3044). The resolver does both in order.
|
||||
_kprofile_k = build_slot_k_resolver(state)
|
||||
|
||||
# Cached active-cycle drying params (filament + target temp) we sent
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
"""Which nozzle an AMS slot feeds, with or without a Filament Track Switch.
|
||||
|
||||
K-profiles are per-nozzle, and the printer's calibration tables are numbered
|
||||
per-nozzle too: ``cali_idx: 16`` means "entry 16 of whichever nozzle feeds this
|
||||
tray". Without a switch that is unambiguous, because each AMS is wired to one
|
||||
extruder and says so in its ``info`` bits. With a switch installed every AMS
|
||||
reports 0xE instead and is bound to a switch *inlet*, so the answer has to come
|
||||
from the inlet binding.
|
||||
K-profiles are per-nozzle: a calibration run belongs to the hotend it ran on,
|
||||
and ``cali_idx: 16`` can name a different profile on each. (It need not — one
|
||||
profile can also be what both extruders' slots point at, which is why the K
|
||||
lookup in ``kprofile_lookup`` treats the extruder as a preference rather than a
|
||||
filter — but the routing question below is the same either way.) Without a
|
||||
switch the answer is unambiguous, because each AMS is wired to one extruder and
|
||||
says so in its ``info`` bits. With a switch installed every AMS reports 0xE
|
||||
instead and is bound to a switch *inlet*, so the answer has to come from the
|
||||
inlet binding.
|
||||
|
||||
Every caller that resolves a slot to an extruder should go through
|
||||
``slot_extruder`` here. Three separate copies of that logic used to end in
|
||||
|
||||
@@ -2,21 +2,41 @@
|
||||
|
||||
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 flat: the printer
|
||||
numbers it **per nozzle**, so entry 16 exists under every nozzle it holds
|
||||
profiles for and means a different profile on each.
|
||||
calibration table in ``state.kprofiles``.
|
||||
|
||||
``state.kprofiles`` is the union across nozzle diameters (see
|
||||
``BambuMQTTClient._store_kprofiles``), which is what the assign paths need but
|
||||
makes ``cali_idx`` alone ambiguous. Resolution here is therefore:
|
||||
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:
|
||||
|
||||
1. the slot's own extruder, which separates the two nozzles of a dual-nozzle
|
||||
machine outright;
|
||||
2. failing that, the diameters currently installed, which separates a live
|
||||
table from one left behind by a nozzle that has since been swapped out.
|
||||
* 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.
|
||||
|
||||
If both fail to single out one profile the answer is ``None``. A blank space on
|
||||
the card is a smaller error than confidently printing the other nozzle's number.
|
||||
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
|
||||
@@ -34,6 +54,9 @@ def build_slot_k_resolver(state) -> Callable[[int | None, int, int], float | Non
|
||||
# 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
|
||||
@@ -45,22 +68,45 @@ def build_slot_k_resolver(state) -> Callable[[int | None, int, int], float | Non
|
||||
extruder = int(kp.extruder_id or 0)
|
||||
except (ValueError, TypeError):
|
||||
extruder = 0
|
||||
table.setdefault((extruder, kp.slot_id), {})[str(kp.nozzle_diameter or "")] = k_value
|
||||
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.
|
||||
by_nozzle = table.get((extruder if extruder is not None else 0, cali_idx))
|
||||
if not by_nozzle:
|
||||
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
|
||||
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
|
||||
candidates = shared.get(cali_idx)
|
||||
return _agreed(candidates) if candidates else None
|
||||
|
||||
return resolve
|
||||
|
||||
@@ -190,6 +190,105 @@ class TestSlotKResolver:
|
||||
assert resolve(3, 0, 0) is None
|
||||
|
||||
|
||||
class TestASlotWhoseExtruderClaimsNoProfile:
|
||||
"""#3044: an X2D showed K on its first AMS and nothing on its second.
|
||||
|
||||
The printer does not always file a profile per hotend. In the reporter's
|
||||
capture the second AMS's slots pointed at the same table entries as the
|
||||
first -- B1 read the K of A4, B3 the K of A1 -- and those entries carry one
|
||||
extruder. Requiring the slot's own extruder to match therefore found
|
||||
nothing for every slot on the right-hand AMS.
|
||||
|
||||
BambuStudio, filling the same card, does not scope by extruder at all:
|
||||
``AMSItem.cpp`` resolves through ``get_pa_k_n_value_by_cali_idx``, which
|
||||
takes the first entry with a matching ``cali_idx``.
|
||||
"""
|
||||
|
||||
def test_a_shared_profile_resolves_on_the_other_extruder(self):
|
||||
resolve = build_slot_k_resolver(
|
||||
_state(
|
||||
[_profile(3, "0.021000", "0.4", extruder=0)],
|
||||
ams_extruder_map={"0": 0, "1": 1},
|
||||
)
|
||||
)
|
||||
|
||||
assert resolve(3, 0, 0) == pytest.approx(0.021)
|
||||
assert resolve(3, 1, 0) == pytest.approx(0.021)
|
||||
|
||||
def test_the_slots_own_extruder_still_wins_over_the_fallback(self):
|
||||
"""The H2C case has to keep winning: the fallback is a last resort, not
|
||||
a replacement. Index 3 exists on both hotends with different K."""
|
||||
resolve = build_slot_k_resolver(
|
||||
_state(
|
||||
[_profile(3, "0.020000", "0.4", extruder=0), _profile(3, "0.018000", "0.4", extruder=1)],
|
||||
ams_extruder_map={"0": 1, "1": 0},
|
||||
)
|
||||
)
|
||||
|
||||
assert resolve(3, 0, 0) == pytest.approx(0.018)
|
||||
assert resolve(3, 1, 0) == pytest.approx(0.020)
|
||||
|
||||
def test_two_entries_agreeing_on_one_k_is_not_an_ambiguity(self):
|
||||
"""Both hotends calibrated to the same number says nothing is in doubt."""
|
||||
resolve = build_slot_k_resolver(
|
||||
_state(
|
||||
[_profile(3, "0.021000", "0.4", extruder=0), _profile(3, "0.021000", "0.6", extruder=0)],
|
||||
nozzles=("0.4", "0.6"),
|
||||
ams_extruder_map={"0": 1},
|
||||
)
|
||||
)
|
||||
|
||||
assert resolve(3, 0, 0) == pytest.approx(0.021)
|
||||
|
||||
def test_the_fallback_still_prefers_the_nozzle_that_is_fitted(self):
|
||||
"""A table left behind by a swapped-out nozzle loses to the live one."""
|
||||
resolve = build_slot_k_resolver(
|
||||
_state(
|
||||
[_profile(3, "0.020000", "0.4", extruder=0), _profile(3, "0.017000", "0.6", extruder=0)],
|
||||
nozzles=("0.6",),
|
||||
ams_extruder_map={"0": 1},
|
||||
)
|
||||
)
|
||||
|
||||
assert resolve(3, 0, 0) == pytest.approx(0.017)
|
||||
|
||||
def test_the_fallback_refuses_when_the_candidates_disagree(self):
|
||||
"""Blank still beats confidently printing one of two different numbers."""
|
||||
resolve = build_slot_k_resolver(
|
||||
_state(
|
||||
[_profile(3, "0.020000", "0.4", extruder=0), _profile(3, "0.017000", "0.6", extruder=0)],
|
||||
nozzles=("0.4", "0.6"),
|
||||
ams_extruder_map={"0": 1},
|
||||
)
|
||||
)
|
||||
|
||||
assert resolve(3, 0, 0) is None
|
||||
|
||||
def test_a_hotend_with_its_own_profiles_does_not_borrow_the_others(self):
|
||||
"""The H2C guard, which the fallback must not reopen.
|
||||
|
||||
Index 16 is the left hotend's entry and index 15 the right's. A
|
||||
right-hand slot bound to 16 means "entry 16 of the right nozzle's
|
||||
table", which this printer does not have -- and the left's entry 16 is
|
||||
a different profile, not a stand-in. Blank is the honest answer.
|
||||
"""
|
||||
resolve = build_slot_k_resolver(
|
||||
_state(
|
||||
[_profile(16, "0.018000", "0.4", extruder=1), _profile(15, "0.020000", "0.4", extruder=0)],
|
||||
ams_extruder_map={"0": 0, "1": 1},
|
||||
)
|
||||
)
|
||||
|
||||
assert resolve(16, 0, 0) is None
|
||||
assert resolve(15, 0, 0) == pytest.approx(0.020)
|
||||
assert resolve(16, 1, 0) == pytest.approx(0.018)
|
||||
|
||||
def test_an_index_no_profile_holds_is_still_nothing(self):
|
||||
resolve = build_slot_k_resolver(_state([_profile(3, "0.020000", "0.4", extruder=0)], ams_extruder_map={"0": 1}))
|
||||
|
||||
assert resolve(9, 0, 0) is None
|
||||
|
||||
|
||||
class TestPrimeKProfileTable:
|
||||
"""Nothing used to read the calibration table on connect.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user