Files
maziggy 309e64b8a2 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".
2026-09-07 19:23:48 +02:00

80 lines
3.5 KiB
Python

"""Which nozzle an AMS slot feeds, with or without a Filament Track Switch.
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
``else 0``, which on a switch machine silently filed every profile under the
right-hand nozzle regardless of where the slot actually was.
Kept as a leaf module with no imports of its own so the routes, the MQTT layer
and the scheduler can all share one answer.
"""
# Which extruder each switch outlet terminates at. Measured on the maintainer's
# H2C, 2026-08-16: Out-A is the left hotend, Out-B is the right one.
#
# We would rather read this than assert it, but it is not in the telemetry:
# ``fila_switch.out`` reported ``[1, 1]`` unchanged across a 90-second capture,
# i.e. both outlets claiming the same extruder, which cannot describe real
# wiring. Whatever that field means, it is not outlet-to-nozzle.
#
# The inlet-to-outlet pairing then comes from the switch's un-crossed rest
# position: In-A -> Out-A, In-B -> Out-B. The switch does cross the two during a
# filament change, so this describes where a slot sits between prints, which is
# what a manual "configure this slot" task needs. It is deliberately one table
# to change if a machine turns up with its outlet tubes swapped.
FTS_INLET_EXTRUDER: dict[str, int] = {
"A": 1, # left / deputy
"B": 0, # right / main
}
def extruder_for_inlet(inlet: str | None) -> int | None:
"""Extruder fed by switch inlet ``"A"`` or ``"B"``; None for anything else."""
if not inlet:
return None
return FTS_INLET_EXTRUDER.get(inlet.upper())
def slot_extruder(
ams_id: int,
tray_id: int,
ams_extruder_map: dict | None,
ams_switch_inlet: dict | None = None,
) -> int | None:
"""Resolve one AMS slot to the extruder it feeds, or None if unknowable.
Returns None rather than guessing. A single-nozzle printer has no map and no
switch, and there the caller's own default of extruder 0 is correct — but on
a dual-nozzle machine "I don't know" and "the right-hand nozzle" are very
different answers, and conflating them is what bound a left-nozzle K-profile
to a slot sitting on the right.
``ams_id`` 255 is the external spool holder, where the tray id names the
side directly: tray 0 is Ext-L (extruder 1) and tray 1 is Ext-R (extruder 0).
"""
if ams_id == 255:
return 1 - tray_id if tray_id in (0, 1) else None
# A real extruder id always wins. BambuStudio treats a non-0xE value as
# authoritative too, so an AMS wired straight to one nozzle keeps that
# binding even on a machine that has a switch fitted for its other units.
if ams_extruder_map:
mapped = ams_extruder_map.get(str(ams_id))
if mapped is not None:
return int(mapped)
if ams_switch_inlet:
return extruder_for_inlet(ams_switch_inlet.get(str(ams_id)))
return None