diff --git a/src/slic3r/GUI/AMSMaterialsSetting.cpp b/src/slic3r/GUI/AMSMaterialsSetting.cpp index 2fbf7b2906..cc9572effe 100644 --- a/src/slic3r/GUI/AMSMaterialsSetting.cpp +++ b/src/slic3r/GUI/AMSMaterialsSetting.cpp @@ -4,6 +4,7 @@ #include "GUI_App.hpp" #include "GUI.hpp" #include "slic3r/Utils/NetworkAgent.hpp" +#include "slic3r/Utils/AmsPayload.hpp" #include "libslic3r/Preset.hpp" #include "I18N.hpp" #include @@ -146,6 +147,7 @@ void AMSMaterialsSetting::create_panel_normal(wxWindow* parent) #else m_comboBox_filament = new ::ComboBox(parent, wxID_ANY, wxEmptyString, wxDefaultPosition, AMS_MATERIALS_SETTING_COMBOX_WIDTH, 0, nullptr, wxCB_READONLY); #endif + m_comboBox_filament->SetClientData(&m_comboBox_from_printer); m_sizer_filament->Add(m_comboBox_filament, 1, wxALIGN_CENTER, 0); @@ -483,10 +485,7 @@ void AMSMaterialsSetting::on_select_reset(wxCommandEvent& event) { m_filament_type = ""; long nozzle_temp_min_int = 0; long nozzle_temp_max_int = 0; - wxColour color = *wxWHITE; - char col_buf[10]; - sprintf(col_buf, "%02X%02X%02X00", (int)color.Red(), (int)color.Green(), (int)color.Blue()); - std::string color_str; // reset use empty string + std::string color_str; // empty; command_ams_filament_settings maps it to the empty-tray color std::string selected_ams_id; PresetBundle *preset_bundle = wxGetApp().preset_bundle; @@ -503,7 +502,7 @@ void AMSMaterialsSetting::on_select_reset(wxCommandEvent& event) { if (obj) { if(m_is_third){ - obj->command_ams_filament_settings(ams_id, slot_id, ams_filament_id, ams_setting_id, std::string(col_buf), m_filament_type, nozzle_temp_min_int, + obj->command_ams_filament_settings(ams_id, slot_id, ams_filament_id, ams_setting_id, color_str, m_filament_type, nozzle_temp_min_int, nozzle_temp_max_int); } @@ -674,7 +673,7 @@ void AMSMaterialsSetting::on_select_ok(wxCommandEvent &event) wxString nozzle_temp_max = m_input_nozzle_max->GetTextCtrl()->GetValue(); - long nozzle_temp_min_int, nozzle_temp_max_int; + long nozzle_temp_min_int = 0, nozzle_temp_max_int = 0; nozzle_temp_min.ToLong(&nozzle_temp_min_int); nozzle_temp_max.ToLong(&nozzle_temp_max_int); wxColour color = m_clr_picker->m_colour; @@ -878,12 +877,22 @@ void AMSMaterialsSetting::on_clr_picker(wxMouseEvent &event) } std::vector ams_colors; - obj->GetFilaSystem()->CollectAmsColors(ams_colors); + // A fixed-vocabulary printer can only show its own palette; anything else + // would offer a color its screen cannot hold. + const FilamentMetadataValues metadata = filament_metadata_values(obj->get_dev_id()); + const bool constrained = metadata.constrained && !metadata.colors.empty(); + if (constrained) { + for (const std::string& color : metadata.colors) + ams_colors.push_back(DevAmsTray::decode_color(color)); + } else { + obj->GetFilaSystem()->CollectAmsColors(ams_colors); + } wxPoint img_pos = m_clr_picker->ClientToScreen(wxPoint(0, 0)); wxPoint popup_pos(img_pos.x - m_color_picker_popup.GetSize().x - FromDIP(95), img_pos.y - FromDIP(65)); m_color_picker_popup.Position(popup_pos, wxSize(0, 0)); m_color_picker_popup.set_ams_colours(ams_colors); + m_color_picker_popup.set_constrained(constrained); m_color_picker_popup.set_def_colour(m_clr_picker->m_colour); m_color_picker_popup.Popup(); } @@ -1008,8 +1017,12 @@ void AMSMaterialsSetting::Popup(wxString filament, wxString sn, wxString temp_mi if (alias.empty()) continue; - filament_items.push_back(alias); _collect_filament_info(alias, *filament_it, query_filament_vendors, query_filament_types); + // A printer with a fixed vocabulary only offers the types it can + // hold, so an incompatible preset never reaches the list. + if (!filament_material_compatible(obj->get_dev_id(), query_filament_types[alias].ToStdString())) + continue; + filament_items.push_back(alias); FilamentInfos filament_infos; filament_infos.filament_id = filament_it->filament_id; @@ -1208,7 +1221,7 @@ void AMSMaterialsSetting::Popup(wxString filament, wxString sn, wxString temp_mi } m_comboBox_filament->Set(filament_items); - m_comboBox_filament->SetClientData(new int(1)); + m_comboBox_from_printer = 1; m_comboBox_filament->SetSelection(selection_idx); post_select_event(selection_idx); @@ -1245,8 +1258,7 @@ void AMSMaterialsSetting::on_select_cali_result(wxCommandEvent &evt) void AMSMaterialsSetting::on_select_filament(wxCommandEvent &evt) { // Get the flag whether to open the filament setting dialog from the device page - int* from_printer = static_cast(m_comboBox_filament->GetClientData()); - const bool initial_printer_selection = from_printer && *from_printer == 1; + const bool initial_printer_selection = m_comboBox_from_printer == 1; m_filament_type = ""; PresetBundle* preset_bundle = wxGetApp().preset_bundle; @@ -1330,7 +1342,7 @@ void AMSMaterialsSetting::on_select_filament(wxCommandEvent &evt) m_input_k_val->GetTextCtrl()->SetValue(wxEmptyString); m_input_n_val->GetTextCtrl()->SetValue(wxEmptyString); } - m_comboBox_filament->SetClientData(new int(0)); + m_comboBox_from_printer = 0; return; } else { @@ -1417,7 +1429,7 @@ void AMSMaterialsSetting::on_select_filament(wxCommandEvent &evt) m_comboBox_cali_result->Set(items); if (ams_id == VIRTUAL_TRAY_MAIN_ID || ams_id == VIRTUAL_TRAY_DEPUTY_ID) { - if (from_printer && (*from_printer == 1)) { + if (m_comboBox_from_printer == 1) { for (auto slot : obj->vt_slot) { if (slot.id == std::to_string(ams_id)) cali_select_idx = CalibUtils::get_selected_calib_idx(m_pa_profile_items, slot.cali_idx); @@ -1434,7 +1446,7 @@ void AMSMaterialsSetting::on_select_filament(wxCommandEvent &evt) } } else { - if (from_printer && (*from_printer == 1)) { + if (m_comboBox_from_printer == 1) { DevAmsTray* selected_tray = this->obj->GetFilaSystem()->GetAmsTray(std::to_string(ams_id), std::to_string(slot_id)); if (!selected_tray) { @@ -1476,7 +1488,7 @@ void AMSMaterialsSetting::on_select_filament(wxCommandEvent &evt) } } - m_comboBox_filament->SetClientData(new int(0)); + m_comboBox_from_printer = 0; } void AMSMaterialsSetting::on_dpi_changed(const wxRect &suggested_rect) @@ -1738,6 +1750,7 @@ ColorPickerPopup::ColorPickerPopup(wxWindow* parent) cp->set_selected(false); cp->SetBackgroundColour(StateColor::darkModeColorFor(wxColour(238,238,238))); m_color_pickers.push_back(cp); + m_def_color_pickers.push_back(cp); fg_sizer->Add(cp, 0, wxALL, FromDIP(3)); cp->Bind(wxEVT_LEFT_DOWN, [this, cp](auto& e) { set_def_colour(cp->m_colour); @@ -1750,27 +1763,27 @@ ColorPickerPopup::ColorPickerPopup(wxWindow* parent) } wxBoxSizer* m_sizer_other = new wxBoxSizer(wxHORIZONTAL); - auto m_title_other = new wxStaticText(m_def_color_box, wxID_ANY, _L("Other Color"), wxDefaultPosition, wxDefaultSize, 0); + m_title_other = new wxStaticText(m_def_color_box, wxID_ANY, _L("Other Color"), wxDefaultPosition, wxDefaultSize, 0); m_title_other->SetFont(::Label::Body_14); m_title_other->SetBackgroundColour(wxColour(238, 238, 238)); m_sizer_other->Add(m_title_other, 0, wxALL, 5); - auto other_line = new wxPanel(m_def_color_box, wxID_ANY, wxDefaultPosition, wxSize(-1, 1), wxTAB_TRAVERSAL); - other_line->SetMinSize(wxSize(-1, 1)); - other_line->SetMaxSize(wxSize(-1, 1)); - other_line->SetBackgroundColour(wxColour("#CECECE")); - m_sizer_other->Add(other_line, 1, wxALIGN_CENTER, 0); + m_other_line = new wxPanel(m_def_color_box, wxID_ANY, wxDefaultPosition, wxSize(-1, 1), wxTAB_TRAVERSAL); + m_other_line->SetMinSize(wxSize(-1, 1)); + m_other_line->SetMaxSize(wxSize(-1, 1)); + m_other_line->SetBackgroundColour(wxColour("#CECECE")); + m_sizer_other->Add(m_other_line, 1, wxALIGN_CENTER, 0); //custom color wxBoxSizer* m_sizer_custom = new wxBoxSizer(wxHORIZONTAL); - auto m_title_custom = new wxStaticText(m_def_color_box, wxID_ANY, _L("Custom Color"), wxDefaultPosition, wxDefaultSize, 0); + m_title_custom = new wxStaticText(m_def_color_box, wxID_ANY, _L("Custom Color"), wxDefaultPosition, wxDefaultSize, 0); m_title_custom->SetFont(::Label::Body_14); m_title_custom->SetBackgroundColour(wxColour(238, 238, 238)); - auto custom_line = new wxPanel(m_def_color_box, wxID_ANY, wxDefaultPosition, wxSize(-1, 1), wxTAB_TRAVERSAL); - custom_line->SetBackgroundColour(wxColour("#CECECE")); - custom_line->SetMinSize(wxSize(-1, 1)); - custom_line->SetMaxSize(wxSize(-1, 1)); + m_custom_line = new wxPanel(m_def_color_box, wxID_ANY, wxDefaultPosition, wxSize(-1, 1), wxTAB_TRAVERSAL); + m_custom_line->SetBackgroundColour(wxColour("#CECECE")); + m_custom_line->SetMinSize(wxSize(-1, 1)); + m_custom_line->SetMaxSize(wxSize(-1, 1)); m_sizer_custom->Add(m_title_custom, 0, wxALL, 5); - m_sizer_custom->Add(custom_line, 1, wxALIGN_CENTER, 0); + m_sizer_custom->Add(m_custom_line, 1, wxALIGN_CENTER, 0); m_custom_cp = new StaticBox(m_def_color_box); m_custom_cp->SetSize(FromDIP(60), FromDIP(25)); @@ -1910,6 +1923,22 @@ void ColorPickerPopup::set_ams_colours(std::vector ams) Fit(); } +// A printer with a fixed vocabulary only shows its own palette: the default +// swatches and the custom picker would offer colors its screen cannot hold. +void ColorPickerPopup::set_constrained(bool constrained) +{ + const bool show = !constrained; + m_title_other->Show(show); + m_other_line->Show(show); + for (ColorPicker* cp : m_def_color_pickers) + cp->Show(show); + m_title_custom->Show(show); + m_custom_line->Show(show); + m_custom_cp->Show(show); + Layout(); + Fit(); +} + void ColorPickerPopup::set_def_colour(wxColour col) { m_def_col = col; diff --git a/src/slic3r/GUI/AMSMaterialsSetting.hpp b/src/slic3r/GUI/AMSMaterialsSetting.hpp index 2cbd58e9a9..7e2246aeb0 100644 --- a/src/slic3r/GUI/AMSMaterialsSetting.hpp +++ b/src/slic3r/GUI/AMSMaterialsSetting.hpp @@ -7,6 +7,8 @@ #include "DeviceManager.hpp" #include "wx/clrpicker.h" #include "wx/colourdata.h" +#include "wx/panel.h" +#include "wx/stattext.h" #include "Widgets/RadioBox.hpp" #include "Widgets/Button.hpp" #include "Widgets/RoundedRectangle.hpp" @@ -77,12 +79,20 @@ public: std::vector m_ams_colors; std::vector m_color_pickers; std::vector m_ams_color_pickers; + // Sections a fixed-vocabulary printer hides in set_constrained: its own + // palette is the only color its screen can show. + std::vector m_def_color_pickers; + wxStaticText* m_title_other{nullptr}; + wxPanel* m_other_line{nullptr}; + wxStaticText* m_title_custom{nullptr}; + wxPanel* m_custom_line{nullptr}; public: ColorPickerPopup(wxWindow* parent); ~ColorPickerPopup() {}; void on_custom_clr_picker(wxMouseEvent& event); void set_ams_colours(std::vector ams); + void set_constrained(bool constrained); void set_def_colour(wxColour col); void paintEvent(wxPaintEvent& evt); virtual void OnDismiss() wxOVERRIDE; @@ -123,6 +133,10 @@ public: std::string ams_setting_id; bool m_is_third; + + // Marks whether the filament combo was seeded from the printer's own tray. + // Stored as a member so the combo's client data never owns an allocation. + int m_comboBox_from_printer{0}; // Orca: view-only mode (laser/cut). When set, the dialog is inspectable but every // editing control is disabled and no command is sent. bool m_view_only = false; diff --git a/src/slic3r/GUI/DeviceCore/DevFilaSystem.cpp b/src/slic3r/GUI/DeviceCore/DevFilaSystem.cpp index 0c44c4f554..b01dcf3574 100644 --- a/src/slic3r/GUI/DeviceCore/DevFilaSystem.cpp +++ b/src/slic3r/GUI/DeviceCore/DevFilaSystem.cpp @@ -20,23 +20,31 @@ static int _hex_digit_to_int(const char c) { return (c >= '0' && c <= '9') ? c - wxColour DevAmsTray::decode_color(const std::string &color) { - std::array ret = {0, 0, 0, 0}; + // 6 hex digits (RRGGBB) and 8 (RRGGBBAA) are both legal on the wire; + // normalize_ams_color pads the short form, so accept it here too. + const size_t hex_digits = (color.size() == 6 || color.size() == 8) ? color.size() : 0; + if (hex_digits == 0) return wxColour(255, 255, 255, 255); + + std::array ret = {0, 0, 0, 255}; const char * c = color.data(); - if (color.size() == 8) { - for (size_t j = 0; j < 4; ++j) { - int digit1 = _hex_digit_to_int(*c++); - int digit2 = _hex_digit_to_int(*c++); - if (digit1 == -1 || digit2 == -1) break; - ret[j] = static_cast(digit1 * 16 + digit2); - } - } else { return wxColour(255, 255, 255, 255); } + for (size_t j = 0; j < hex_digits / 2; ++j) { + int digit1 = _hex_digit_to_int(*c++); + int digit2 = _hex_digit_to_int(*c++); + if (digit1 == -1 || digit2 == -1) break; + ret[j] = digit1 * 16 + digit2; + } return wxColour(ret[0], ret[1], ret[2], ret[3]); } void DevAmsTray::UpdateColorFromStr(const std::string& color) { - if (color.empty()) return; + // An empty color clears, exactly like a payload that omits the key. + if (color.empty()) { + wx_color = wxColour(); + this->color.clear(); + return; + } if (this->color != color) { wx_color = "#" + wxString::FromUTF8(color); @@ -729,7 +737,8 @@ void DevFilaSystemParser::ParseV1_0(const json& jj, MachineObject* obj, DevFilaS } else { - curr_tray->color = ""; + // Absent and empty carry the same meaning. + curr_tray->UpdateColorFromStr(std::string()); } if (tray_it->contains("nozzle_temp_max")) { diff --git a/src/slic3r/GUI/DeviceManager.cpp b/src/slic3r/GUI/DeviceManager.cpp index 4d9f056914..5f4577f6a7 100644 --- a/src/slic3r/GUI/DeviceManager.cpp +++ b/src/slic3r/GUI/DeviceManager.cpp @@ -565,6 +565,37 @@ bool MachineObject::ams_filament_ack_failed(const nlohmann::json& jj, std::strin return true; } +bool MachineObject::ams_filament_native_sync_pending(const nlohmann::json& jj) +{ + return jj.contains("native_sync") && jj["native_sync"].is_string() && + jj["native_sync"].get() == "pending"; +} + +bool MachineObject::ams_filament_connector_only(const nlohmann::json& jj) +{ + return jj.contains("native_sync") && jj["native_sync"].is_string() && + jj["native_sync"].get() == "connector_only"; +} + +void MachineObject::fill_ams_filament_ack_tray(DevAmsTray& tray, const nlohmann::json& jj) +{ + // Every field is optional: `jj` is a mutable copy, so a bare operator[] + // would insert null and the get<> below would throw type_error. + if (jj.contains("nozzle_temp_max") && jj["nozzle_temp_max"].is_number()) + tray.nozzle_temp_max = std::to_string(jj["nozzle_temp_max"].get()); + if (jj.contains("nozzle_temp_min") && jj["nozzle_temp_min"].is_number()) + tray.nozzle_temp_min = std::to_string(jj["nozzle_temp_min"].get()); + if (jj.contains("tray_color") && jj["tray_color"].is_string()) + tray.color = jj["tray_color"].get(); + if (jj.contains("tray_info_idx") && jj["tray_info_idx"].is_string()) + tray.setting_id = jj["tray_info_idx"].get(); + if (jj.contains("tray_type") || jj.contains("tray_info_idx")) { + const std::string type = (jj.contains("tray_type") && jj["tray_type"].is_string()) ? jj["tray_type"].get() : tray.m_fila_type; + tray.m_fila_type = setting_id_to_type(tray.setting_id, type); + } + tray.set_hold_count(); +} + PrinterArch MachineObject::get_printer_arch() const { return DevPrinterConfigUtil::get_printer_arch(printer_type); @@ -1791,8 +1822,10 @@ int MachineObject::command_ams_filament_settings(int ams_id, int slot_id, std::s j["print"]["tray_id"] = tag_tray_id; j["print"]["tray_info_idx"] = filament_id; j["print"]["setting_id"] = setting_id; - // format "FFFFFFFF" RGBA - j["print"]["tray_color"] = tray_color; + // Always present: Bambu firmware echoes it into the tray handler, and a + // slim request would make that echo throw. An empty value becomes the + // protocol's own empty-tray color. + j["print"]["tray_color"] = tray_color.empty() ? std::string("00000000") : tray_color; j["print"]["nozzle_temp_min"] = nozzle_temp_min; j["print"]["nozzle_temp_max"] = nozzle_temp_max; j["print"]["tray_type"] = tray_type; @@ -4257,6 +4290,10 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_ if (!ack_reason.empty()) text += wxString::FromUTF8(": ") + wxString::FromUTF8(ack_reason); GUI::wxGetApp().push_notification(this, text); + } else if (ams_filament_native_sync_pending(jj)) { + GUI::wxGetApp().push_notification(this, _L("Filament settings are saved and will be applied when the printer reconnects.")); + } else if (ams_filament_connector_only(jj)) { + GUI::wxGetApp().push_notification(this, _L("Filament settings were saved by OrcaSonar but could not be applied to the printer.")); } if (!ack_failed && jj["ams_id"].is_number()) { @@ -4267,39 +4304,15 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_ } if (ams_id == 255 && tray_id == VIRTUAL_TRAY_MAIN_ID && !vt_slot.empty()) { BOOST_LOG_TRIVIAL(info) << "ams_filament_setting, parse tray info"; - vt_slot[0].nozzle_temp_max = std::to_string(jj["nozzle_temp_max"].get()); - vt_slot[0].nozzle_temp_min = std::to_string(jj["nozzle_temp_min"].get()); - vt_slot[0].color = jj["tray_color"].get(); - vt_slot[0].setting_id = jj["tray_info_idx"].get(); - //vt_tray.type = jj["tray_type"].get(); - vt_slot[0].m_fila_type = setting_id_to_type(vt_slot[0].setting_id, jj["tray_type"].get()); + fill_ams_filament_ack_tray(vt_slot[0], jj); vt_slot[0].is_exists = true; - // delay update - vt_slot[0].set_hold_count(); } else { auto ams = m_fila_system->GetAmsById(std::to_string(ams_id)); if (ams) { auto tray_it = ams->GetTrays().find(std::to_string(tray_id)); if (tray_it != ams->GetTrays().end()) { BOOST_LOG_TRIVIAL(trace) << "ams_filament_setting, parse tray info"; - tray_it->second->nozzle_temp_max = std::to_string(jj["nozzle_temp_max"].get()); - tray_it->second->nozzle_temp_min = std::to_string(jj["nozzle_temp_min"].get()); - //tray_it->second->type = jj["tray_type"].get(); - tray_it->second->color = jj["tray_color"].get(); - - /*tray_it->second->cols.clear(); - if (jj.contains("cols")) { - if (jj["cols"].is_array()) { - for (auto it = jj["cols"].begin(); it != jj["cols"].end(); it++) { - tray_it->second->cols.push_back(it.value().get()); - } - } - }*/ - - tray_it->second->setting_id = jj["tray_info_idx"].get(); - tray_it->second->m_fila_type = setting_id_to_type(tray_it->second->setting_id, jj["tray_type"].get()); - // delay update - tray_it->second->set_hold_count(); + fill_ams_filament_ack_tray(*tray_it->second, jj); } else { BOOST_LOG_TRIVIAL(warning) << "ams_filament_setting, can not find in trayList, tray_id=" << tray_id; } @@ -5972,14 +5985,11 @@ void MachineObject::check_ams_filament_valid() if (curr_tray->setting_id.size() == 8 && curr_tray->setting_id[0] == 'P' && filament_list.find(curr_tray->setting_id) == filament_list.end()) { if (checked_filament.find(curr_tray->setting_id) != checked_filament.end()) { need_checked_filament_id[nozzle_diameter_str].insert(curr_tray->setting_id); - wxColour color = *wxWHITE; - char col_buf[10]; - sprintf(col_buf, "%02X%02X%02XFF", (int) color.Red(), (int) color.Green(), (int) color.Blue()); try { BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " " << __LINE__ << " ams settings_id is not exist in filament_list and reset, ams_id: " << ams_id << " tray_id" << slot_id << "filament_id: " << curr_tray->setting_id; - command_ams_filament_settings(std::stoi(ams_id), std::stoi(slot_id), "", "", std::string(col_buf), "", 0, 0); + command_ams_filament_settings(std::stoi(ams_id), std::stoi(slot_id), "", "", "00000000", "", 0, 0); continue; } catch (...) { BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " " << __LINE__ << " stoi error and ams_id: " << ams_id << " tray_id" << slot_id; @@ -6034,12 +6044,9 @@ void MachineObject::check_ams_filament_valid() if (vt_tray.setting_id.size() == 8 && vt_tray.setting_id[0] == 'P' && filament_list.find(vt_tray.setting_id) == filament_list.end()) { if (checked_filament.find(vt_tray.setting_id) != checked_filament.end()) { need_checked_filament_id[nozzle_diameter_str].insert(vt_tray.setting_id); - wxColour color = *wxWHITE; - char col_buf[10]; - sprintf(col_buf, "%02X%02X%02XFF", (int) color.Red(), (int) color.Green(), (int) color.Blue()); try { BOOST_LOG_TRIVIAL(info) << "vt_tray.setting_id is not exist in filament_list and reset vt_tray and the filament_id is: " << vt_tray.setting_id; - command_ams_filament_settings(vt_id, 0, "", "", std::string(col_buf), "", 0, 0); + command_ams_filament_settings(vt_id, 0, "", "", "00000000", "", 0, 0); continue; } catch (...) { BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " " << __LINE__ << " stoi error and tray_id" << vt_tray.id; diff --git a/src/slic3r/GUI/DeviceManager.hpp b/src/slic3r/GUI/DeviceManager.hpp index 5c3fa41b36..82cfa42a51 100644 --- a/src/slic3r/GUI/DeviceManager.hpp +++ b/src/slic3r/GUI/DeviceManager.hpp @@ -170,6 +170,11 @@ public: // Orca: an OPCP failure ack (result != "success"). Fills reason when present. static bool ams_filament_ack_failed(const nlohmann::json& jj, std::string& reason); + static bool ams_filament_native_sync_pending(const nlohmann::json& jj); + static bool ams_filament_connector_only(const nlohmann::json& jj); + // Orca: apply one ams_filament_setting echo to a tray. A slim ack omits + // fields; an unguarded get here would throw and drop the whole push frame. + static void fill_ams_filament_ack_tray(DevAmsTray& tray, const nlohmann::json& jj); // Orca: these DeviceCore module accessors are unwired on the read side — axis/chamber/status // are fed every MQTT push but no GUI consumer reads them yet, and for calib/upgrade the inline diff --git a/src/slic3r/GUI/StatusPanel.cpp b/src/slic3r/GUI/StatusPanel.cpp index 712e7155e6..44d0de6bc8 100644 --- a/src/slic3r/GUI/StatusPanel.cpp +++ b/src/slic3r/GUI/StatusPanel.cpp @@ -4616,6 +4616,12 @@ void StatusPanel::on_filament_edit(wxCommandEvent &event) filament = tray->sub_brands; } } + else + { + // No tray state for this slot: never show the previous slot's. + m_filament_setting_dlg->set_empty_color(AMS_MATERIALS_SETTING_DEF_COLOUR); + m_filament_setting_dlg->ams_filament_id = ""; + } m_filament_setting_dlg->Move(wxPoint(current_position_x, current_position_y)); m_filament_setting_dlg->Popup(filament, sn_number, temp_min, temp_max, k_val, n_val); diff --git a/src/slic3r/Utils/AmsPayload.cpp b/src/slic3r/Utils/AmsPayload.cpp index 2043f6332b..713b77be94 100644 --- a/src/slic3r/Utils/AmsPayload.cpp +++ b/src/slic3r/Utils/AmsPayload.cpp @@ -89,7 +89,8 @@ std::string map_filament_type_to_generic_id(const std::string& filament_type) if (it == type_to_ofl_family.end()) return UNKNOWN_FILAMENT_ID; - if (auto* bundle = GUI::wxGetApp().preset_bundle) { + auto* bundle = wxTheApp != nullptr ? GUI::wxGetApp().preset_bundle : nullptr; + if (bundle) { const Preset* preset = bundle->filaments.find_preset("Generic " + it->second + " @System"); if (preset != nullptr && preset->is_system && !preset->filament_id.empty()) return preset->filament_id; @@ -270,7 +271,7 @@ LaneDataFetch read_moonraker_lane_data(const std::string& origin, void resolve_tray_info_idx(std::vector& trays) { - auto* bundle = GUI::wxGetApp().preset_bundle; + auto* bundle = wxTheApp != nullptr ? GUI::wxGetApp().preset_bundle : nullptr; for (auto& tray : trays) { // Absent lanes render as placeholders; resolving an empty type is busy // work at best and could bind a bogus id. @@ -280,9 +281,15 @@ void resolve_tray_info_idx(std::vector& trays) // what is still empty so the generic fallback cannot overwrite it. if (!tray.tray_info_idx.empty()) continue; - tray.tray_info_idx = bundle - ? bundle->filaments.filament_id_by_type(tray.tray_type) - : map_filament_type_to_generic_id(tray.tray_type); + if (bundle) { + std::string filament_id; + if (bundle->filaments.filament_id_by_type(tray.tray_type, filament_id)) + tray.tray_info_idx = std::move(filament_id); + } else { + const std::string filament_id = map_filament_type_to_generic_id(tray.tray_type); + if (filament_id != UNKNOWN_FILAMENT_ID) + tray.tray_info_idx = filament_id; + } } } @@ -388,6 +395,7 @@ struct AmsDeviceCaps bool ops_known = false; bool has_ams = false; bool filament_slots = false; + FilamentMetadataValues metadata; }; static std::map g_ams_caps; @@ -447,6 +455,84 @@ bool has_filament_slots(const std::string& dev_id) return it != g_ams_caps.end() && it->second.filament_slots; } +void register_filament_metadata(const std::string& dev_id, const FilamentMetadataValues& values) +{ + if (dev_id.empty()) + return; + std::lock_guard lock(g_ams_state_mutex); + g_ams_caps[dev_id].metadata = values; +} + +FilamentMetadataValues filament_metadata_values(const std::string& dev_id) +{ + std::lock_guard lock(g_ams_state_mutex); + auto it = g_ams_caps.find(dev_id); + if (it == g_ams_caps.end()) + return {}; + return it->second.metadata; +} + +static bool material_prefix(const std::string& value, const char* family) +{ + const std::string prefix(family); + if (value == prefix) + return true; + if (value.size() <= prefix.size() || value.compare(0, prefix.size(), prefix) != 0) + return false; + const char next = value[prefix.size()]; + return next == ' ' || next == '+' || next == '-' || next == '/'; +} + +static bool qidi_pa_material(const std::string& value) +{ + return material_prefix(value, "PA") || material_prefix(value, "PA12") || + material_prefix(value, "PAHT") || material_prefix(value, "ULTRAPA"); +} + +// Keep distinct material families separate; only known variants share a stem. +std::string normalize_qidi_material_family(const std::string& value) +{ + const std::string upper = boost::algorithm::to_upper_copy(boost::algorithm::trim_copy(value)); + if (material_prefix(upper, "PLA-CF")) return "PLA-CF"; + if (material_prefix(upper, "PPA-CF")) return "PPA-CF"; + if (material_prefix(upper, "PC-ABS")) return "PC"; + if (material_prefix(upper, "PCTG")) return "PCTG"; + if (material_prefix(upper, "PPA")) return "PPA"; + if (material_prefix(upper, "PLA")) return "PLA"; + if (material_prefix(upper, "ABS")) return "ABS"; + if (material_prefix(upper, "PETG")) return "PETG"; + if (material_prefix(upper, "TPU")) return "TPU"; + if (material_prefix(upper, "ASA")) return "ASA"; + if (qidi_pa_material(upper) || material_prefix(upper, "NYLON")) return "PA"; + if (material_prefix(upper, "PC")) return "PC"; + if (material_prefix(upper, "PVA")) return "PVA"; + return upper; +} + +bool filament_material_compatible(const std::string& dev_id, const std::string& preset_type) +{ + const FilamentMetadataValues values = filament_metadata_values(dev_id); + if (!values.constrained) + return true; + if (boost::algorithm::trim_copy(preset_type).empty()) + return true; // a reply that never carried a type must not hide presets + std::vector parts; + boost::algorithm::split(parts, preset_type, boost::algorithm::is_any_of(";"), boost::token_compress_on); + for (const std::string& part : parts) { + const std::string type = boost::algorithm::trim_copy(part); + if (type.empty()) + continue; + const std::string category = normalize_qidi_material_family(type); + for (const std::string& declared : values.materials) { + if (boost::algorithm::iequals(boost::algorithm::trim_copy(declared), type)) + return true; + if (normalize_qidi_material_family(declared) == category) + return true; + } + } + return false; +} + void clear_ams_caps(const std::string& dev_id) { if (dev_id.empty()) diff --git a/src/slic3r/Utils/AmsPayload.hpp b/src/slic3r/Utils/AmsPayload.hpp index d5678aed58..148c3a004d 100644 --- a/src/slic3r/Utils/AmsPayload.hpp +++ b/src/slic3r/Utils/AmsPayload.hpp @@ -113,6 +113,26 @@ bool ams_caps_known(const std::string& dev_id); void register_filament_slots(const std::string& dev_id, bool has_slots); bool has_filament_slots(const std::string& dev_id); +// The values a fixed-vocabulary printer can hold for filament metadata, from +// protocol.filament_metadata..values. Materials are the strings the +// status projection reports and colors are RRGGBBAA. `constrained` is true only +// when every writing driver declared values: one free-form driver keeps the +// full picker available. +struct FilamentMetadataValues +{ + bool constrained = false; + std::vector materials; + std::vector colors; +}; +void register_filament_metadata(const std::string& dev_id, const FilamentMetadataValues& values); +FilamentMetadataValues filament_metadata_values(const std::string& dev_id); +std::string normalize_qidi_material_family(const std::string& material); +// Whether a preset's filament type can be represented by the device's declared +// materials: an exact type, or the material category the two names share. An +// unconstrained or unanswered device accepts everything, and a type the reply +// never carried is kept rather than hidden. +bool filament_material_compatible(const std::string& dev_id, const std::string& preset_type); + // Forget a device's declared capabilities, so a reconnect starts from "no // reply yet" instead of a stale declaration. void clear_ams_caps(const std::string& dev_id); diff --git a/src/slic3r/Utils/OrcaPrinterAgent.cpp b/src/slic3r/Utils/OrcaPrinterAgent.cpp index b7816d55e8..6824ad90ab 100644 --- a/src/slic3r/Utils/OrcaPrinterAgent.cpp +++ b/src/slic3r/Utils/OrcaPrinterAgent.cpp @@ -686,6 +686,43 @@ void OrcaPrinterAgent::register_ams_capabilities(const std::string& dev_id, cons has_slots = slots_it->get(); } register_filament_slots(dev_id, has_slots); + + // filament_metadata..values publishes what a fixed-vocabulary + // driver can hold. It constrains the picker only when every writing driver + // declared values: one free-form driver (Happy Hare, AFC) keeps the full + // color picker and preset list available. Registered on every reply so a + // later one without the key clears a stale palette. + FilamentMetadataValues metadata; + const auto meta_it = proto_it->find("filament_metadata"); + if (meta_it != proto_it->end() && meta_it->is_object() && !meta_it->empty()) { + const auto collect = [](std::vector& out, const nlohmann::json& values, const char* key) { + const auto arr = values.find(key); + if (arr == values.end() || !arr->is_array()) + return; + for (const auto& v : *arr) { + if (!v.is_string()) + continue; + const std::string entry = v.get(); + if (std::find(out.begin(), out.end(), entry) == out.end()) + out.push_back(entry); + } + }; + bool all_constrained = true; + for (auto driver = meta_it->begin(); driver != meta_it->end() && all_constrained; ++driver) { + const auto values_it = driver.value().find("values"); + if (values_it == driver.value().end() || !values_it->is_object() || values_it->empty()) { + all_constrained = false; + break; + } + collect(metadata.materials, *values_it, "material"); + collect(metadata.colors, *values_it, "color"); + } + if (all_constrained && (!metadata.materials.empty() || !metadata.colors.empty())) + metadata.constrained = true; + else + metadata = FilamentMetadataValues{}; + } + register_filament_metadata(dev_id, metadata); } void OrcaPrinterAgent::deliver_to_sink(const std::string& dev_id, const std::string& payload, bool local) @@ -1299,9 +1336,9 @@ std::string OrcaPrinterAgent::canonicalize_ams_payload(const std::string& dev_id if (cmd.empty() || (cmd.rfind("ams_", 0) != 0 && cmd != "auto_stop_ams_dry")) return json_str; - // filament_setting is exempt from the ams_ops union: it persists - // connector state through the filament-slot model, so filament_slots - // alone advertises it (OrcaSonar OPCP §7.8). + // filament_setting is exempt from the ams_ops union: filament_slots + // advertises the canonical slot write, while native metadata support is + // reported separately by the driver (OrcaSonar OPCP §7.8). auto op_allowed = [&dev_id](const std::string& o) { if (o == "filament_setting") return !ams_caps_known(dev_id) || has_filament_slots(dev_id); diff --git a/src/slic3r/Utils/QidiPrinterAgent.cpp b/src/slic3r/Utils/QidiPrinterAgent.cpp index 94280e658b..81184dc5db 100644 --- a/src/slic3r/Utils/QidiPrinterAgent.cpp +++ b/src/slic3r/Utils/QidiPrinterAgent.cpp @@ -1,6 +1,7 @@ #include "QidiPrinterAgent.hpp" #include "Http.hpp" #include "IPrinterAgent.hpp" +#include "slic3r/Utils/AmsPayload.hpp" #include "libslic3r/PresetBundle.hpp" #include "slic3r/GUI/GUI_App.hpp" @@ -507,26 +508,7 @@ std::string QidiPrinterAgent::infer_series_id(const std::string& model_id, const std::string QidiPrinterAgent::normalize_filament_type(const std::string& filament_type) { - const std::string upper = trim_and_upper(filament_type); - - if (upper.find("PLA") != std::string::npos) - return "PLA"; - if (upper.find("ABS") != std::string::npos) - return "ABS"; - if (upper.find("PETG") != std::string::npos) - return "PETG"; - if (upper.find("TPU") != std::string::npos) - return "TPU"; - if (upper.find("ASA") != std::string::npos) - return "ASA"; - if (upper.find("PA") != std::string::npos || upper.find("NYLON") != std::string::npos) - return "PA"; - if (upper.find("PC") != std::string::npos) - return "PC"; - if (upper.find("PVA") != std::string::npos) - return "PVA"; - - return upper; + return normalize_qidi_material_family(filament_type); } } // namespace Slic3r diff --git a/tests/slic3rutils/test_device_manager.cpp b/tests/slic3rutils/test_device_manager.cpp index 46686aafd5..462a0bc005 100644 --- a/tests/slic3rutils/test_device_manager.cpp +++ b/tests/slic3rutils/test_device_manager.cpp @@ -285,6 +285,56 @@ TEST_CASE("AMS filament setting acks expose OPCP failures", "[DeviceManager]") CHECK_FALSE(MachineObject::ams_filament_ack_failed(plain, reason)); } +TEST_CASE("AMS filament acknowledgements report native sync state", "[DeviceManager]") +{ + CHECK(MachineObject::ams_filament_native_sync_pending(json::parse(R"({"native_sync":"pending"})"))); + CHECK_FALSE(MachineObject::ams_filament_native_sync_pending(json::parse(R"({"native_sync":"applied"})"))); + CHECK(MachineObject::ams_filament_connector_only(json::parse(R"({"native_sync":"connector_only"})"))); + CHECK_FALSE(MachineObject::ams_filament_connector_only(json::parse(R"({"native_sync":"not_applicable"})"))); +} + +// Every tray field in the echo is optional: an unguarded get would throw +// type_error into parse_json's frame-wide catch and drop the rest of the push. +TEST_CASE("A slim AMS filament acknowledgement leaves the tray untouched", "[DeviceManager]") +{ + DevAmsTray tray("0"); + tray.color = "FF0000FF"; + tray.nozzle_temp_min = "190"; + tray.m_fila_type = "PLA"; + + CHECK_NOTHROW(MachineObject::fill_ams_filament_ack_tray( + tray, json::parse(R"({"command":"ams_filament_setting","result":"success","errno":0})"))); + CHECK(tray.color == "FF0000FF"); + CHECK(tray.nozzle_temp_min == "190"); + CHECK(tray.m_fila_type == "PLA"); + + CHECK_NOTHROW(MachineObject::fill_ams_filament_ack_tray( + tray, json::parse(R"({"tray_color":"0000FFFF","tray_type":"ABS","tray_info_idx":"GFA01","nozzle_temp_min":210,"nozzle_temp_max":240})"))); + CHECK(tray.color == "0000FFFF"); + CHECK(tray.setting_id == "GFA01"); + CHECK(tray.nozzle_temp_min == "210"); + CHECK(tray.nozzle_temp_max == "240"); + CHECK(tray.m_fila_type == "ABS"); +} + +// An echo that carries no tray fields at all must leave the frame alive: the +// frame-wide catch would silently swallow every later message in the push. +TEST_CASE("An AMS filament acknowledgement without tray fields does not abort the frame", "[DeviceManager]") +{ + MachineObject machine(nullptr, nullptr, "test", "test-device", "127.0.0.1"); + + const json ams = json::parse(R"({"ams":{"tray_exist_bits":"1","ams":[ + { "id": "0", "info": "00000001", "tray": [ { "id": "0" } ] } ]}})"); + DevFilaSystemParser::ParseV1_0(ams, &machine, machine.GetFilaSystem().get(), false); + REQUIRE(machine.GetFilaSystem()->GetAmsTray("0", "0") != nullptr); + + CHECK_NOTHROW(machine.parse_json("lan", R"({"print":{"command":"ams_filament_setting","result":"success","errno":0,"ams_id":0,"tray_id":0}})", false)); + + const DevAmsTray* tray = machine.GetFilaSystem()->GetAmsTray("0", "0"); + REQUIRE(tray != nullptr); + CHECK(tray->color.empty()); +} + // An ack that targets a tray but omits tray_id must not abort the frame: it // falls back to slot 0 instead of an unguarded get on a missing key. TEST_CASE("An AMS filament ack without a tray_id targets slot 0", "[DeviceManager]") diff --git a/tests/slic3rutils/test_orca_printer_agent.cpp b/tests/slic3rutils/test_orca_printer_agent.cpp index b9c9d1b5cb..bab7892ae7 100644 --- a/tests/slic3rutils/test_orca_printer_agent.cpp +++ b/tests/slic3rutils/test_orca_printer_agent.cpp @@ -351,6 +351,15 @@ TEST_CASE("OrcaPrinterAgent rewrites Bambu ams_* payloads onto the canonical Orc CHECK(!out["print"].contains("tray_id")); CHECK(out["print"]["tray_type"] == "PLA"); + // tray_color is the one field this sync exists to carry: it must survive + // the funnel, both as a real color and as the empty-tray marker a reset + // sends. + out = nlohmann::json::parse(canon("dev-c1", R"({"print":{"command":"ams_filament_setting","ams_id":1,"slot_id":2,"tray_id":2,"tray_type":"PLA","tray_color":"00FF00FF","tray_info_idx":"GFL01"}})")); + CHECK(out["print"]["tray_color"] == "00FF00FF"); + out = nlohmann::json::parse(canon("dev-c1", R"({"print":{"command":"ams_filament_setting","ams_id":1,"slot_id":2,"tray_id":2,"tray_type":"","tray_info_idx":"","tray_color":"00000000"}})")); + CHECK(out["print"]["tray_color"] == "00000000"); + CHECK(out["print"]["tray_type"] == ""); + // Wide-box dual form: both addressings resolve to one slot server-side. out = nlohmann::json::parse(canon("dev-c1", R"({"print":{"command":"ams_filament_setting","ams_id":1,"slot_id":5,"tray_type":"PLA"}})")); CHECK(out["print"]["ams_id"] == 1); @@ -471,6 +480,73 @@ TEST_CASE("filament slot writes remain independent of FMS", "[OrcaPrinterAgent]" CHECK(unsupported); } +// A fixed-vocabulary printer publishes the values it can hold, so the dialog +// offers only those: an exact type and its variants pass the filter, a material +// the printer lacks does not. A free-form driver and an unanswered device stay +// unconstrained. +TEST_CASE("A printer publishes its filament metadata values", "[OrcaPrinterAgent]") { + Probe agent("/tmp"); + agent.deliver_to_sink("dev-qidi", + R"({"info":{"command":"get_capabilities","capabilities":{"protocol":{ + "filament_metadata":{"qidi_box":{"read":["material","color"],"write":["material","color"], + "values":{"material":["PLA","PETG"],"color":["FF0000FF","00FF00FF"]}}}}}}})", + /*local=*/true); + + const Slic3r::FilamentMetadataValues values = Slic3r::filament_metadata_values("dev-qidi"); + CHECK(values.constrained); + CHECK(values.materials.size() == 2); + CHECK(values.colors.size() == 2); + + CHECK(Slic3r::filament_material_compatible("dev-qidi", "PLA")); + CHECK(Slic3r::filament_material_compatible("dev-qidi", "PETG")); + CHECK(Slic3r::filament_material_compatible("dev-qidi", "PLA+")); + CHECK_FALSE(Slic3r::filament_material_compatible("dev-qidi", "HIPS")); + + CHECK(Slic3r::normalize_qidi_material_family("PCTG") == "PCTG"); + CHECK(Slic3r::normalize_qidi_material_family("PPA") == "PPA"); + CHECK(Slic3r::normalize_qidi_material_family("PLA-CF") == "PLA-CF"); + CHECK(Slic3r::normalize_qidi_material_family("PA12-CF") == "PA"); + CHECK(Slic3r::normalize_qidi_material_family("PC-ABS-FR") == "PC"); + + agent.deliver_to_sink("dev-qidi-polymer", + R"({"info":{"command":"get_capabilities","capabilities":{"protocol":{ + "filament_metadata":{"qidi_box":{"read":["material","color"],"write":["material","color"], + "values":{"material":["PC","PA"],"color":["FF0000FF"]}}}}}}})", + /*local=*/true); + CHECK(Slic3r::filament_material_compatible("dev-qidi-polymer", "PC")); + CHECK(Slic3r::filament_material_compatible("dev-qidi-polymer", "PA Nylon")); + CHECK(Slic3r::filament_material_compatible("dev-qidi-polymer", "PA-CF")); + CHECK(Slic3r::filament_material_compatible("dev-qidi-polymer", "PA12-CF")); + CHECK(Slic3r::filament_material_compatible("dev-qidi-polymer", "PAHT-CF")); + CHECK(Slic3r::filament_material_compatible("dev-qidi-polymer", "PAHT-GF")); + CHECK(Slic3r::filament_material_compatible("dev-qidi-polymer", "PAHT-S")); + CHECK(Slic3r::filament_material_compatible("dev-qidi-polymer", "UltraPA")); + CHECK(Slic3r::filament_material_compatible("dev-qidi-polymer", "UltraPA-CF25")); + CHECK(Slic3r::filament_material_compatible("dev-qidi-polymer", "PC-ABS-FR")); + CHECK_FALSE(Slic3r::filament_material_compatible("dev-qidi-polymer", "PCTG")); + CHECK_FALSE(Slic3r::filament_material_compatible("dev-qidi-polymer", "PPA")); + + agent.deliver_to_sink("dev-happyhare", + R"({"info":{"command":"get_capabilities","capabilities":{"protocol":{ + "filament_metadata":{"happy_hare":{"read":["material","color"],"write":["material","color"]}}}}}})", + /*local=*/true); + CHECK_FALSE(Slic3r::filament_metadata_values("dev-happyhare").constrained); + CHECK(Slic3r::filament_material_compatible("dev-happyhare", "HIPS")); + + CHECK(Slic3r::filament_material_compatible("dev-unanswered", "HIPS")); +} + +TEST_CASE("An unknown lane material does not bind to an arbitrary filament id", "[OrcaPrinterAgent]") { + AmsTrayData tray; + tray.has_filament = true; + tray.tray_type = "Unlisted polymer family"; + std::vector trays{tray}; + + Slic3r::resolve_tray_info_idx(trays); + + CHECK(trays[0].tray_info_idx.empty()); +} + // A box wider than 4 slots is shown as several 4-tray units, so the BBL tray id // a panel reports must split into that unit's own (ams_id, slot_id). A flat lane // would be the tray id, which is not the layout lane for a wide box