mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-02 06:47:04 +00:00
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:
@@ -742,6 +742,63 @@ void AMSExtImage::doRender(wxDC& dc)
|
||||
}
|
||||
|
||||
|
||||
// Orca: SwitcherImage — routing glyph drawn when a Filament Track Switch is installed.
|
||||
void SwitcherImage::paintEvent(wxPaintEvent &evt)
|
||||
{
|
||||
wxPaintDC dc(this);
|
||||
render(dc);
|
||||
}
|
||||
|
||||
void SwitcherImage::render(wxDC &dc)
|
||||
{
|
||||
#ifdef __WXMSW__
|
||||
wxSize size = GetSize();
|
||||
wxMemoryDC memdc;
|
||||
wxBitmap bmp(size.x, size.y);
|
||||
memdc.SelectObject(bmp);
|
||||
memdc.Blit({0, 0}, size, &dc, {0, 0});
|
||||
|
||||
{
|
||||
wxGCDC dc2(memdc);
|
||||
doRender(dc2);
|
||||
}
|
||||
|
||||
memdc.SelectObject(wxNullBitmap);
|
||||
dc.DrawBitmap(bmp, 0, 0);
|
||||
#else
|
||||
doRender(dc);
|
||||
#endif
|
||||
}
|
||||
|
||||
void SwitcherImage::doRender(wxDC &dc)
|
||||
{
|
||||
auto size = GetSize();
|
||||
if (m_show_state){
|
||||
dc.SetPen(*wxTRANSPARENT_PEN);
|
||||
dc.SetBrush(*wxWHITE);
|
||||
dc.DrawBitmap(m_switcher.bmp(), wxPoint((size.x - m_switcher.GetBmpSize().x) / 2, 0));
|
||||
}
|
||||
Layout();
|
||||
}
|
||||
|
||||
SwitcherImage::SwitcherImage(wxWindow *parent, wxWindowID id, string file_name, const wxSize& size, const wxPoint &pos)
|
||||
{
|
||||
wxWindow::Create(parent, id, pos, size);
|
||||
SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE));
|
||||
m_show_state = true;
|
||||
m_switcher = ScalableBitmap(this, file_name, 16);
|
||||
m_file_name = file_name;
|
||||
SetSize(size);
|
||||
SetMinSize(size);
|
||||
SetMaxSize(size);
|
||||
|
||||
|
||||
Bind(wxEVT_PAINT, &SwitcherImage::paintEvent, this);
|
||||
}
|
||||
|
||||
SwitcherImage::~SwitcherImage() {}
|
||||
|
||||
|
||||
//DevAms Extruder
|
||||
AMSextruder::AMSextruder(wxWindow *parent, wxWindowID id, int nozzle_num, const wxPoint &pos, const wxSize &size)
|
||||
{
|
||||
@@ -3577,6 +3634,18 @@ void AmsItem::doRender(wxDC& dc)
|
||||
}
|
||||
}
|
||||
|
||||
// Orca: toggle the road segment below the item (m_panel_road). Used to drop the external-spool
|
||||
// road when a Filament Track Switch is installed. No-op (returns false) when already in state.
|
||||
bool AmsItem::ShowRoad(bool show)
|
||||
{
|
||||
if (!m_panel_road) return false;
|
||||
if (m_panel_road->IsShown() == show) return false;
|
||||
m_panel_road->Show(show);
|
||||
Layout();
|
||||
Refresh();
|
||||
return true;
|
||||
}
|
||||
|
||||
void AmsItem::RenderLiteRoad(wxDC& dc, wxSize size) {
|
||||
auto end_top = size.x - FromDIP(3);
|
||||
if (m_panel_pos == AMSPanelPos::RIGHT_PANEL){
|
||||
@@ -3706,4 +3775,342 @@ void AmsItem::show_sn_value(bool show)
|
||||
}
|
||||
}
|
||||
|
||||
DevExtruderImage::DevExtruderImage(wxWindow *parent, wxWindowID id, int extruder_num, const wxPoint &pos, const wxSize &size) : wxWindow(parent, id, pos, wxDefaultSize), m_extruder_num(extruder_num)
|
||||
{
|
||||
// wxWindow::Create(parent, id, pos, wxSize(FromDIP(45), FromDIP(112)));
|
||||
SetBackgroundColour(*wxWHITE);
|
||||
SetSize(wxSize(FromDIP(48), FromDIP(112)));
|
||||
SetMinSize(wxSize(FromDIP(48), FromDIP(112)));
|
||||
SetMaxSize(wxSize(FromDIP(48), FromDIP(112)));
|
||||
|
||||
|
||||
m_left_extruder_active_filled = new ScalableBitmap(this, "left_extruder_active_filled", 62);
|
||||
m_left_extruder_active_empty = new ScalableBitmap(this, "left_extruder_active_empty", 62);
|
||||
m_left_extruder_unactive_filled = new ScalableBitmap(this, "left_extruder_unactive_filled", 62);
|
||||
m_left_extruder_unactive_empty = new ScalableBitmap(this, "left_extruder_unactive_empty", 62);
|
||||
m_right_extruder_active_filled = new ScalableBitmap(this, "right_extruder_active_filled", 62);
|
||||
m_right_extruder_active_empty = new ScalableBitmap(this, "right_extruder_active_empty", 62);
|
||||
m_right_extruder_unactive_filled = new ScalableBitmap(this, "right_extruder_unactive_filled", 62);
|
||||
m_right_extruder_unactive_empty = new ScalableBitmap(this, "right_extruder_unactive_empty", 62);
|
||||
|
||||
m_extruder_single_nozzle_empty_load = new ScalableBitmap(this, "monitor_extruder_empty_load", 106);
|
||||
m_extruder_single_nozzle_empty_unload = new ScalableBitmap(this, "monitor_extruder_empty_unload", 106);
|
||||
m_extruder_single_nozzle_filled_load = new ScalableBitmap(this, "monitor_extruder_filled_load", 106);
|
||||
m_extruder_single_nozzle_filled_unload = new ScalableBitmap(this, "monitor_extruder_filled_unload", 106);
|
||||
|
||||
Bind(wxEVT_PAINT, &DevExtruderImage::paintEvent, this);
|
||||
}
|
||||
|
||||
void DevExtruderImage::msw_rescale()
|
||||
{
|
||||
m_left_extruder_active_filled->msw_rescale();
|
||||
m_left_extruder_active_empty->msw_rescale();
|
||||
m_left_extruder_unactive_filled->msw_rescale();
|
||||
m_left_extruder_unactive_empty->msw_rescale();
|
||||
m_right_extruder_active_filled->msw_rescale();
|
||||
m_right_extruder_active_empty->msw_rescale();
|
||||
m_right_extruder_unactive_filled->msw_rescale();
|
||||
m_right_extruder_unactive_empty->msw_rescale();
|
||||
|
||||
m_extruder_single_nozzle_empty_load->msw_rescale();
|
||||
m_extruder_single_nozzle_empty_unload->msw_rescale();
|
||||
m_extruder_single_nozzle_filled_load->msw_rescale();
|
||||
m_extruder_single_nozzle_filled_unload->msw_rescale();
|
||||
Layout();
|
||||
Refresh();
|
||||
}
|
||||
|
||||
void DevExtruderImage::render(wxDC &dc)
|
||||
{
|
||||
#ifdef __WXMSW__
|
||||
wxSize size = GetSize();
|
||||
wxMemoryDC memdc;
|
||||
wxBitmap bmp(size.x, size.y);
|
||||
memdc.SelectObject(bmp);
|
||||
memdc.Blit({0, 0}, size, &dc, {0, 0});
|
||||
|
||||
{
|
||||
wxGCDC dc2(memdc);
|
||||
doRender(dc2);
|
||||
}
|
||||
|
||||
memdc.SelectObject(wxNullBitmap);
|
||||
dc.DrawBitmap(bmp, 0, 0);
|
||||
#else
|
||||
doRender(dc);
|
||||
#endif
|
||||
}
|
||||
|
||||
void DevExtruderImage::doRender(wxDC &dc)
|
||||
{
|
||||
auto size = GetSize();
|
||||
auto pot = wxPoint(size.x / 2, (size.y - m_left_extruder_active_filled->GetBmpSize().y) / 2);
|
||||
|
||||
if (m_extruder_num >= 2)
|
||||
{
|
||||
ScalableBitmap *left_extruder_bmp{nullptr};
|
||||
ScalableBitmap *right_extruder_bmp{nullptr};
|
||||
|
||||
switch (m_right_ext_state)
|
||||
{
|
||||
case DevExtruderState::FILLED_LOAD:
|
||||
right_extruder_bmp = current_extruder_loc == "right" ? m_right_extruder_active_filled : m_right_extruder_unactive_filled;
|
||||
break;
|
||||
case DevExtruderState::FILLED_UNLOAD:
|
||||
right_extruder_bmp = current_extruder_loc == "right" ? m_right_extruder_active_filled : m_right_extruder_unactive_filled;
|
||||
break;
|
||||
case DevExtruderState::EMPTY_LOAD:
|
||||
if (current_extruder_loc.empty())
|
||||
{
|
||||
right_extruder_bmp = m_right_extruder_active_empty;
|
||||
}
|
||||
else
|
||||
{
|
||||
right_extruder_bmp = current_extruder_loc == "right" ? m_right_extruder_active_empty : m_right_extruder_unactive_empty;
|
||||
}
|
||||
break;
|
||||
case DevExtruderState::EMPTY_UNLOAD:
|
||||
right_extruder_bmp = current_extruder_loc == "right" ? m_right_extruder_active_empty : m_right_extruder_unactive_empty;
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
|
||||
switch (m_left_ext_state)
|
||||
{
|
||||
case DevExtruderState::FILLED_LOAD:
|
||||
left_extruder_bmp = current_extruder_loc == "left" ? m_left_extruder_active_filled : m_left_extruder_unactive_filled;
|
||||
break;
|
||||
case DevExtruderState::FILLED_UNLOAD:
|
||||
left_extruder_bmp = current_extruder_loc == "left" ? m_left_extruder_active_filled : m_left_extruder_unactive_filled;
|
||||
break;
|
||||
case DevExtruderState::EMPTY_LOAD:
|
||||
if (current_extruder_loc.empty())
|
||||
{
|
||||
left_extruder_bmp = m_left_extruder_active_empty;
|
||||
}
|
||||
else
|
||||
{
|
||||
left_extruder_bmp = current_extruder_loc == "left" ? m_left_extruder_active_empty : m_left_extruder_unactive_empty;
|
||||
}
|
||||
break;
|
||||
case DevExtruderState::EMPTY_UNLOAD:
|
||||
left_extruder_bmp = current_extruder_loc == "left" ? m_left_extruder_active_empty : m_left_extruder_unactive_empty;
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
|
||||
if (left_extruder_bmp) { dc.DrawBitmap(left_extruder_bmp->bmp(), pot.x - left_extruder_bmp->GetBmpWidth(), pot.y); }
|
||||
if (right_extruder_bmp) { dc.DrawBitmap(right_extruder_bmp->bmp(), pot.x, pot.y); }
|
||||
}
|
||||
else
|
||||
{
|
||||
ScalableBitmap *extruder_bmp = nullptr;
|
||||
switch (m_single_ext_state)
|
||||
{
|
||||
case DevExtruderState::FILLED_LOAD:
|
||||
extruder_bmp = m_extruder_single_nozzle_filled_load;
|
||||
break;
|
||||
case DevExtruderState::FILLED_UNLOAD:
|
||||
extruder_bmp = m_extruder_single_nozzle_filled_unload;
|
||||
break;
|
||||
case DevExtruderState::EMPTY_LOAD:
|
||||
extruder_bmp = m_extruder_single_nozzle_empty_load;
|
||||
break;
|
||||
case DevExtruderState::EMPTY_UNLOAD:
|
||||
extruder_bmp = m_extruder_single_nozzle_empty_unload;
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
|
||||
if (extruder_bmp) { dc.DrawBitmap(extruder_bmp->bmp(), pot.x - extruder_bmp->GetBmpWidth() / 2, (size.y - extruder_bmp->GetBmpHeight()) / 2); }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
FeedDirectionDialog::FeedDirectionDialog(wxWindow* parent,
|
||||
const int extruderNum,
|
||||
const std::string& printer_type)
|
||||
: wxDialog(parent, wxID_ANY, "", wxDefaultPosition, wxDefaultSize),
|
||||
m_extruder_num(extruderNum),
|
||||
m_printer_type(printer_type)
|
||||
{
|
||||
SetBackgroundColour(wxColour("#FFFFFF"));
|
||||
SetMaxSize(wxSize(FromDIP(360), FromDIP(207)));
|
||||
SetMinSize(wxSize(FromDIP(360), FromDIP(207)));
|
||||
SetSize(wxSize(FromDIP(360), FromDIP(207)));
|
||||
|
||||
wxBoxSizer* mainSizer = new wxBoxSizer(wxVERTICAL);
|
||||
|
||||
wxGridSizer* topSizer = new wxGridSizer (1, 3, FromDIP(5), 0);
|
||||
|
||||
m_radioHelper = new wxRadioButton(this, wxID_ANY, wxT(""), wxDefaultPosition, wxDefaultSize, wxRB_GROUP);
|
||||
m_leftRadio = new wxRadioButton(this, wxID_ANY,
|
||||
_L(DevPrinterConfigUtil::get_toolhead_display_name(m_printer_type, DEPUTY_EXTRUDER_ID, ToolHeadComponent::Extruder, ToolHeadNameCase::TitleCase, true)));
|
||||
m_rightRadio = new wxRadioButton(this, wxID_ANY,
|
||||
_L(DevPrinterConfigUtil::get_toolhead_display_name(m_printer_type, MAIN_EXTRUDER_ID, ToolHeadComponent::Extruder, ToolHeadNameCase::TitleCase, true)));
|
||||
m_radioHelper->Show(false);
|
||||
m_radioHelper->SetCanFocus(false);
|
||||
|
||||
m_leftRadio->SetForegroundColour(*wxBLACK);
|
||||
m_rightRadio->SetForegroundColour(*wxBLACK);
|
||||
|
||||
topSizer->Add(m_leftRadio, 0, wxALIGN_CENTER | wxALL, FromDIP(20));
|
||||
m_extruderImage = new DevExtruderImage(this, wxID_ANY, m_extruder_num);
|
||||
topSizer->Add(m_extruderImage, 0, wxALIGN_CENTER | wxALL, FromDIP(5));
|
||||
topSizer->Add(m_rightRadio, 0, wxALIGN_CENTER | wxALL, FromDIP(20));
|
||||
|
||||
mainSizer->AddStretchSpacer(1);
|
||||
mainSizer->Add(topSizer, 1, wxEXPAND);
|
||||
mainSizer->AddStretchSpacer(1);
|
||||
|
||||
wxBoxSizer* bottomSizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
m_confirmBtn = new Button(this, _L("Confirm"));
|
||||
m_confirmBtn->SetSize(wxSize(FromDIP(80), FromDIP(32)));
|
||||
m_confirmBtn->Enable(false);
|
||||
m_confirmBtn->SetFont(::Label::Body_14);
|
||||
bottomSizer->Add(m_confirmBtn, 0, wxALIGN_RIGHT);
|
||||
|
||||
mainSizer->Add(bottomSizer, 0, wxALIGN_RIGHT | wxBOTTOM | wxRIGHT, FromDIP(10));
|
||||
|
||||
SetSizer(mainSizer);
|
||||
Layout();
|
||||
Centre(wxBOTH);
|
||||
|
||||
m_lastChecked = m_radioHelper;
|
||||
m_confirmBtn->Bind(wxEVT_BUTTON, &FeedDirectionDialog::OnConfirm, this);
|
||||
m_leftRadio->Bind(wxEVT_RADIOBUTTON, &FeedDirectionDialog::OnRadioClicked, this);
|
||||
m_rightRadio->Bind(wxEVT_RADIOBUTTON, &FeedDirectionDialog::OnRadioClicked, this);
|
||||
}
|
||||
|
||||
void FeedDirectionDialog::OnConfirm(wxCommandEvent& event)
|
||||
{
|
||||
EndModal(wxID_OK);
|
||||
}
|
||||
|
||||
void FeedDirectionDialog::OnRadioClicked(wxCommandEvent& evt)
|
||||
{
|
||||
auto clicked = static_cast<wxRadioButton*>(evt.GetEventObject());
|
||||
m_load_extruder_id = std::nullopt;
|
||||
if (clicked == m_lastChecked)
|
||||
{
|
||||
m_radioHelper->SetValue(true);
|
||||
m_lastChecked = m_radioHelper;
|
||||
m_confirmBtn->Enable(false);
|
||||
m_extruderImage->update(DevExtruderState::EMPTY_LOAD, DevExtruderState::EMPTY_LOAD);
|
||||
m_extruderImage->setExtruderUsed("");
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
clicked->SetValue(true);
|
||||
m_lastChecked = clicked;
|
||||
m_confirmBtn->Enable(true);
|
||||
|
||||
if (clicked == m_leftRadio)
|
||||
{
|
||||
m_extruderImage->update(DevExtruderState::FILLED_LOAD, DevExtruderState::EMPTY_LOAD);
|
||||
m_extruderImage->setExtruderUsed("left");
|
||||
m_load_extruder_id = 1;
|
||||
{
|
||||
SetTitle(wxString::Format(_L("Load %s to ") + _L(DevPrinterConfigUtil::get_toolhead_display_name(m_printer_type, DEPUTY_EXTRUDER_ID, ToolHeadComponent::Extruder, ToolHeadNameCase::LowerCase)), m_filament_id));
|
||||
}
|
||||
}
|
||||
else if (clicked == m_rightRadio)
|
||||
{
|
||||
m_extruderImage->update(DevExtruderState::EMPTY_LOAD, DevExtruderState::FILLED_LOAD);
|
||||
m_extruderImage->setExtruderUsed("right");
|
||||
m_load_extruder_id = 0;
|
||||
{
|
||||
SetTitle(wxString::Format(_L("Load %s to ") + _L(DevPrinterConfigUtil::get_toolhead_display_name(m_printer_type, MAIN_EXTRUDER_ID, ToolHeadComponent::Extruder, ToolHeadNameCase::LowerCase)), m_filament_id));
|
||||
}
|
||||
}
|
||||
}
|
||||
m_leftRadio->Refresh();
|
||||
m_rightRadio->Refresh();
|
||||
Fit();
|
||||
Update();
|
||||
}
|
||||
|
||||
std::optional<int> FeedDirectionDialog::GetExtruderID()
|
||||
{
|
||||
if (m_extruderImage)
|
||||
{
|
||||
return m_load_extruder_id;
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
wxString FeedDirectionDialog::calcTrayName(MachineObject* obj, const std::string& amsID, const std::string& slotID)
|
||||
{
|
||||
if (amsID.empty() || slotID.empty() || !obj)
|
||||
return wxString();
|
||||
|
||||
auto filaSys = obj->GetFilaSystem();
|
||||
if (!filaSys)
|
||||
return wxString();
|
||||
|
||||
DevAms* ams = filaSys->GetAmsById(amsID);
|
||||
if (!ams)
|
||||
return wxString();
|
||||
|
||||
int ams_id_int = std::stoi(amsID);
|
||||
int slot_id_int = std::stoi(slotID);
|
||||
int tray_id = 0;
|
||||
|
||||
// Orca: DevAms::AmsType is the local enum (AMS / AMS_LITE / N3F / N3S); it has no EXT_SPOOL
|
||||
// member, and external/virtual spools are not real DevAms objects (GetAmsById returns nullptr
|
||||
// above), so the EXT_SPOOL branch from upstream is unnecessary here.
|
||||
if (ams->GetAmsType() == DevAms::AMS || ams->GetAmsType() == DevAms::AMS_LITE || ams->GetAmsType() == DevAms::N3F) {
|
||||
tray_id = ams_id_int * 4 + slot_id_int;
|
||||
} else if (ams->GetAmsType() == DevAms::N3S) {
|
||||
tray_id = ams_id_int + slot_id_int;
|
||||
} else {
|
||||
return wxString();
|
||||
}
|
||||
|
||||
return wxGetApp().transition_tridid(tray_id);
|
||||
}
|
||||
|
||||
void FeedDirectionDialog::SetExtruderMapping(MachineObject* obj,
|
||||
const std::string& currAmsId,
|
||||
const std::string& currSlotId,
|
||||
const std::vector<std::pair<std::string, std::string>>& extruderSlots)
|
||||
{
|
||||
wxString filamentID = calcTrayName(obj, currAmsId, currSlotId);
|
||||
if (filamentID.empty())
|
||||
return;
|
||||
|
||||
m_filament_id = filamentID;
|
||||
SetTitle(wxString::Format(_L("Load %s to "), filamentID));
|
||||
|
||||
std::vector<wxString> extruderMapping(extruderSlots.size());
|
||||
for (size_t i = 0; i < extruderSlots.size(); ++i) {
|
||||
if (!extruderSlots[i].first.empty())
|
||||
extruderMapping[i] = calcTrayName(obj, extruderSlots[i].first, extruderSlots[i].second);
|
||||
}
|
||||
|
||||
if (extruderMapping.size() > 1 && extruderMapping[1] == filamentID) //left extruder
|
||||
{
|
||||
m_leftRadio->Enable(false);
|
||||
m_confirmBtn->Enable(false);
|
||||
m_extruderImage->update(DevExtruderState::EMPTY_LOAD, DevExtruderState::EMPTY_LOAD);
|
||||
m_extruderImage->setExtruderUsed("right");
|
||||
}
|
||||
else if (!extruderMapping.empty() && extruderMapping[0] == filamentID) //right extruder
|
||||
{
|
||||
m_rightRadio->Enable(false);
|
||||
m_confirmBtn->Enable(false);
|
||||
m_extruderImage->update(DevExtruderState::EMPTY_LOAD, DevExtruderState::EMPTY_LOAD);
|
||||
m_extruderImage->setExtruderUsed("left");
|
||||
}
|
||||
else
|
||||
{
|
||||
m_radioHelper->SetValue(true);
|
||||
m_lastChecked = m_radioHelper;
|
||||
m_confirmBtn->Enable(false);
|
||||
m_extruderImage->update(DevExtruderState::EMPTY_LOAD, DevExtruderState::EMPTY_LOAD);
|
||||
m_extruderImage->setExtruderUsed("");
|
||||
}
|
||||
}
|
||||
|
||||
}} // namespace Slic3r::GUI
|
||||
|
||||
Reference in New Issue
Block a user