fts_routing.py 3.3 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576
  1. """Which nozzle an AMS slot feeds, with or without a Filament Track Switch.
  2. K-profiles are per-nozzle, and the printer's calibration tables are numbered
  3. per-nozzle too: ``cali_idx: 16`` means "entry 16 of whichever nozzle feeds this
  4. tray". Without a switch that is unambiguous, because each AMS is wired to one
  5. extruder and says so in its ``info`` bits. With a switch installed every AMS
  6. reports 0xE instead and is bound to a switch *inlet*, so the answer has to come
  7. from the inlet binding.
  8. Every caller that resolves a slot to an extruder should go through
  9. ``slot_extruder`` here. Three separate copies of that logic used to end in
  10. ``else 0``, which on a switch machine silently filed every profile under the
  11. right-hand nozzle regardless of where the slot actually was.
  12. Kept as a leaf module with no imports of its own so the routes, the MQTT layer
  13. and the scheduler can all share one answer.
  14. """
  15. # Which extruder each switch outlet terminates at. Measured on the maintainer's
  16. # H2C, 2026-08-16: Out-A is the left hotend, Out-B is the right one.
  17. #
  18. # We would rather read this than assert it, but it is not in the telemetry:
  19. # ``fila_switch.out`` reported ``[1, 1]`` unchanged across a 90-second capture,
  20. # i.e. both outlets claiming the same extruder, which cannot describe real
  21. # wiring. Whatever that field means, it is not outlet-to-nozzle.
  22. #
  23. # The inlet-to-outlet pairing then comes from the switch's un-crossed rest
  24. # position: In-A -> Out-A, In-B -> Out-B. The switch does cross the two during a
  25. # filament change, so this describes where a slot sits between prints, which is
  26. # what a manual "configure this slot" task needs. It is deliberately one table
  27. # to change if a machine turns up with its outlet tubes swapped.
  28. FTS_INLET_EXTRUDER: dict[str, int] = {
  29. "A": 1, # left / deputy
  30. "B": 0, # right / main
  31. }
  32. def extruder_for_inlet(inlet: str | None) -> int | None:
  33. """Extruder fed by switch inlet ``"A"`` or ``"B"``; None for anything else."""
  34. if not inlet:
  35. return None
  36. return FTS_INLET_EXTRUDER.get(inlet.upper())
  37. def slot_extruder(
  38. ams_id: int,
  39. tray_id: int,
  40. ams_extruder_map: dict | None,
  41. ams_switch_inlet: dict | None = None,
  42. ) -> int | None:
  43. """Resolve one AMS slot to the extruder it feeds, or None if unknowable.
  44. Returns None rather than guessing. A single-nozzle printer has no map and no
  45. switch, and there the caller's own default of extruder 0 is correct — but on
  46. a dual-nozzle machine "I don't know" and "the right-hand nozzle" are very
  47. different answers, and conflating them is what bound a left-nozzle K-profile
  48. to a slot sitting on the right.
  49. ``ams_id`` 255 is the external spool holder, where the tray id names the
  50. side directly: tray 0 is Ext-L (extruder 1) and tray 1 is Ext-R (extruder 0).
  51. """
  52. if ams_id == 255:
  53. return 1 - tray_id if tray_id in (0, 1) else None
  54. # A real extruder id always wins. BambuStudio treats a non-0xE value as
  55. # authoritative too, so an AMS wired straight to one nozzle keeps that
  56. # binding even on a machine that has a switch fitted for its other units.
  57. if ams_extruder_map:
  58. mapped = ams_extruder_map.get(str(ams_id))
  59. if mapped is not None:
  60. return int(mapped)
  61. if ams_switch_inlet:
  62. return extruder_for_inlet(ams_switch_inlet.get(str(ams_id)))
  63. return None