Files
bambuddy/backend/app/utils/fts_routing.py
T
maziggy c5e0055864 Track which nozzle each AMS feeds through the Filament Track Switch
With a switch fitted, an AMS is not wired to a nozzle any more. It is
plumbed into one of the switch's two inlets and reaches both nozzles
through it, so every unit reports its extruder as "not fixed" (0xE) and
ams_extruder_map comes back empty. Bambuddy had nothing to fall back on
but the AMS unit number, so AMS-A was badged R and AMS-B was badged L
purely because their unit ids are 0 and 1, a third unit got no badge at
all, and every one of those labels was wrong.

The binding needed no new telemetry. BambuStudio reads it out of bits
24-27 of the same AMS info string we already parse for the type and the
extruder id -- 0 is In-B, 1 is In-A -- and it only means anything when a
switch is installed, because without one 0xE really does mean an
uninitialised unit. That gates the read, which forced the switch block to
be parsed before the AMS block: _handle_ams_data runs early in
_process_message and _update_state only much later, so the binding was
lost on every frame that carried both.

The badge letters stay L and R, In-A reading as L, with the inlet named
in full in the tooltip -- the letter is the inlet's position and not a
claim about which nozzle that AMS feeds, since the switch can route
either inlet to either outlet. Both views update live now. The switch
fields were missing from the WebSocket payload, and the frontend
shallow-merges each push over its cached status, so an absent field kept
whatever the last full fetch left behind. The broadcast dedup key had no
term for them either, so "Join IN-B" on the printer screen moved nothing:
the binding is not in the tray component of that key, and not in the AMS
change-hash, which covers tray fields only and must stay that way because
it drives Spoolman sync.

The rest of this is the calibration half, which is where it actually
bites. K-profiles are calibrated per nozzle and the printer numbers its
calibration table per nozzle too, so entry 16 exists on both hotends and
means a different profile on each. A tray holds exactly one index. Move
an AMS to the other inlet and every configured slot in it silently keeps
pointing at the old hotend's table -- measured on the maintainer's H2C, a
black PLA calibrated 0.018 left and 0.020 right stayed on the left
profile after the move, and a manual RFID re-read only re-asserted the
same wrong one. Bambu has not solved this either; their AMS dialog
carries "TODO: fila_switcher broken the connection of ams->extruder"
above the line that decides which nozzle's profiles to offer.

Three copies of "which extruder is this slot on" each ended in else 0,
which on a switch machine filed every profile under the right-hand
nozzle. They now share one resolver that returns None for "unknown",
because unknown and extruder 0 are very different answers on a
dual-nozzle machine and conflating them is what bound a left-nozzle
profile to a slot sitting on the right. The per-slot K value on the
printer card had the same confusion from the other direction: its lookup
was keyed on cali_idx alone, so one nozzle's K silently overwrote the
other's.

Moving an AMS now re-selects each configured slot's counterpart profile
for the nozzle it has arrived on. Only the calibration binding moves, and
only for slots whose spool already has a profile for that nozzle:
configuring a slot is a deliberate preparation step, so a slot we know
nothing about, or a spool calibrated on one hotend only, is left exactly
as the operator set it. Nothing fires on the first sighting of a binding
either, since every reconnect learns them afresh and re-applying there
would overwrite a choice made by hand.

Configure Slot resolves against the slot's own nozzle throughout. Option
identity carries the extruder, so a filament calibrated on both hotends
gives two distinguishable entries instead of two that collapse into
whichever the printer listed first; options name the hotend; matches are
scoped to the nozzle the slot feeds, with the other hotend's profiles
still reachable under Other K profiles; and the slot's active index is
resolved as a pair rather than followed into the wrong table.

Inlet to nozzle is one table, In-A to the left hotend and In-B to the
right, measured rather than assumed -- fila_switch.out cannot be used for
it, reporting [1, 1] unchanged across a 90-second capture, both outlets
claiming the same extruder. The print dialog picks up the same inlet
labelling in its slot dropdown, replacing a left/right hint that never
rendered because it matched snow-encoded values against global tray ids;
decoding it correctly would not have saved it, since the firmware never
reports which inlet is currently paired with which outlet. The dialog
also notes when every filament a print needs sits behind one inlet, which
is legal but slow -- a change between two filaments on the same inlet
retracts the outgoing spool all the way back to its AMS, where a change
across the two only retracts as far as the switch.

Assigning an AMS to an inlet remains printer-side. BambuStudio can read
that binding and has no command to write it, so there is no wire format
to copy.
2026-08-16 15:59:59 +02:00

77 lines
3.3 KiB
Python

"""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.
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