feat: Filament Track Switch (H2-series O2L-FTS) support

The Filament Track Switch (H2-series accessory, product code O2L-FTS) feeds
every AMS to both extruders through a two-track switch. Port full support
across the device layer, project config, and GUI.

Device / config:
- Model the switch-aware AMS binding (the set of extruders an AMS can feed
  and which input track A/B feeds it), switch readiness, the O2L-FTS firmware
  module, and the fun2 capability bit for checking a slice against installed
  hardware.
- Register has_filament_switcher and enable_filament_dynamic_map as project
  config that persists with the project and restores from a saved 3mf, and
  force both back to false on every project/printer/CLI load path. Live
  device sync is the only thing that sets them true.

GUI:
- Sidebar sync activates the switch from live device state, attributes each
  AMS to the extruder its input track feeds, shows a floating status icon
  (ready / not-calibrated), and surfaces a one-time tip / not-calibrated
  warning.
- Send dialog gains a non-blocking slice-vs-hardware mismatch warning and a
  blocking error when a slice needs dynamic nozzle mapping but the switch is
  missing or not set up.
- AMS load/unload guards, AMS-view routing glyph + un-calibrated banner +
  hidden external-spool road, and mapping-popup external-spool lockout.
- Filament pickers collapse the per-extruder split into a single deduplicated
  "AMS filaments" group with a smart-assign toggle when the switch is ready.
- Firmware-upgrade panel lists the O2L-FTS accessory and its version.
- Device-provided filament-change steps (ams.cfs) drive the change-step
  display when firmware sends them, including the three switch steps.
- "Load current filament" asks which extruder to feed via a
  FeedDirectionDialog when the switch is calibrated.

Inert without the accessory: every path is gated on the switch being
installed (MQTT aux bit 29, default off) or ready, both project flags default
false, and the per-extruder AMS attribution is byte-identical, so AMS state,
the send/load UI, and sliced g-code are unchanged for every printer that does
not report a Filament Track Switch.
This commit is contained in:
SoftFever
2026-07-09 18:57:05 +08:00
parent 26a0caff96
commit 0d31325df0
38 changed files with 1655 additions and 38 deletions
@@ -346,6 +346,15 @@ void DevFilaSystemParser::ParseV1_0(const json& jj, MachineObject* obj, DevFilaS
if (!key_field_only)
{
// Newer firmware sends the exact filament-change step sequence in ams.cfs, so the
// client no longer hardcodes the steps per model. Absent on current firmware, which
// keeps the list empty and the legacy hardcoded/ams_status_sub step path in effect.
if (jj["ams"].contains("cfs")) {
system->m_filament_change_steps = DevJsonValParser::GetVal<std::vector<DevFilamentStep>>(jj["ams"], "cfs");
} else {
system->m_filament_change_steps.clear();
}
if (jj["ams"].contains("tray_read_done_bits"))
{
obj->tray_read_done_bits = stol(jj["ams"]["tray_read_done_bits"].get<std::string>(), nullptr, 16);
@@ -403,17 +412,43 @@ void DevFilaSystemParser::ParseV1_0(const json& jj, MachineObject* obj, DevFilaS
int type_id = 1; // 0:dummy 1:ams 2:ams-lite 3:n3f 4:n3s
/*ams info*/
std::set<int> binded_extruder_set;
std::optional<DevFilaSwitch::SwitchPos> binded_switcher_pos;
if (it->contains("info")) {
const std::string& info = (*it)["info"].get<std::string>();
type_id = DevUtil::get_flag_bits(info, 0, 4);
extuder_id = DevUtil::get_flag_bits(info, 8, 4);
if (extuder_id == 0xE && obj->GetFilaSwitch()->IsInstalled()) {
int bind_switch_in = DevUtil::get_flag_bits(info, 24, 4);
if (bind_switch_in == 0 || bind_switch_in == 1) {
binded_extruder_set = { MAIN_EXTRUDER_ID, DEPUTY_EXTRUDER_ID };
}
if (bind_switch_in == 0) {
binded_switcher_pos = DevFilaSwitch::SwitchPos::POS_IN_B;
} else if (bind_switch_in == 1) {
binded_switcher_pos = DevFilaSwitch::SwitchPos::POS_IN_A;
}
// Orca: the switch feeds every AMS to both extruders; pin a deterministic
// single-extruder id so legacy GetExtruderId() consumers keep a valid value
// while switch-aware code reads the binding set instead.
extuder_id = MAIN_EXTRUDER_ID;
} else if (extuder_id != 0xE) {
binded_extruder_set = { extuder_id };
}
} else {
if (!obj->is_enable_ams_np && obj->get_printer_ams_type() == "f1") {
type_id = DevAms::AMS_LITE;
}
binded_extruder_set = { MAIN_EXTRUDER_ID };
}
/*AMS without initialization*/
// Orca: an AMS reporting extruder 0xE without a Filament Track Switch installed has
// no usable extruder binding; drop it, preserving the existing display for
// half-initialized printers. With the switch installed the 0xE case is remapped
// above to both extruders and falls through to normal handling.
if (extuder_id == 0xE)
{
ams_id_set.erase(ams_id);
@@ -445,6 +480,13 @@ void DevFilaSystemParser::ParseV1_0(const json& jj, MachineObject* obj, DevFilaS
/*set ams type flag*/
curr_ams->SetAmsType(type_id);
// Refresh the switch-aware extruder binding on every push (both create and
// update paths) so it can't go stale after an AMS is re-homed to another
// extruder. Without the switch the set holds the single bound extruder and
// the track position stays empty.
curr_ams->m_binded_extruder_set = binded_extruder_set;
curr_ams->m_binded_switcher_pos = binded_switcher_pos;
/*set ams exist flag*/
try