feat(printer): support Filament Track Switch (FTS) accessory in print modal (#1162)

The FTS routes any AMS slot to either extruder, so AMS info reports
  bits 8-11 = 0xE (uninitialized) and ams_extruder_map ends up empty.
  The print modal's per-nozzle dropdown filter then hides every loaded
  slot, leaving the user with an empty filament dropdown.

  Detection: parse print.device.fila_switch from MQTT push_status into a
  new FilaSwitchState dataclass on PrinterState; surface it through the
  GET /printers/{id}/status response as a nullable FilaSwitchResponse.

  Frontend: useFilamentMapping and FilamentMapping skip the per-extruder
  filter when fila_switch.installed is true. Slots currently fed into a
  track display an [L]/[R] routing badge in the dropdown so the user
  can see where the FTS is currently routing them.

  Tests: 4 backend unit (TestFilamentTrackSwitchDetection), 2 backend
  integration (status route), 2 hook regression, 2 component regression.
This commit is contained in:
maziggy
2026-04-29 16:18:45 +02:00
parent f45f6a131d
commit b45ca2a662
14 changed files with 596 additions and 117 deletions
+13 -3
View File
@@ -96,6 +96,10 @@ export function computeAmsMapping(
const loadedFilaments = buildLoadedFilaments(printerStatus);
if (loadedFilaments.length === 0) return undefined;
// FTS routes any AMS slot to any extruder, so per-nozzle slot restriction
// doesn't apply when it's installed (#1162).
const ftsActive = printerStatus?.fila_switch?.installed === true;
// Track which trays have been assigned to avoid duplicates
const usedTrayIds = new Set<number>();
@@ -108,7 +112,8 @@ export function computeAmsMapping(
// Nozzle-aware filtering: restrict to trays on the correct nozzle.
// This is a hard filter — cross-nozzle assignment causes print failures
// ("position of left hotend is abnormal"), so we never fall back to wrong-nozzle trays.
if (req.nozzle_id != null) {
// Skip when an FTS is installed: it can route any slot to either extruder.
if (req.nozzle_id != null && !ftsActive) {
available = available.filter((f) => f.extruderId === req.nozzle_id);
}
@@ -311,6 +316,10 @@ export function useFilamentMapping(
): UseFilamentMappingResult {
const loadedFilaments = useLoadedFilaments(printerStatus);
// FTS routes any AMS slot to any extruder, so per-nozzle slot restriction
// doesn't apply when it's installed (#1162).
const ftsActive = printerStatus?.fila_switch?.installed === true;
const filamentComparison = useMemo(() => {
if (!filamentReqs?.filaments || filamentReqs.filaments.length === 0) return [];
@@ -363,7 +372,8 @@ export function useFilamentMapping(
// Nozzle-aware filtering: restrict to trays on the correct nozzle.
// This is a hard filter — cross-nozzle assignment causes print failures.
if (req.nozzle_id != null) {
// Skip when an FTS is installed: it can route any slot to either extruder.
if (req.nozzle_id != null && !ftsActive) {
available = available.filter((f) => f.extruderId === req.nozzle_id);
}
@@ -469,7 +479,7 @@ export function useFilamentMapping(
isManual: false,
};
});
}, [filamentReqs, loadedFilaments, manualMappings, preferLowest]);
}, [filamentReqs, loadedFilaments, manualMappings, preferLowest, ftsActive]);
// Build AMS mapping from matched filaments
// Format: array matching 3MF filament slot structure