#include "AMSDryControl.hpp" #include "slic3r/GUI/DeviceCore/DevFilaSystem.h" #include "GUI_App.hpp" #include "I18N.hpp" #include "slic3r/GUI/DeviceCore/DevExtruderSystem.h" #include "slic3r/GUI/DeviceCore/DevManager.h" #include "slic3r/GUI/MsgDialog.hpp" #include "slic3r/GUI/Widgets/AnimaController.hpp" #include "slic3r/GUI/Widgets/Label.hpp" #include "slic3r/GUI/Widgets/ComboBox.hpp" #include "slic3r/GUI/Widgets/ProgressBar.hpp" #include "slic3r/GUI/DeviceCore/DevUtilBackend.h" #include "libslic3r/PresetBundle.hpp" #include "libslic3r/Preset.hpp" #include namespace Slic3r { namespace GUI { const int AMS_DRY_STATUS_IMAGE_SIZE = 96; static std::string get_humidity_level_img_path(int humidity_percent) { const int num_levels = 5; int hum_level; if (humidity_percent <= 100 / num_levels) { hum_level = 5; } else if (humidity_percent <= 100 / num_levels * 2) { hum_level = 4; } else if (humidity_percent <= 100 / num_levels * 3) { hum_level = 3; } else if (humidity_percent <= 100 / num_levels * 4) { hum_level = 2; } else { hum_level = 1; } if (wxGetApp().dark_mode()) { return "hum_level" + std::to_string(hum_level) + "_no_num_light"; } else { return "hum_level" + std::to_string(hum_level) + "_no_num_light"; } } static std::string get_dry_status_img_path(DevAmsType type, DevAms::DryStatus status, DevAms::DrySubStatus sub_status) { std::string img_name = "dev_ams_dry_ctr_"; switch (type) { case DevAmsType::N3S: img_name += "n3s"; break; case DevAmsType::N3F: img_name += "n3f"; break; default: return ""; } img_name += "_"; switch (status) { case DevAms::DryStatus::Error: img_name += "error"; return img_name; } switch (sub_status) { case DevAms::DrySubStatus::Heating: img_name += "heating"; return img_name; case DevAms::DrySubStatus::Dehumidify: img_name += "dehumidifying"; return img_name; } return ""; } FilamentItemPanel::FilamentItemPanel(wxWindow* parent, const wxString& text, const std::string& icon_name, wxWindowID id) : wxPanel(parent, id) , m_icon_name(icon_name) { SetBackgroundColour(wxColour("#F7F7F7")); // Light gray background SetMinSize(wxSize(FromDIP(64), FromDIP(106))); // Width: 64, Height: 106 SetSize(wxSize(FromDIP(64), FromDIP(106))); // Fixed size // Create sizer for vertical layout wxBoxSizer* sizer = new wxBoxSizer(wxVERTICAL); // Top section with text - moved to be closer to center wxBoxSizer* top_sizer = new wxBoxSizer(wxVERTICAL); top_sizer->AddStretchSpacer(5); m_text_label = new Label(this, text); m_text_label->SetForegroundColour(StateColor::darkModeColorFor(wxColour(*wxBLACK))); m_text_label->SetBackgroundColour(StateColor::darkModeColorFor(wxColour("#F7F7F7"))); m_text_label->SetFont(Label::Body_12); m_text_label->Wrap(FromDIP(40)); top_sizer->Add(m_text_label, 0, wxALIGN_CENTER_HORIZONTAL); top_sizer->AddStretchSpacer(1); // Increased bottom spacer to push text down sizer->Add(top_sizer, 1, wxEXPAND); // Bottom section with icon - vertically centered in bottom half wxBoxSizer* bottom_sizer = new wxBoxSizer(wxVERTICAL); bottom_sizer->AddStretchSpacer(1); m_icon_bitmap = new wxStaticBitmap(this, wxID_ANY, wxNullBitmap); m_icon_bitmap->SetBackgroundColour(StateColor::darkModeColorFor(wxColour("#F7F7F7"))); m_icon_bitmap->SetMinSize(wxSize(FromDIP(24), FromDIP(24))); m_icon_bitmap->SetMaxSize(wxSize(FromDIP(24), FromDIP(24))); bottom_sizer->Add(m_icon_bitmap, 0, wxALIGN_CENTER_HORIZONTAL); bottom_sizer->AddStretchSpacer(1); sizer->Add(bottom_sizer, 1, wxEXPAND); SetSizer(sizer); // Bind events Bind(wxEVT_PAINT, &FilamentItemPanel::OnPaint, this); Bind(wxEVT_SIZE, &FilamentItemPanel::OnSize, this); SetIcon(icon_name); wxGetApp().UpdateDarkUI(this); } void FilamentItemPanel::SetText(const wxString& text) { m_text_label->SetLabel(text); Layout(); } void FilamentItemPanel::SetIcon(const std::string& icon_name) { m_icon_name = icon_name; if (!icon_name.empty()) { try { m_icon = ScalableBitmap(this, icon_name, 20); m_icon.msw_rescale(); m_icon_bitmap->SetBitmap(m_icon.bmp()); } catch (Exception&) { BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": failed to load filament icon"; m_icon_bitmap->SetBitmap(wxNullBitmap); } } else { m_icon_bitmap->SetBitmap(wxNullBitmap); } m_icon_bitmap->Refresh(); } void FilamentItemPanel::msw_rescale() { if (m_icon_bitmap && !m_icon_name.empty()) { m_icon.msw_rescale(); m_icon_bitmap->SetBitmap(m_icon.bmp()); m_icon_bitmap->Refresh(); } Layout(); } void FilamentItemPanel::OnPaint(wxPaintEvent& event) { wxPaintDC dc(this); wxSize size = GetSize(); // bool is_dark_mode = wxGetApp().dark_mode(); wxColour backgroundColor = StateColor::darkModeColorFor(wxColour("#F7F7F7")); wxColour borderColor = StateColor::darkModeColorFor(wxColour("#DBDBDB")); // Draw white background rectangle with rounded corners inside the thick vertical lines dc.SetBrush(wxBrush(backgroundColor)); dc.SetPen(wxPen(backgroundColor)); dc.DrawRectangle(FromDIP(6), FromDIP(12), size.GetWidth() - FromDIP(12), size.GetHeight() - 2 * FromDIP(12)); // Draw much thicker vertical rounded rectangles on left and right (3 times thicker) dc.SetBrush(wxBrush(wxColour(borderColor))); dc.SetPen(wxPen(wxColour(borderColor))); dc.DrawRoundedRectangle(FromDIP(0), FromDIP(0), FromDIP(6), size.GetHeight(), FromDIP(6)); // Left rounded rectangle dc.DrawRoundedRectangle(size.GetWidth() - FromDIP(6), FromDIP(0), FromDIP(6), size.GetHeight(), FromDIP(6)); // Right rounded rectangle // Draw thin horizontal rounded rectangles on top and bottom (moved closer to center by half the distance) dc.SetBrush(wxBrush(wxColour(borderColor))); dc.SetPen(wxPen(wxColour(borderColor))); dc.DrawRoundedRectangle(FromDIP(6), FromDIP(12), size.GetWidth() - FromDIP(12), FromDIP(3), FromDIP(1)); // Top rounded rectangle dc.DrawRoundedRectangle(FromDIP(6), size.GetHeight() - FromDIP(13), size.GetWidth() - FromDIP(12), FromDIP(3), FromDIP(1)); // Bottom rounded rectangle } void FilamentItemPanel::OnSize(wxSizeEvent& event) { Refresh(); event.Skip(); } // class AMSFilamentPanel AMSFilamentPanel::AMSFilamentPanel(wxWindow* parent, const wxString& ams_name, wxWindowID id) : wxPanel(parent, id) { SetBackgroundColour(wxColour("#DBDBDB")); wxBoxSizer* main_sizer = new wxBoxSizer(wxVERTICAL); // Filament items section m_filament_container = new wxPanel(this); m_filament_container->SetBackgroundColour(wxColour("#F7F7F7")); m_filament_sizer = new wxBoxSizer(wxHORIZONTAL); m_filament_container->SetSizer(m_filament_sizer); // AMS name section m_ams_name_label = new Label(this, ams_name); m_ams_name_label->SetForegroundColour(wxColour("#858585")); m_ams_name_label->SetFont(Label::Body_14); m_ams_name_label->SetBackgroundColour(wxColour("#DBDBDB")); main_sizer->Add(m_filament_container, 1, wxEXPAND | wxALL, 0); main_sizer->Add(m_ams_name_label, 0, wxALIGN_LEFT | wxALL, FromDIP(5)); SetSizer(main_sizer); wxGetApp().UpdateDarkUI(this); } void AMSFilamentPanel::AddFilamentItem(FilamentItemPanel* panel) { if (panel) { panel->Reparent(m_filament_container); m_filament_sizer->Add(panel, 0, wxALL, FromDIP(5)); m_filament_items.push_back(panel); Layout(); } } void AMSFilamentPanel::AddFilamentItem(const wxString& text, const std::string& icon_name) { FilamentItemPanel* item = new FilamentItemPanel(m_filament_container, text, icon_name); m_filament_items.push_back(item); m_filament_sizer->Add(item, 0, wxALL, FromDIP(5)); Layout(); } void AMSFilamentPanel::SetAmsName(const wxString& ams_name) { m_ams_name_label->SetLabel(ams_name); Layout(); } void AMSFilamentPanel::Clear() { m_filament_sizer->Clear(true); m_filament_items.clear(); } void AMSFilamentPanel::msw_rescale() { for (auto item : m_filament_items) { item->msw_rescale(); } Layout(); } AMSDryCtrWin::AMSDryCtrWin(wxWindow *parent) :DPIDialog(parent, wxID_ANY, _L("AMS Dryness Control"), wxDefaultPosition, wxDefaultSize, wxCAPTION | wxCLOSE_BOX) { create(); } AMSDryCtrWin::~AMSDryCtrWin() { if (m_progress_timer) { delete m_progress_timer; } } wxScrolledWindow* AMSDryCtrWin::create_preview_scrolled_window(wxWindow* parent) { wxScrolledWindow* panel = new wxScrolledWindow(parent, wxID_ANY); panel->SetScrollRate(10, 0); panel->SetSize(AMS_ITEMS_PANEL_SIZE); panel->SetMinSize(AMS_ITEMS_PANEL_SIZE); panel->SetBackgroundColour(AMS_CONTROL_DEF_BLOCK_BK_COLOUR); return panel; } wxBoxSizer* AMSDryCtrWin::create_humidity_status_section(wxPanel* parent) { wxBoxSizer* image_sizer = new wxBoxSizer(wxVERTICAL); m_humidity_img = new wxStaticBitmap(parent, wxID_ANY, wxNullBitmap); image_sizer->Add(m_humidity_img, 1, wxALIGN_CENTER_HORIZONTAL, 0); m_humidity_img->SetBitmap(wxNullBitmap); // Create a horizontal sizer for description and icon wxBoxSizer* desc_sizer = new wxBoxSizer(wxHORIZONTAL); m_image_description_icon = new wxStaticBitmap(parent, wxID_ANY, wxNullBitmap); m_image_description_icon->SetBackgroundColour(*wxWHITE); m_image_description_icon->SetMinSize(wxSize(FromDIP(20), FromDIP(20))); m_image_description_icon->SetMaxSize(wxSize(FromDIP(20), FromDIP(20))); m_image_description_icon->Show(false); desc_sizer->Add(m_image_description_icon, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(5)); m_image_description = new Label(parent, _L("Idle")); m_image_description->SetFont(Label::Head_14); m_image_description->SetForegroundColour(*wxBLACK); m_image_description->SetBackgroundColour(*wxWHITE); desc_sizer->Add(m_image_description, 0, wxALIGN_CENTER_VERTICAL); image_sizer->Add(desc_sizer, 0, wxALIGN_CENTER | wxALL, FromDIP(5)); return image_sizer; } wxBoxSizer* AMSDryCtrWin::create_description_item(wxPanel* parent, const wxString& title, Label*& dataLabel) { wxBoxSizer* item_sizer = new wxBoxSizer(wxVERTICAL); Label* titleLabel = new Label(parent, title); titleLabel->SetForegroundColour(*wxBLACK); titleLabel->SetFont(Label::Body_16); dataLabel = new Label(parent, wxT("--")); dataLabel->SetForegroundColour(*wxBLACK); dataLabel->SetFont(Label::Body_14); item_sizer->AddStretchSpacer(); item_sizer->Add(titleLabel, 0, wxALIGN_CENTER | wxALL, FromDIP(2)); item_sizer->Add(dataLabel, 0, wxALIGN_CENTER | wxALL, FromDIP(2)); item_sizer->AddStretchSpacer(); return item_sizer; } wxBoxSizer* AMSDryCtrWin::create_status_descriptions_section(wxPanel* parent) { wxBoxSizer* desc_sizer = new wxBoxSizer(wxHORIZONTAL); wxBoxSizer* hum_desc_sizer = create_description_item(parent, _L("Humidity"), m_humidity_data_label); desc_sizer->Add(hum_desc_sizer, 1, wxEXPAND, FromDIP(1)); wxStaticLine* vert_separator = new wxStaticLine(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_VERTICAL); desc_sizer->Add(vert_separator, 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(5)); wxBoxSizer* temp_desc_sizer = create_description_item(parent, _L("Temperature"), m_temperature_data_label); desc_sizer->Add(temp_desc_sizer, 1, wxEXPAND, FromDIP(1)); m_time_descrition_container = new wxBoxSizer(wxHORIZONTAL); wxStaticLine* vert_separator2 = new wxStaticLine(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_VERTICAL); m_time_descrition_container->Add(vert_separator2, 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(5)); wxBoxSizer* time_desc_sizer = create_description_item(parent, _L("Left Time"), m_time_data_label); m_time_descrition_container->Add(time_desc_sizer, 1, wxEXPAND, FromDIP(1)); desc_sizer->Add(m_time_descrition_container, 1, wxEXPAND, 0); // m_time_descrition_container->Show(false); return desc_sizer; } wxBoxSizer* AMSDryCtrWin::create_left_panel(wxPanel* parent) { wxBoxSizer* left_sizer = new wxBoxSizer(wxVERTICAL); wxBoxSizer* image_section = create_humidity_status_section(parent); left_sizer->Add(image_section, 1, wxEXPAND | wxALL, FromDIP(5)); wxBoxSizer* descriptions_section = create_status_descriptions_section(parent); left_sizer->Add(descriptions_section, 0, wxEXPAND | wxALL, FromDIP(20)); return left_sizer; } Button* AMSDryCtrWin::create_button(wxPanel* parent, const wxString& title, const wxColour& background_color, const wxColour& border_color, const wxColour& text_color) { Button* button = new Button(parent, title); // Create state colors for background StateColor bg_color( std::pair(AMS_CONTROL_DISABLE_COLOUR, StateColor::Disabled), std::pair(background_color.ChangeLightness(80), StateColor::Pressed), std::pair(background_color.ChangeLightness(120), StateColor::Hovered), std::pair(background_color, StateColor::Normal) ); // Create state colors for border StateColor bd_color( std::pair(AMS_CONTROL_WHITE_COLOUR, StateColor::Disabled), std::pair(border_color, StateColor::Enabled) ); button->SetBackgroundColor(bg_color); button->SetBorderColor(bd_color); button->SetTextColor(text_color); button->SetFont(Label::Body_14); // Auto-size button based on text content with padding wxSize best_size = button->GetBestSize(); int padding_width = FromDIP(4); int padding_height = FromDIP(2); wxSize final_size(best_size.GetWidth() + padding_width, best_size.GetHeight() + padding_height); button->SetMinSize(final_size); return button; } wxBoxSizer* AMSDryCtrWin::create_normal_state_panel(wxPanel* parent) { wxBoxSizer* normal_state_sizer = new wxBoxSizer(wxVERTICAL); Label* description_label = new Label(parent, _L("Filament Drying Settings")); description_label->SetForegroundColour(*wxBLACK); description_label->SetFont(Label::Head_14); normal_state_sizer->Add(description_label, 0, wxALL, FromDIP(5)); // Part 2: ComboBox for material selection wxBoxSizer* combo_sizer = new wxBoxSizer(wxHORIZONTAL); m_trays_combo = new ComboBox(parent, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize(-1, FromDIP(26)), 0, nullptr, wxCB_READONLY); m_trays_combo->SetFont(Label::Body_14); combo_sizer->Add(m_trays_combo, 1, wxEXPAND); normal_state_sizer->Add(combo_sizer, 0, wxEXPAND | wxALL, FromDIP(5)); m_trays_combo->Bind(wxEVT_COMBOBOX, &AMSDryCtrWin::OnFilamentSelectionChanged, this); // part 3 time and temperature // Temperature part wxBoxSizer* temp_sizer = new wxBoxSizer(wxHORIZONTAL); m_temperature_input = new wxTextCtrl(parent, wxID_ANY, "", wxDefaultPosition, wxSize(FromDIP(80), -1)); m_temperature_input->SetMaxLength(3); // Limit to 3 digits m_temperature_input->Bind(wxEVT_CHAR, [this](wxKeyEvent& event) { int keycode = event.GetKeyCode(); if (keycode >= '0' && keycode <= '9') { event.Skip(); } else if (keycode == WXK_BACK || keycode == WXK_DELETE || keycode == WXK_LEFT || keycode == WXK_RIGHT) { event.Skip(); } }); m_temperature_input->Bind(wxEVT_TEXT, [this](wxCommandEvent&) { m_next_button->Disable(); m_start_button->Disable(); }); m_temperature_input->SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE)); m_temperature_input->SetForegroundColour(StateColor::darkModeColorFor(*wxBLACK)); Label* temp_unit_label = new Label(parent, wxString::FromUTF8("℃")); temp_unit_label->SetForegroundColour(*wxBLACK); temp_sizer->Add(m_temperature_input, 1, wxRIGHT, FromDIP(1)); temp_sizer->Add(temp_unit_label, 0, wxALIGN_CENTER_VERTICAL); // Time part wxBoxSizer* time_sizer = new wxBoxSizer(wxHORIZONTAL); m_time_input = new wxTextCtrl(parent, wxID_ANY, "", wxDefaultPosition, wxSize(FromDIP(100), -1)); m_time_input->SetMaxLength(3); // Limit to 3 digits m_time_input->Bind(wxEVT_CHAR, [this](wxKeyEvent& event) { int keycode = event.GetKeyCode(); if (keycode >= '0' && keycode <= '9') { event.Skip(); } else if (keycode == WXK_BACK || keycode == WXK_DELETE || keycode == WXK_LEFT || keycode == WXK_RIGHT) { event.Skip(); } }); m_time_input->Bind(wxEVT_TEXT, [this](wxCommandEvent&) { m_next_button->Disable(); m_start_button->Disable(); }); m_time_input->SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE)); m_time_input->SetForegroundColour(StateColor::darkModeColorFor(*wxBLACK)); Label* time_unit_label = new Label(parent, "H"); time_unit_label->SetForegroundColour(*wxBLACK); time_sizer->Add(m_time_input, 1, wxRIGHT, FromDIP(1)); time_sizer->Add(time_unit_label, 0, wxALIGN_CENTER_VERTICAL); wxBoxSizer* input_sizer = new wxBoxSizer(wxHORIZONTAL); input_sizer->Add(temp_sizer, 1, wxRIGHT, FromDIP(10)); input_sizer->Add(time_sizer, 1, 0); normal_state_sizer->Add(input_sizer, 0, wxEXPAND | wxALL, FromDIP(5)); // Part 4: Abnormal description/message area m_normal_description = new Label(parent, ""); m_normal_description->SetForegroundColour(wxColour("#F09A17")); m_normal_description->SetFont(Label::Body_12); normal_state_sizer->Add(m_normal_description, 0, wxALL, FromDIP(5)); // Part 5: Start button m_next_button = create_button( parent, _L("Start"), AMS_CONTROL_BRAND_COLOUR, // Background color - green AMS_CONTROL_BRAND_COLOUR, // Border color - green wxColour("#FFFFFE") // Text color - white ); m_next_button->Bind(wxEVT_COMMAND_BUTTON_CLICKED, [this](wxCommandEvent& event) { m_main_simplebook->SetSelection(1); }); normal_state_sizer->Add(m_next_button, 0, wxALL, FromDIP(5)); m_next_button->Disable(); m_stop_button = create_button( parent, _L("Stop"), wxColour(255, 0, 0), // Background color - red wxColour(255, 0, 0), // Border color - red wxColour("#FFFFFE") // Text color - white ); m_stop_button->Bind(wxEVT_COMMAND_BUTTON_CLICKED, [this](wxCommandEvent& event) { auto fila_system = get_fila_system(); if (!fila_system) { BOOST_LOG_TRIVIAL(info) << "AMSDryCtrWin::start_sending_drying_command: Invalid FilaSystem Pointer"; return; } fila_system->CtrlAmsStopDrying(std::stoi(m_ams_info.m_ams_id)); // Temporarily show stopping state, then restore after 2 seconds if (m_stop_button) { m_stop_button->SetLabel(_L("Stopping")); update_button_size(m_stop_button); // Adjust button size for longer text m_stop_button->Disable(); m_stop_button->Layout(); m_stop_button->GetParent()->Layout(); // Relayout parent container m_stop_button->Refresh(); m_stop_button_restore_deadline.reset(); m_stop_button_restore_deadline = std::chrono::steady_clock::now() + std::chrono::seconds(2); } }); m_stop_button->Show(false); normal_state_sizer->Add(m_stop_button, 0, wxALL, FromDIP(5)); return normal_state_sizer; } wxBoxSizer* AMSDryCtrWin::create_cannot_dry_panel(wxPanel* parent) { wxBoxSizer* abnormal_sizer = new wxBoxSizer(wxVERTICAL); Label* description_label = new Label(parent, _L("Unable to dry temporarily due to ...")); description_label->SetForegroundColour(*wxBLACK); description_label->SetFont(Label::Head_16); description_label->Wrap(FromDIP(300)); abnormal_sizer->Add(description_label, 0, wxALIGN_CENTER_VERTICAL, FromDIP(10)); // Add a description label for the abnormal state m_cannot_dry_description_label = new Label(parent, ("")); m_cannot_dry_description_label->SetForegroundColour(*wxBLACK); m_cannot_dry_description_label->SetFont(Label::Body_14); m_cannot_dry_description_label->Wrap(FromDIP(250)); // Wrap text to fit within panel abnormal_sizer->Add(m_cannot_dry_description_label, 0, wxALIGN_CENTER_VERTICAL, FromDIP(10)); // Unload button shown only when ConsumableAtAmsOutlet reason is active m_unload_button = create_button( parent, _L("Unload"), AMS_CONTROL_BRAND_COLOUR, // Background color - light gray AMS_CONTROL_BRAND_COLOUR, // Border color - gray *wxWHITE // Text color - white ); m_unload_button->Bind(wxEVT_COMMAND_BUTTON_CLICKED, [this](wxCommandEvent& event) { auto fila_system = get_fila_system(); if (!fila_system) { BOOST_LOG_TRIVIAL(info) << "AMSDryCtrWin::unload_button: Invalid FilaSystem Pointer"; return; } MachineObject* obj = fila_system->GetOwner(); if (!obj) { BOOST_LOG_TRIVIAL(info) << "AMSDryCtrWin::unload_button: Invalid MachineObject Pointer"; return; } obj->command_ams_change_filament(false, m_ams_info.m_ams_id, "255"); m_unload_button->Disable(); m_unload_button_restore_deadline.reset(); m_unload_button_restore_deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5); }); m_unload_button->Show(false); abnormal_sizer->Add(m_unload_button, 0, wxALL, FromDIP(5)); return abnormal_sizer; } wxBoxSizer* AMSDryCtrWin::create_drying_error_panel(wxPanel* parent) { wxBoxSizer* err_sizer = new wxBoxSizer(wxVERTICAL); Label* description_label = new Label(parent, _L("Drying Error")); description_label->SetForegroundColour(*wxRED); description_label->SetFont(Label::Body_14); // Wrap text to fit within panel err_sizer->Add(description_label, 0, wxALIGN_CENTER_VERTICAL, FromDIP(10)); Label* additional_description_label = new Label(parent, _L("Please check the Assistant for troubleshooting")); additional_description_label->SetForegroundColour(*wxBLACK); additional_description_label->SetFont(Label::Body_14); // Wrap text to fit within panel err_sizer->Add(additional_description_label, 0, wxALIGN_CENTER_VERTICAL, FromDIP(10)); return err_sizer; } wxBoxSizer* AMSDryCtrWin::create_right_panel(wxPanel* parent) { wxBoxSizer* right_sizer = new wxBoxSizer(wxHORIZONTAL); m_normal_state_sizer = create_normal_state_panel(parent); m_cannot_dry_sizer = create_cannot_dry_panel(parent); m_dry_error_sizer = create_drying_error_panel(parent); right_sizer->Add(m_normal_state_sizer, 1, wxEXPAND); right_sizer->Add(m_cannot_dry_sizer, 1, wxEXPAND); m_cannot_dry_sizer->Show(false); m_dry_error_sizer->Show(false); return right_sizer; } wxBoxSizer* AMSDryCtrWin::create_main_content_section(wxPanel* parent) { wxBoxSizer* content_sizer = new wxBoxSizer(wxHORIZONTAL); wxBoxSizer* left_panel = create_left_panel(parent); content_sizer->Add(left_panel, 2, wxALIGN_CENTER_VERTICAL | wxALL, FromDIP(5)); left_panel->SetMinSize(wxSize(FromDIP(250), -1)); wxBoxSizer* right_panel = create_right_panel(parent); content_sizer->Add(right_panel, 1, wxALIGN_CENTER_VERTICAL | wxALL, FromDIP(5)); right_panel->SetMinSize(wxSize(FromDIP(250), -1)); return content_sizer; } wxBoxSizer* AMSDryCtrWin::create_main_page_sizer(wxPanel* parent) { wxBoxSizer* main_sizer = new wxBoxSizer(wxVERTICAL); wxBoxSizer* content_section = create_main_content_section(parent); main_sizer->Add(content_section, 1, wxEXPAND | wxALL, FromDIP(5)); return main_sizer; } wxBoxSizer* AMSDryCtrWin::create_guide_info_filament(wxPanel* parent) { wxBoxSizer* filament_section = new wxBoxSizer(wxHORIZONTAL); m_ams_filament_panel = new AMSFilamentPanel(parent, ""); filament_section->Add(m_ams_filament_panel, 0, wxEXPAND | wxALL | wxALIGN_CENTER_HORIZONTAL, FromDIP(5)); return filament_section; } wxBoxSizer* AMSDryCtrWin::create_guide_info_section(wxPanel* parent) { wxBoxSizer* info_section = new wxBoxSizer(wxVERTICAL); // Part 1: Title m_guide_title_label = new Label(parent, _L("Please remove and store the filament (as shown).")); m_guide_title_label->SetForegroundColour(*wxBLACK); m_guide_title_label->SetFont(Label::Head_18); info_section->Add(m_guide_title_label, 0, wxEXPAND | wxALL, FromDIP(5)); // Part 2: Description m_guide_description_label = new Label(parent, _L("The AMS can rotate the filament which is properly stored, providing better drying results.")); m_guide_description_label->SetForegroundColour(*wxBLACK); m_guide_description_label->SetFont(Label::Body_14); m_guide_description_label->Wrap(FromDIP(300)); // Wrap text to fit within panel info_section->Add(m_guide_description_label, 0, wxEXPAND | wxALL, FromDIP(5)); // Part 3: filament panel wxBoxSizer* fila_section = create_guide_info_filament(parent); info_section->Add(fila_section, 0, wxEXPAND | wxALL, FromDIP(5)); // Part 4: Circular toggle with description wxBoxSizer* toggle_section = new wxBoxSizer(wxHORIZONTAL); m_rotate_spool_toggle = new wxCheckBox(parent, wxID_ANY, ""); m_rotate_spool_toggle->SetValue(false); m_rotate_spool_toggle->Bind(wxEVT_CHECKBOX, [this](wxCommandEvent& event) { bool is_checked = event.IsChecked(); // Add toggle behavior logic here }); toggle_section->Add(m_rotate_spool_toggle, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(10)); // Toggle description Label* toggle_description = new Label(parent, _L("Rotate spool when drying"), LB_AUTO_WRAP); toggle_description->SetForegroundColour(*wxBLACK); toggle_description->SetFont(Label::Body_12); toggle_section->Add(toggle_description, 1, wxALIGN_CENTER_VERTICAL | wxEXPAND, 0); info_section->Add(toggle_section, 0, wxEXPAND | wxALL, FromDIP(5)); return info_section; } wxBoxSizer* AMSDryCtrWin::create_guide_right_section(wxPanel* parent) { wxBoxSizer* right_section = new wxBoxSizer(wxVERTICAL); // Upper part: Image section m_image_placeholder = new wxStaticBitmap(parent, wxID_ANY, wxNullBitmap); m_guide_image = ScalableBitmap(parent, "dev_ams_dry_ctr_filament_in_chamber", 256); m_image_placeholder->SetBitmap(m_guide_image.bmp()); wxBoxSizer* image_container = new wxBoxSizer(wxHORIZONTAL); image_container->AddStretchSpacer(1); image_container->Add(m_image_placeholder, 0, wxALL, FromDIP(5)); right_section->Add(image_container, 0, wxEXPAND | wxALL, FromDIP(5)); wxBoxSizer* buttons_container = new wxBoxSizer(wxHORIZONTAL); buttons_container->AddStretchSpacer(1); m_back_button = create_button( parent, wxString::FromUTF8(_CTX_utf8(L_CONTEXT("Back", "amsdrying"), "amsdrying")), wxColour("#F8F8F8"), // Background color - light gray wxColour("#D0D0D0"), // Border color - gray *wxBLACK // Text color - black ); m_back_button->Bind(wxEVT_COMMAND_BUTTON_CLICKED, [this](wxCommandEvent& event) { m_main_simplebook->SetSelection(0); }); buttons_container->Add(m_back_button, 0, wxALL, FromDIP(5)); m_start_button = create_button( parent, _L("Start"), AMS_CONTROL_BRAND_COLOUR, // Background color - green AMS_CONTROL_BRAND_COLOUR, // Border color - green wxColour("#FFFFFE") // Text color - white ); m_start_button->Bind(wxEVT_COMMAND_BUTTON_CLICKED, [this](wxCommandEvent& event) { m_main_simplebook->SetSelection(2); m_progress_gauge->SetValue(0); m_progress_timer->Start(70); m_progress_value = 0; start_sending_drying_command(); }); buttons_container->Add(m_start_button, 0, wxALL, FromDIP(5)); right_section->Add(buttons_container, 0, wxEXPAND | wxALL, FromDIP(5)); return right_section; } wxBoxSizer* AMSDryCtrWin::create_guide_page_sizer(wxPanel* parent) { wxBoxSizer* guide_sizer = new wxBoxSizer(wxHORIZONTAL); wxBoxSizer* info_section = create_guide_info_section(parent); guide_sizer->Add(info_section, 1, wxEXPAND | wxALL, FromDIP(10)); wxBoxSizer* right_section = create_guide_right_section(parent); guide_sizer->Add(right_section, 0, wxEXPAND | wxALL, FromDIP(10)); return guide_sizer; } void AMSDryCtrWin::start_sending_drying_command() { auto fila_system = get_fila_system(); if (!fila_system) { BOOST_LOG_TRIVIAL(info) << "AMSDryCtrWin::start_sending_drying_command: Invalid FilaSystem Pointer"; return; } if (m_temperature_input->GetValue().IsEmpty() || m_time_input->GetValue().IsEmpty()) { // Show error message to user BOOST_LOG_TRIVIAL(info) << "AMSDryCtrWin::start_sending_drying_command: Time or temperature is empty"; return; } long temperature, time; if (!m_temperature_input->GetValue().ToLong(&temperature) || !m_time_input->GetValue().ToLong(&time)) { BOOST_LOG_TRIVIAL(info) << "AMSDryCtrWin::start_sending_drying_command: Failed to convert temperature or time"; return; } int tray_index; tray_index = m_trays_combo->GetSelection(); if (m_tray_ids.empty() || tray_index < 0 || tray_index >= m_tray_ids.size()) { BOOST_LOG_TRIVIAL(warning) << "AMSDryCtrWin::start_sending_drying_command: Invalid tray_index " << tray_index << ", m_tray_ids.size=" << m_tray_ids.size(); return; } int cooling_temp = 50; std::optional preset = DevUtilBackend::GetFilamentDryingPreset(m_tray_ids[tray_index].filament_id); if (preset.has_value()) { cooling_temp = static_cast(preset.value().filament_dev_drying_softening_temperature); } fila_system->CtrlAmsStartDryingHour(std::stoi(m_ams_info.m_ams_id), m_tray_ids[tray_index].filament_type, temperature, time, m_rotate_spool_toggle->GetValue(), cooling_temp, false); m_dry_setting.m_filament_names[m_ams_info.m_ams_id] = m_tray_ids[tray_index].filament_name; m_dry_setting.m_filament_type[m_ams_info.m_ams_id] = m_tray_ids[tray_index].filament_type; m_dry_setting.m_dry_temp[m_ams_info.m_ams_id] = temperature; m_dry_setting.m_dry_time[m_ams_info.m_ams_id] = time; } void AMSDryCtrWin::OnProgressTimer(wxTimerEvent& event) { m_progress_value += 1; if (m_progress_value <= 100) m_progress_gauge->SetValue(m_progress_value); if (m_progress_value == 1) { // First tick, reset message index m_progress_message_index = 0; if (!m_progress_text.empty()) { m_progress_title->SetLabel(m_progress_text[0]); } } else if (m_progress_value % 15 == 0) { // Approximately every second m_progress_message_index++; if (m_progress_message_index < m_progress_text.size()) { m_progress_title->SetLabel(m_progress_text[m_progress_message_index]); m_progress_title->Refresh(); } } if (m_progress_value >= 100 && !is_dry_ctr_idle()) { m_progress_value = 0; m_progress_timer->Stop(); m_main_simplebook->SetSelection(0); } } void AMSDryCtrWin::restore_stop_button_if_deadline_passed() { if (m_stop_button_restore_deadline.has_value()) { if (std::chrono::steady_clock::now() >= m_stop_button_restore_deadline.value()) { if (m_stop_button) { m_stop_button->SetLabel(_L("Stop")); update_button_size(m_stop_button); // Adjust button size back to original m_stop_button->Enable(); m_stop_button->Layout(); m_stop_button->GetParent()->Layout(); m_stop_button->Refresh(); } m_stop_button_restore_deadline.reset(); } } } void AMSDryCtrWin::restore_unload_button_if_deadline_passed() { if (m_unload_button_restore_deadline.has_value()) { if (std::chrono::steady_clock::now() >= m_unload_button_restore_deadline.value()) { if (m_unload_button) { m_unload_button->Enable(); } m_unload_button_restore_deadline.reset(); } } } void AMSDryCtrWin::update_button_size(Button* button) { if (!button) return; // Reset MinSize first to avoid accumulation of size button->SetMinSize(wxSize(-1, -1)); // Recalculate button size based on current text and DPI settings wxSize best_size = button->GetBestSize(); int padding_width = FromDIP(4); int padding_height = FromDIP(2); wxSize final_size(best_size.GetWidth() + padding_width, best_size.GetHeight() + padding_height); button->SetMinSize(final_size); } void AMSDryCtrWin::OnShow(wxShowEvent& event) { if (event.IsShown()) { wxGetApp().UpdateDlgDarkUI(this); msw_rescale(); } event.Skip(); } void AMSDryCtrWin::OnClose(wxCloseEvent& event) { if (m_progress_timer && m_progress_timer->IsRunning()) { m_progress_timer->Stop(); } if (m_progress_gauge) { m_progress_gauge->SetValue(0); } m_progress_value = 0; m_progress_message_index = 0; if (m_main_simplebook) { m_main_simplebook->SetSelection(0); } if (m_progress_title && !m_progress_text.empty()) { m_progress_title->SetLabel(m_progress_text[0]); } // Clean up and restore stop button state if (m_stop_button) { m_stop_button->SetLabel(_L("Stop")); update_button_size(m_stop_button); // Adjust button size back to original m_stop_button->Enable(); m_stop_button->Layout(); m_stop_button->GetParent()->Layout(); m_stop_button->Refresh(); } m_stop_button_restore_deadline.reset(); // Clean up unload button state if (m_unload_button) { m_unload_button->Enable(); } m_unload_button_restore_deadline.reset(); event.Skip(); } wxBoxSizer* AMSDryCtrWin::create_progress_page_sizer(wxPanel* parent) { wxBoxSizer* progress_sizer = new wxBoxSizer(wxVERTICAL); m_progress_title = new Label(parent, m_progress_text[0]); m_progress_title->SetForegroundColour(*wxBLACK); m_progress_title->SetFont(Label::Body_16); progress_sizer->Add(0, 0, 1, wxEXPAND, 0); // Spacer progress_sizer->Add(m_progress_title, 0, wxALIGN_CENTER | wxALL, FromDIP(30)); m_progress_gauge = new ProgressBar(parent, wxID_ANY, 100, wxDefaultPosition, wxSize(FromDIP(300), FromDIP(8))); m_progress_gauge->SetValue(0); m_progress_gauge->SetHeight(FromDIP(8)); progress_sizer->Add(m_progress_gauge, 0, wxALIGN_CENTER | wxALL, FromDIP(10)); progress_sizer->Add(0, 0, 1, wxEXPAND, 0); return progress_sizer; } void AMSDryCtrWin::create() { // set title icon SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE)); this->SetDoubleBuffered(true); std::string icon_path = (boost::format("%1%/images/BambuStudioTitle.ico") % resources_dir()).str(); SetIcon(wxIcon(encode_path(icon_path.c_str()), wxBITMAP_TYPE_ICO)); SetSize(wxSize(FromDIP(700), FromDIP(500))); SetMinSize(wxSize(FromDIP(700), FromDIP(500))); SetMaxSize(wxSize(FromDIP(700), FromDIP(500))); m_main_simplebook = new wxSimplebook(this, wxID_ANY, wxDefaultPosition, wxDefaultSize); // Create main page m_original_page = new wxPanel(m_main_simplebook, wxID_ANY); m_original_page->SetBackgroundColour(*wxWHITE); wxBoxSizer* main_sizer = create_main_page_sizer(m_original_page); m_original_page->SetSizer(main_sizer); m_main_simplebook->AddPage(m_original_page, "Main Page"); m_guide_page = new wxPanel(m_main_simplebook, wxID_ANY); m_guide_page->SetBackgroundColour(*wxWHITE); wxBoxSizer* guide_sizer = create_guide_page_sizer(m_guide_page); m_guide_page->SetSizer(guide_sizer); m_main_simplebook->AddPage(m_guide_page, "Guide Page"); // Create progress page m_progress_page = new wxPanel(m_main_simplebook, wxID_ANY); m_progress_page->SetBackgroundColour(*wxWHITE); wxBoxSizer* progress_sizer = create_progress_page_sizer(m_progress_page); m_progress_page->SetSizer(progress_sizer); m_main_simplebook->AddPage(m_progress_page, "Progress Page"); m_progress_timer = new wxTimer(this, wxID_ANY); Bind(wxEVT_TIMER, &AMSDryCtrWin::OnProgressTimer, this, m_progress_timer->GetId()); Bind(wxEVT_CLOSE_WINDOW, &AMSDryCtrWin::OnClose, this); wxBoxSizer* top_level_sizer = new wxBoxSizer(wxVERTICAL); top_level_sizer->Add(m_main_simplebook, 1, wxEXPAND | wxALL, FromDIP(10)); Bind(wxEVT_SHOW, &AMSDryCtrWin::OnShow, this); SetSizer(top_level_sizer); Layout(); Refresh(); wxGetApp().UpdateDlgDarkUI(this); } void AMSDryCtrWin::on_dpi_changed(const wxRect &suggested_rect) { msw_rescale(); } void AMSDryCtrWin::msw_rescale() { if (!IsShown()) { return; } if (m_ams_filament_panel) {m_ams_filament_panel->msw_rescale();} if (m_trays_combo) {m_trays_combo->Rescale(); m_trays_combo->Layout(); m_trays_combo->SetFont(Label::Body_14), m_trays_combo->Refresh();} if (m_humidity_img && m_humidity_image.bmp().IsOk()) {m_humidity_image.msw_rescale(); m_humidity_img->SetBitmap(m_humidity_image.bmp());} if (m_image_placeholder && m_guide_image.bmp().IsOk()) {m_guide_image.msw_rescale();m_image_placeholder->SetBitmap(m_guide_image.bmp());} if (m_image_description_icon && m_description_icon_bitmap.bmp().IsOk()) {m_description_icon_bitmap.msw_rescale(); m_image_description_icon->SetBitmap(m_description_icon_bitmap.bmp());} // Rescale and update button sizes based on text content if (m_next_button) { m_next_button->Rescale(); update_button_size(m_next_button); m_next_button->Layout(); m_next_button->Refresh(); } if (m_stop_button) { m_stop_button->Rescale(); update_button_size(m_stop_button); m_stop_button->Layout(); m_stop_button->Refresh(); } if (m_start_button) { m_start_button->Rescale(); update_button_size(m_start_button); m_start_button->Layout(); m_start_button->Refresh(); } if (m_back_button) { m_back_button->Rescale(); update_button_size(m_back_button); m_back_button->Layout(); m_back_button->Refresh(); } if (m_unload_button) { m_unload_button->Rescale(); update_button_size(m_unload_button); m_unload_button->Layout(); m_unload_button->Refresh(); } if (m_guide_page) {m_guide_page->Layout();} if (m_original_page) {m_original_page->Layout();} Fit(); Layout(); Refresh(); } void AMSDryCtrWin::set_ams_id(const std::string& ams_id) { m_ams_info.m_ams_id = ams_id; m_is_ams_changed = true; } void AMSDryCtrWin::update_img_description(DevAms::DryStatus status, DevAms::DrySubStatus sub_status) { if (status == DevAms::DryStatus::Off || status == DevAms::DryStatus::Cooling) { m_image_description->SetLabel(_L("Idle")); m_image_description_icon->Show(false); return; } // Determine label text for non-idle states wxString label_text; if (status == DevAms::DryStatus::Error) { label_text = _L("Drying"); } else if (sub_status == DevAms::DrySubStatus::Heating) { label_text = _L("Drying-Heating"); } else if (sub_status == DevAms::DrySubStatus::Dehumidify) { label_text = _L("Drying-Dehumidifying"); } else { m_image_description_icon->Show(false); return; } m_image_description->SetLabel(label_text); try { m_description_icon_bitmap = ScalableBitmap(this, "dev_ams_dry_ctr_heating_icon", 20); m_description_icon_bitmap.msw_rescale(); m_image_description_icon->SetBitmap(m_description_icon_bitmap.bmp()); m_image_description_icon->Show(true); } catch (Exception&) { BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " Error loading drying icon"; m_image_description_icon->Show(false); } } int AMSDryCtrWin::update_image(DevAmsType model, DevAms::DryStatus status, DevAms::DrySubStatus sub_status, int humidity_percent) { if (model == m_ams_info.m_model && status == m_ams_info.m_dry_status && sub_status == m_ams_info.m_dry_sub_status && humidity_percent == m_ams_info.m_humidity_percent) { return 0; } std::string img_path; if (status == DevAms::DryStatus::Off && sub_status == DevAms::DrySubStatus::Off) { img_path = get_humidity_level_img_path(humidity_percent); } else { img_path = get_dry_status_img_path(model, status, sub_status); } if (img_path.empty()) { return 0; } m_humidity_image = ScalableBitmap(this, img_path, AMS_DRY_STATUS_IMAGE_SIZE); m_humidity_img->SetBitmap(m_humidity_image.bmp()); update_img_description(status, sub_status); return 1; } bool AMSDryCtrWin::check_values_changed(DevAms* dev_ams) { bool changed = false; if (m_ams_info.m_model != dev_ams->GetAmsType()) { m_ams_info.m_model = dev_ams->GetAmsType(); changed = true; } if (dev_ams->GetDryStatus().has_value() && m_ams_info.m_dry_status != dev_ams->GetDryStatus()) { m_ams_info.m_dry_status = dev_ams->GetDryStatus().value(); changed = true; } if (dev_ams->GetDrySubStatus().has_value() && m_ams_info.m_dry_sub_status != dev_ams->GetDrySubStatus()) { m_ams_info.m_dry_sub_status = dev_ams->GetDrySubStatus().value(); changed = true; } if (m_ams_info.m_humidity_percent != dev_ams->GetHumidityPercent()) { m_ams_info.m_humidity_percent = dev_ams->GetHumidityPercent(); changed = true; } return changed; } void AMSDryCtrWin::update_normal_description(DevAms* dev_ams) { if (m_tray_ids.empty() || m_trays_combo->GetSelection() >= m_tray_ids.size()) { return; } FilamentBaseInfo info = m_tray_ids[m_trays_combo->GetSelection()]; wxString warning_text; bool can_enable_button = true; long temp_val = 0, time_val = 0; m_temperature_input->GetValue().ToLong(&temp_val); m_time_input->GetValue().ToLong(&time_val); std::optional preset = DevUtilBackend::GetFilamentDryingPreset(info.filament_id); auto total_dry = preset.has_value() ? preset.value().ams_limitations : std::unordered_set(); struct AmsTempLimit { DevAmsType type; int min_temp; int max_temp; const char* name; }; static const AmsTempLimit ams_limits[] = { { DevAmsType::N3F, 45, 65, "AMS2" }, { DevAmsType::N3S, 45, 85, "AMS-S" } }; for (const auto& lim : ams_limits) { if (dev_ams->GetAmsType() == lim.type) { if (temp_val > lim.max_temp) { wxString msg = wxString(lim.name) + _L(" maximum drying temperature is ") + wxString::Format(wxT("%d"), lim.max_temp) + wxString::FromUTF8("°C."); warning_text += msg + "\n"; can_enable_button = false; } else if (temp_val < lim.min_temp) { wxString msg = wxString(lim.name) + _L(" minimum drying temperature is ") + wxString::Format(wxT("%d"), lim.min_temp) + wxString::FromUTF8("°C."); warning_text += msg + "\n"; can_enable_button = false; } if (total_dry.find(lim.type) == total_dry.end()) { warning_text += _L("This filament may not be completely dried.") + "\n\n"; } break; } } if (m_printer_status.m_is_printing && temp_val > m_ams_info.m_recommand_dry_temp) { warning_text += _L("This AMS is currently printing. To ensure print quality, the drying temperature cannot exceed the recommended drying temperature.") + "\n"; can_enable_button = false; } else if (preset.has_value()) { // Only check heat distortion temperature when AMS has filament inserted; // empty AMS only needs to respect the device hardware temperature limit. bool has_filament_inserted = false; for (const auto& tray_pair : dev_ams->GetTrays()) { if (tray_pair.second && tray_pair.second->is_exists) { has_filament_inserted = true; break; } } if (has_filament_inserted) { auto limit_temperature = preset.value().filament_dev_ams_drying_heat_distortion_temperature; if (temp_val > limit_temperature) { warning_text += _L("The temperature shall not exceed the filament's heat distortion temperature") + "(" + wxString::Format(wxT("%d"), static_cast(limit_temperature)) + wxString::FromUTF8("°C)\n"); can_enable_button = false; } } } if (time_val < 1) { warning_text += _L("Minimum time value cannot be less than 1.") + "\n"; can_enable_button = false; } else if (time_val > 24) { warning_text += _L("Maximum time value cannot be greater than 24.") + "\n"; can_enable_button = false; } m_next_button->Enable(can_enable_button); m_normal_description->SetLabel(warning_text); m_normal_description->Wrap(FromDIP(250)); m_normal_description->GetParent()->Layout(); } void AMSDryCtrWin::update_normal_state(DevAms* dev_ams) { if (is_dry_ctr_idle(dev_ams)) { m_next_button->Show(true); m_normal_description->Show(true); m_trays_combo->Enable(); m_temperature_input->SetEditable(true); m_time_input->SetEditable(true); m_stop_button->Hide(); m_dry_error_sizer->Show(false); } else if (is_dry_ctr_err(dev_ams)) { m_dry_error_sizer->Show(true); m_next_button->Hide(); m_normal_description->Hide(); m_trays_combo->Hide(); m_temperature_input->Hide(); m_time_input->Hide(); m_stop_button->Show(true); } else { m_next_button->Hide(); m_normal_description->Hide(); m_trays_combo->Disable(); m_temperature_input->SetEditable(false); m_time_input->SetEditable(false); m_stop_button->Show(true); m_dry_error_sizer->Show(false); } if (m_temperature_input->GetValue().IsEmpty() || m_time_input->GetValue().IsEmpty()) { m_next_button->Disable(); m_start_button->Disable(); } else { m_next_button->Enable(); } update_normal_description(dev_ams); } void AMSDryCtrWin::OnFilamentSelectionChanged(wxCommandEvent& event) { int selectionIndex = event.GetSelection(); wxString selectedFilament = event.GetString(); auto fila_system = get_fila_system(); if (!fila_system) { BOOST_LOG_TRIVIAL(info) << "AMSDryCtrWin::OnFilamentSelectionChanged: Invalid FilaSystem Pointer"; return; } DevAms* dev_ams = fila_system->GetAmsById(m_ams_info.m_ams_id); if (!dev_ams) { BOOST_LOG_TRIVIAL(info) << "AMSDryCtrWin::OnFilamentSelectionChanged: Invalid AMS id"; return; } std::optional preset = DevUtilBackend::GetFilamentDryingPreset(m_tray_ids[selectionIndex].filament_id); if (preset.has_value()) { DevFilamentDryingPreset info = preset.value(); if (m_printer_status.m_is_printing) { m_temperature_input->SetValue(std::to_string(static_cast(info.filament_dev_ams_drying_temperature_on_print[dev_ams->GetAmsType()]))); m_time_input->SetValue(std::to_string(static_cast(info.filament_dev_ams_drying_time_on_print[dev_ams->GetAmsType()]))); } else { m_temperature_input->SetValue(std::to_string(static_cast(info.filament_dev_ams_drying_temperature_on_idle[dev_ams->GetAmsType()]))); m_time_input->SetValue(std::to_string(static_cast(info.filament_dev_ams_drying_time_on_idle[dev_ams->GetAmsType()]))); } } update_filament_guide_info(dev_ams); } wxString get_cannot_reason_text(DevAms::CannotDryReason reason) { wxString cannot_reason_text; switch (reason) { case DevAms::CannotDryReason::InsufficientPower: cannot_reason_text = "*" + _L("Insufficient power") + "\n"; cannot_reason_text += _L(" Too many AMS drying simultaneously. Please plug in the power or stop other drying processes before starting.") + "\n"; break; case DevAms::CannotDryReason::AmsBusy: cannot_reason_text = "*" + _L("AMS is busy") + "\n"; cannot_reason_text += _L(" AMS is calibrating | reading RFID | loading/unloading material, please wait.") + "\n"; break; case DevAms::CannotDryReason::ConsumableAtAmsOutlet: cannot_reason_text = "*" + _L("Filament in AMS outlet") + "\n"; cannot_reason_text += _L(" The high drying temperature may cause AMS blockage, please unload first."); break; case DevAms::CannotDryReason::InitiatingAmsDrying: cannot_reason_text = "*" + _L("Initiating AMS drying") + "\n"; break; case DevAms::CannotDryReason::NotSupportedIn2dMode: cannot_reason_text = "*" + _L("Not supported in 2D mode") + "\n"; break; case DevAms::CannotDryReason::DryingInProgress: cannot_reason_text = "*" + _L("Task in progress") + "\n"; cannot_reason_text += _L(" The AMS might be in use during Task.") + "\n"; break; case DevAms::CannotDryReason::Upgrading: cannot_reason_text = "*" + _L("Upgrading") + "\n"; cannot_reason_text += _L(" Firmware update in progress, please wait...") + "\n"; break; case DevAms::CannotDryReason::InsufficientPowerNeedPluginPower: cannot_reason_text = "*" + _L("Insufficient power") + "\n"; cannot_reason_text += _L(" Please plug in the power and then use the drying function.") + "\n"; break; case DevAms::CannotDryReason::FilamentAtAmsOutletManualUnload: cannot_reason_text = "*" + _L("Filament in AMS outlet") + "\n"; cannot_reason_text += _L(" The high drying temperature may cause AMS blockage. Please unload the filament manually before proceeding.") + "\n"; break; default: cannot_reason_text = "*" + _L("System is busy") + "\n"; cannot_reason_text += _L(" Initiating other drying processes, please wait a few seconds...") + "\n"; BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": unknown cannot dry reason"; break; } return cannot_reason_text; } wxString organize_cannot_reasons_text(std::vector& reasons) { wxString cannot_reasons_text; if (std::find(reasons.begin(), reasons.end(), DevAms::CannotDryReason::DryingInProgress) != reasons.end()) { cannot_reasons_text += get_cannot_reason_text(DevAms::CannotDryReason::DryingInProgress); } else if (std::find(reasons.begin(), reasons.end(), DevAms::CannotDryReason::AmsBusy) != reasons.end()) { cannot_reasons_text += get_cannot_reason_text(DevAms::CannotDryReason::AmsBusy); } else if (std::find(reasons.begin(), reasons.end(), DevAms::CannotDryReason::ConsumableAtAmsOutlet) != reasons.end()) { cannot_reasons_text += get_cannot_reason_text(DevAms::CannotDryReason::ConsumableAtAmsOutlet); } else if (std::find(reasons.begin(), reasons.end(), DevAms::CannotDryReason::FilamentAtAmsOutletManualUnload) != reasons.end()) { cannot_reasons_text += get_cannot_reason_text(DevAms::CannotDryReason::FilamentAtAmsOutletManualUnload); } if (std::find(reasons.begin(), reasons.end(), DevAms::CannotDryReason::InsufficientPower) != reasons.end()) { cannot_reasons_text += get_cannot_reason_text(DevAms::CannotDryReason::InsufficientPower); } for (auto reason : reasons) { if (reason == DevAms::CannotDryReason::InsufficientPowerNeedPluginPower) { cannot_reasons_text += get_cannot_reason_text(DevAms::CannotDryReason::InsufficientPowerNeedPluginPower); } else if (reason == DevAms::CannotDryReason::NotSupportedIn2dMode) { cannot_reasons_text += get_cannot_reason_text(DevAms::CannotDryReason::NotSupportedIn2dMode); } else if (reason == DevAms::CannotDryReason::InitiatingAmsDrying) { cannot_reasons_text += get_cannot_reason_text(DevAms::CannotDryReason::InitiatingAmsDrying); } else if (reason == DevAms::CannotDryReason::Upgrading) { cannot_reasons_text += get_cannot_reason_text(DevAms::CannotDryReason::Upgrading); } } if (cannot_reasons_text.empty()) { cannot_reasons_text += "*" + _L("System is busy") + "\n"; cannot_reasons_text += _L(" Initiating other drying processes, please wait a few seconds...") + "\n"; } return cannot_reasons_text; } int AMSDryCtrWin::update_state(DevAms* dev_ams) { std::vector cannot_reasons = dev_ams->GetCannotDryReason().has_value()? dev_ams->GetCannotDryReason().value(): std::vector(); if (cannot_reasons.size() == 0 || (cannot_reasons.size() == 1 && cannot_reasons[0] == DevAms::CannotDryReason::DryingInProgress)) { m_normal_state_sizer->Show(true); m_cannot_dry_sizer->Show(false); m_unload_button->Show(false); update_normal_state(dev_ams); } else if (cannot_reasons.size() > 0) { m_normal_state_sizer->Show(false); m_cannot_dry_sizer->Show(true); m_cannot_dry_description_label->SetLabel(organize_cannot_reasons_text(cannot_reasons)); m_cannot_dry_description_label->Wrap(FromDIP(300)); // Show unload button when ConsumableAtAmsOutlet reason is present bool has_consumable_at_outlet = std::find(cannot_reasons.begin(), cannot_reasons.end(), DevAms::CannotDryReason::ConsumableAtAmsOutlet) != cannot_reasons.end(); m_unload_button->Show(has_consumable_at_outlet); } restore_stop_button_if_deadline_passed(); restore_unload_button_if_deadline_passed(); return 1; } int AMSDryCtrWin::update_ams_change(DevAms* dev_ams) { if (!is_ams_changed(dev_ams)) { return 0; } m_ams_info.m_ams_id = dev_ams->GetAmsId(); if (dev_ams->GetAmsType() == DevAmsType::N3F) { m_temperature_input->SetHint("45-65" + wxString::FromUTF8("°C")); } else if (dev_ams->GetAmsType() == DevAmsType::N3S) { m_temperature_input->SetHint("45-85" + wxString::FromUTF8("°C")); } m_time_input->SetHint("1-24 h"); return 0; } int AMSDryCtrWin::update_dryness_status(DevAms* dev_ams) { int updated = 0; if (m_ams_info.m_humidity_percent != dev_ams->GetHumidityPercent()) { updated += 1; m_ams_info.m_humidity_percent = dev_ams->GetHumidityPercent(); m_humidity_data_label->SetLabel(std::to_string(m_ams_info.m_humidity_percent) + "%"); } if (m_ams_info.m_temperature != dev_ams->GetCurrentTemperature()) { updated += 1; m_ams_info.m_temperature = dev_ams->GetCurrentTemperature(); m_temperature_data_label->SetLabel(std::to_string(m_ams_info.m_temperature) + wxString::FromUTF8("°C")); } if (is_dry_ctr_idle(dev_ams)) { m_time_descrition_container->Show(false); m_original_page->Layout(); } else { if (is_dry_status_changed(dev_ams)) { m_time_descrition_container->Show(true); m_original_page->Layout(); } if (m_ams_info.m_left_dry_time != dev_ams->GetLeftDryTime()) { updated += 1; m_ams_info.m_left_dry_time = dev_ams->GetLeftDryTime(); m_time_data_label->SetLabel(wxString::Format("%02d : %02d", m_ams_info.m_left_dry_time / 60, m_ams_info.m_left_dry_time % 60)); } } return updated; } bool AMSDryCtrWin::is_tray_changed(DevAms* dev_ams) { return false; } void AMSDryCtrWin::update_filament_guide_info(DevAms* dev_ams) { if (!IsShown()) { return; } m_guide_page->Freeze(); m_ams_filament_panel->Clear(); m_ams_filament_panel->SetAmsName(dev_ams->GetDisplayName()); // Get the temperature input value long input_temp = 0; bool valid_temp = !m_temperature_input->GetValue().IsEmpty() && m_temperature_input->GetValue().ToLong(&input_temp); bool can_start = true; int slot_count = 0, empty_count = 0; for (auto& tray_pair : dev_ams->GetTrays()) { if (!tray_pair.second) { continue; } ++slot_count; if (!tray_pair.second->is_exists) { m_ams_filament_panel->AddFilamentItem("/", "dev_ams_dry_ctr_enable"); ++empty_count; continue; } auto preset_opt = tray_pair.second->get_ams_drying_preset(); wxString filament_type = tray_pair.second->get_display_filament_type(); DevFilamentDryingPreset preset; if (filament_type.IsEmpty()) { auto fallback_preset = DevUtilBackend::GetFilamentDryingPreset("GFA00"); preset = fallback_preset.value(); filament_type = "?"; } else if (preset_opt.has_value()) { preset = preset_opt.value(); } else { auto fallback_preset = DevUtilBackend::GetFilamentDryingPreset("GFA00"); preset = fallback_preset.value(); } std::string icon_path = "dev_ams_dry_ctr_enable"; int distortion_temp = static_cast(preset.filament_dev_ams_drying_heat_distortion_temperature); if (valid_temp && distortion_temp < input_temp) { icon_path = "dev_ams_dry_ctr_disable"; can_start = false; } m_ams_filament_panel->AddFilamentItem(filament_type, icon_path); } if (slot_count == 0) { can_start = false; } if (can_start) { m_guide_title_label->SetLabel(_L("For better drying results, remove the filament and allow it to rotate.")); m_guide_description_label->SetLabel(_L("The AMS will automatically rotate the stored filament slots to enhance the drying performance.") + "\n" + _L("Alternatively, you can dry the filament without removing it.") + "\n" + "*" + _L("Unknown filaments will be treated as PLA.")); m_guide_description_label->Wrap(FromDIP(300)); } else { m_guide_title_label->SetLabel(_L("Please store the filament marked with an exclamation mark.")); m_guide_description_label->SetLabel(_L("Filament left in the feeder during drying may soften because the drying temperature exceeds the softening point of materials like PLA and TPU.") + "\n" + "*" + _L("Unknown filaments will be treated as PLA.")); m_guide_description_label->Wrap(FromDIP(300)); } m_start_button->Enable(can_start); m_guide_page->Layout(); m_guide_page->Thaw(); } int AMSDryCtrWin::update_filament_list(DevAms* dev_ams, MachineObject* obj) { const bool ams_changed = is_ams_changed(dev_ams); bool rebuilt_filament_list = false; // Build `m_tray_ids` and the combobox items from the preset bundle. // This list is also used as a lookup table in non-idle state (type -> name), // so it must be available even when the combobox is disabled. auto rebuild_filament_list = [&]() -> bool { m_trays_combo->Clear(); m_tray_ids.clear(); PresetBundle *preset_bundle = wxGetApp().preset_bundle; if (!preset_bundle || !obj) { return false; } std::set filament_id_set; auto & filaments = preset_bundle->filaments; // Get nozzle diameter using the same method as AMSMaterialsSetting::Popup std::ostringstream stream; int extruder_id = obj->GetFilaSystem()->GetExtruderIdByAmsId(m_ams_info.m_ams_id); if (!obj->GetExtderSystem()->GetExtderById(extruder_id)) { BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " get extruder id failed"; extruder_id = 0; } stream << std::fixed << std::setprecision(1) << obj->GetExtderSystem()->GetNozzleDiameter(extruder_id); std::string nozzle_diameter_str = stream.str(); std::set printer_names = preset_bundle->get_printer_names_by_printer_type_and_nozzle( DevPrinterConfigUtil::get_printer_display_name(obj->printer_type), nozzle_diameter_str); for (auto filament_it = filaments.begin(); filament_it != filaments.end(); ++filament_it) { Preset& preset = *filament_it; // Filter by system preset: root preset and (system preset or user preset is supported) if (filaments.get_preset_base(*filament_it) != &preset || (!filament_it->is_system && !obj->is_support_user_preset)) { continue; } ConfigOption * printer_opt = filament_it->config.option("compatible_printers"); ConfigOptionStrings *printer_strs = dynamic_cast(printer_opt); if (!printer_strs) continue; for (auto printer_str : printer_strs->values) { if (printer_names.find(printer_str) != printer_names.end()) { if (filament_id_set.find(filament_it->filament_id) != filament_id_set.end()) { continue; } filament_id_set.insert(filament_it->filament_id); auto filament_alias = filament_it->alias; if (!filament_alias.empty()) { auto opt_info = preset_bundle->get_filament_by_filament_id(filament_it->filament_id); if (opt_info.has_value()) { auto real_info = opt_info.value(); real_info.filament_name = filament_alias; m_tray_ids.push_back(std::move(real_info)); m_trays_combo->Append(wxString::FromUTF8(filament_alias)); } } } } } if (m_tray_ids.empty()) { BOOST_LOG_TRIVIAL(warning) << "AMSDryCtrWin::update_filament_list: No valid filaments found"; return false; } return true; }; if (ams_changed || m_tray_ids.empty() || m_trays_combo->GetCount() == 0) { // Disable start button during rebuild if (m_start_button) { m_start_button->Disable(); } rebuilt_filament_list = rebuild_filament_list(); } if (!is_dry_ctr_idle(dev_ams)) { const std::string& ams_id = dev_ams->GetAmsId(); const auto settings_opt = dev_ams->GetDrySettings(); // Set the combobox display label by the filament "type" string. // Priority: last saved selection for this AMS (if the type matches) -> fallback to current `m_tray_ids`. auto set_filament_label_by_type = [&](const std::string& filament_type) { if (filament_type.empty()) { return; } auto it_name = m_dry_setting.m_filament_names.find(ams_id); auto it_type = m_dry_setting.m_filament_type.find(ams_id); if (it_name != m_dry_setting.m_filament_names.end() && it_type != m_dry_setting.m_filament_type.end() && it_type->second == filament_type) { m_trays_combo->SetLabel(wxString::FromUTF8(it_name->second)); return; } const auto it = std::find_if(m_tray_ids.begin(), m_tray_ids.end(), [&](const FilamentBaseInfo& tray) { return tray.filament_type == filament_type; }); if (it != m_tray_ids.end()) { m_trays_combo->SetLabel(wxString::FromUTF8(it->filament_name)); } }; if (settings_opt.has_value()) { const auto& settings = settings_opt.value(); m_temperature_input->SetValue(std::to_string(settings.dry_temp)); m_time_input->SetValue(std::to_string(settings.dry_hour)); set_filament_label_by_type(settings.dry_filament); } else { auto it_name = m_dry_setting.m_filament_names.find(ams_id); if (it_name != m_dry_setting.m_filament_names.end()) { m_trays_combo->SetLabel(wxString::FromUTF8(it_name->second)); m_temperature_input->SetValue(std::to_string(m_dry_setting.m_dry_temp[ams_id])); m_time_input->SetValue(std::to_string(m_dry_setting.m_dry_time[ams_id])); } } return 0; } // Idle state: avoid re-selecting / firing selection change unless the list was rebuilt. if (!rebuilt_filament_list && !ams_changed && is_dry_ctr_idle()) { return 0; } if (m_tray_ids.empty()) { return 0; } // Select recommended drying temperature and default filament float min_dry_temp = std::numeric_limits::max(); std::string default_filament_id = "GFA00"; bool has_ready = false; const auto fallback_preset = DevUtilBackend::GetFilamentDryingPreset("GFA00"); for (const auto& tray_pair : dev_ams->GetTrays()) { if (!tray_pair.second || !tray_pair.second->is_tray_info_ready()) continue; has_ready = true; wxString filament_type = tray_pair.second->get_display_filament_type(); Slic3r::DevFilamentDryingPreset preset; if (filament_type.IsEmpty()) { // no filament type, is PLA if (!fallback_preset) continue; preset = fallback_preset.value(); } else { auto preset_opt = tray_pair.second->get_ams_drying_preset(); if (preset_opt.has_value()) { preset = preset_opt.value(); } else { // no preset,is PLA if (!fallback_preset) continue; preset = fallback_preset.value(); } } float cur_temp = m_printer_status.m_is_printing ? std::min({preset.filament_dev_ams_drying_temperature_on_print[dev_ams->GetAmsType()], preset.filament_dev_drying_softening_temperature, preset.filament_dev_ams_drying_heat_distortion_temperature}) : preset.filament_dev_ams_drying_temperature_on_idle[dev_ams->GetAmsType()]; if (cur_temp < min_dry_temp) { min_dry_temp = cur_temp; default_filament_id = preset.filament_id; } } // if no tray is ready, use PLA as default if (!has_ready) { if (fallback_preset.has_value()) { Slic3r::DevFilamentDryingPreset preset = fallback_preset.value(); float cur_temp = m_printer_status.m_is_printing ? std::min({preset.filament_dev_ams_drying_temperature_on_print[dev_ams->GetAmsType()], preset.filament_dev_drying_softening_temperature, preset.filament_dev_ams_drying_heat_distortion_temperature}) : preset.filament_dev_ams_drying_temperature_on_idle[dev_ams->GetAmsType()]; min_dry_temp = cur_temp; default_filament_id = preset.filament_id; } } m_ams_info.m_recommand_dry_temp = min_dry_temp; // Set default selection unsigned int default_index = 0; for (unsigned int i = 0; i < m_tray_ids.size(); i++) { if (m_tray_ids[i].filament_id == default_filament_id) { default_index = i; break; } } m_trays_combo->SetSelection(default_index); wxCommandEvent evt(wxEVT_COMBOBOX, m_trays_combo->GetId()); evt.SetInt(default_index); OnFilamentSelectionChanged(evt); return 1; } std::shared_ptr AMSDryCtrWin::get_fila_system() const { std::shared_ptr fila_system = m_fila_system.lock(); if (!fila_system) { const_cast(this)->Close(); } return fila_system; } void AMSDryCtrWin::update_printer_state(MachineObject* obj) { m_printer_status.m_is_printing = obj->is_in_printing() && obj->GetExtderSystem()->GetCurrentAmsId() == m_ams_info.m_ams_id; } void AMSDryCtrWin::update(std::shared_ptr fila_system, MachineObject* obj) { if (!fila_system || !obj) { return; } update_printer_state(obj); m_fila_system = fila_system; DevAms* dev_ams = fila_system->GetAmsById(m_ams_info.m_ams_id); if (!dev_ams) { BOOST_LOG_TRIVIAL(info) << "AMSDryCtrWin::update: Invalid AMS id"; m_ams_info.m_ams_id = ""; Close(); return; } if (!dev_ams->IsSupportRemoteDry(fila_system->GetOwner())) { BOOST_LOG_TRIVIAL(info) << "AMSDryCtrWin::update: Selected AMS does not support remote drying"; Close(); return; } update_ams_change(dev_ams); update_image(dev_ams->GetAmsType(), dev_ams->GetDryStatus().has_value()? dev_ams->GetDryStatus().value(): DevAms::DryStatus::Off, dev_ams->GetDrySubStatus().has_value()? dev_ams->GetDrySubStatus().value(): DevAms::DrySubStatus::Off, dev_ams->GetHumidityPercent()); update_state(dev_ams); update_dryness_status(dev_ams); update_filament_list(dev_ams, obj); update_filament_guide_info(dev_ams); m_is_ams_changed = false; check_values_changed(dev_ams); Layout(); Refresh(); } bool AMSDryCtrWin::is_ams_changed(DevAms* dev_ams) { return m_ams_info.m_ams_id != dev_ams->GetAmsId() || m_is_ams_changed; } bool AMSDryCtrWin::is_dry_status_changed(DevAms* dev_ams) { if (!dev_ams->GetDryStatus().has_value() || !dev_ams->GetDrySubStatus().has_value()) { return true; } return dev_ams->GetDryStatus().value() != m_ams_info.m_dry_status || dev_ams->GetDrySubStatus().value() != m_ams_info.m_dry_sub_status; } bool AMSDryCtrWin::is_dry_ctr_idle(DevAms* dev_ams) { if (!dev_ams->GetDryStatus().has_value() || !dev_ams->GetDrySubStatus().has_value()) { return true; } return dev_ams->GetDryStatus().value() == DevAms::DryStatus::Off || dev_ams->GetDryStatus().value() == DevAms::DryStatus::Cooling; } bool AMSDryCtrWin::is_dry_ctr_idle() { return m_ams_info.m_dry_status == DevAms::DryStatus::Off || m_ams_info.m_dry_status == DevAms::DryStatus::Cooling; } bool AMSDryCtrWin::is_dry_ctr_err(DevAms* dev_ams) { if (!dev_ams->GetDryStatus().has_value() || !dev_ams->GetDrySubStatus().has_value()) { return false; } return dev_ams->GetDryStatus().value() == DevAms::DryStatus::Error; } } // GUI } // Slic3r