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

View File

@@ -9,9 +9,11 @@
#include "slic3r/GUI/DeviceCore/DevManager.h"
#include "slic3r/GUI/DeviceCore/DevFilaSystem.h"
#include "slic3r/GUI/DeviceCore/DevFilaSwitch.h"
#include <wx/simplebook.h>
#include <wx/dcgraph.h>
#include <wx/artprov.h>
#include <boost/log/trivial.hpp>
@@ -39,7 +41,7 @@ AMSControl::AMSControl(wxWindow *parent, wxWindowID id, const wxPoint &pos, cons
SetBackgroundColour(*wxWHITE);
// normal mode
//Freeze();
wxBoxSizer *m_sizer_body = new wxBoxSizer(wxVERTICAL);
m_sizer_body = new wxBoxSizer(wxVERTICAL);
m_amswin = new wxWindow(this, wxID_ANY);
m_amswin->SetBackgroundColour(*wxWHITE);
m_amswin->SetSize(wxSize(FromDIP(578), -1));
@@ -151,6 +153,12 @@ AMSControl::AMSControl(wxWindow *parent, wxWindowID id, const wxPoint &pos, cons
m_extruder = new AMSextruder(m_amswin, wxID_ANY, m_total_ext_count, wxDefaultPosition, AMS_EXTRUDER_SIZE);
m_sizer_option_mid->Add( m_extruder, 0, wxALIGN_CENTER, 0 );
// Orca: filament-switch routing glyph; hidden by default so it stays inert (zero layout impact)
// on any printer without a Filament Track Switch. Shown from UpdateAms only when installed.
m_switcher = new SwitcherImage(m_amswin, wxID_ANY, "fila_switch", wxSize(FromDIP(29), FromDIP(16)), wxDefaultPosition);
m_switcher->Hide();
m_sizer_option_mid->Add(m_switcher, 0, wxALIGN_CENTER | wxLEFT, FromDIP(6));
/*option right*/
m_button_extruder_feed = new Button(m_panel_option_right, _L("Load"));
@@ -975,6 +983,32 @@ void AMSControl::UpdateAms(const std::string &series_name,
{
m_amswin->Layout();
}
/*update switch status*/
// Orca: inert on any printer without a Filament Track Switch — install is false, so the banner is
// never materialized, the glyph stays hidden, and every ShowRoad(true) below is a no-op.
const auto [install, ready] = isFilaSwitchReady();
show_switcher_status(install && (!ready));
bool isShow = install && m_total_ext_count >= 2;
if (m_switcher->IsShown() != isShow)
{
m_switcher->Show(isShow);
m_sizer_body->Layout();
m_sizer_body->Fit(this);
this->Layout();
this->Refresh(true);
this->Update();
}
/*update ext road visibility when fila switch installed*/
bool road_visibility_changed = false;
for (auto& [ams_id, ams_item] : m_ams_item_list) {
if (!ams_item) continue;
// Orca: drop the external-spool road (AMSModel::EXT_AMS) while the switch routes all filament.
const bool should_show_road = !(install && ams_item->get_ams_model() == AMSModel::EXT_AMS);
if (ams_item->ShowRoad(should_show_road)) { road_visibility_changed = true; }
}
if (road_visibility_changed) { m_amswin->Layout(); }
}
void AMSControl::AddAmsPreview(AMSinfo info, AMSModel type)
@@ -1596,6 +1630,54 @@ void AMSControl::on_ams_setting_click(wxMouseEvent &event)
post_event(SimpleEvent(EVT_AMS_SETTINGS));
}
std::tuple<bool, bool> AMSControl::isFilaSwitchReady()
{
DeviceManager* dev = Slic3r::GUI::wxGetApp().getDeviceManager();
if (!dev) return {false, false};
MachineObject* obj = dev->get_selected_machine();
if (!obj) return {false, false};
// Orca: GetFilaSwitch() is a raw non-null accessor (BBS returns a shared_ptr).
DevFilaSwitch* fila_switch = obj->GetFilaSwitch();
if (fila_switch)
{
return {fila_switch->IsInstalled(), fila_switch->IsReady()};
}
return {false, false};
}
void AMSControl::show_switcher_status(bool show)
{
// Orca: never materialize the banner on printers that never request it, so the AMS control is
// byte-identical without a Filament Track Switch (UpdateAms calls this with show=false every refresh).
if (!show && tipPanel == nullptr) { return; }
if (tipPanel == nullptr)
{
m_sizer_body->Add(0, 0, 1, wxEXPAND | wxTOP, FromDIP(5));
tipPanel = new wxPanel(m_amswin);
tipPanel->SetBackgroundColour(wxColour(255, 153, 0));
tipSizer = new wxBoxSizer(wxHORIZONTAL);
tipPanel->SetSizer(tipSizer);
icon = new wxStaticBitmap(tipPanel, wxID_ANY,
wxArtProvider::GetBitmap(wxART_INFORMATION, wxART_MESSAGE_BOX, wxSize(FromDIP(16), FromDIP(16))));
tipSizer->Add(icon, 0, wxALL, FromDIP(8));
tipText = new wxStaticText(tipPanel, wxID_ANY, _L("AMS has not been initialized. Please initialize it before use."));
tipText->SetForegroundColour(wxColour(255, 255, 255));
tipText->SetFont(wxFont(10, wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD));
tipText->Wrap(-1);
tipText->SetMinSize(wxSize(-1, -1));
tipSizer->Add(tipText, 0, wxALL | wxALIGN_CENTER_VERTICAL | wxEXPAND, FromDIP(8));
m_sizer_body->Add(tipPanel, 1, wxEXPAND, 0);
}
if (tipPanel->IsShown() == show)
{
return;
}
tipPanel->Show(show);
m_amswin->Layout();
m_amswin->Fit();
}
void AMSControl::parse_object(MachineObject* obj) {
if (!obj || obj->GetFilaSystem()->GetAmsList().size() == 0)
{