fts_routing.py 3.5 KB

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