From 543ca95059a09f93b1c343b9784eaa3eb64d5fb3 Mon Sep 17 00:00:00 2001 From: Lam Wei Lun Date: Tue, 29 Sep 2026 17:47:24 +0800 Subject: [PATCH] fix: make the AMS device UI work on non-Bambu printer agents MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolve the filament list's printer model from the agent-reported model id or the selected printer profile, so the AMS Materials Setting dropdown and its Confirm button work on OrcaSonar. Third-party trays pre-fill and allow editing their temps, and the popup's initial selection no longer overwrites them. Render explicitly-empty trays as "Empty" instead of "?" using a wire-derived DevAmsTray::is_empty, while keeping the slot editable. Hide the flow-dynamics K/N controls for non-Bambu agents — tile, dialog, validation and sends — with Bambu behavior unchanged. Tests cover model resolution, the K/N gate and empty-tray classification. --- src/libslic3r/PresetBundle.cpp | 26 +++- src/libslic3r/PresetBundle.hpp | 3 + src/slic3r/GUI/AMSDryControl.cpp | 11 +- src/slic3r/GUI/AMSMaterialsSetting.cpp | 120 ++++++++++++------ src/slic3r/GUI/AMSMaterialsSetting.hpp | 5 +- src/slic3r/GUI/CaliHistoryDialog.cpp | 8 +- src/slic3r/GUI/DeviceCore/DevFilaSystem.cpp | 15 ++- src/slic3r/GUI/DeviceCore/DevFilaSystem.h | 5 +- src/slic3r/GUI/DeviceManager.cpp | 42 +++++- src/slic3r/GUI/DeviceManager.hpp | 10 ++ .../GUI/DeviceTab/uiAMSBestPositionPopup.cpp | 10 +- src/slic3r/GUI/StatusPanel.cpp | 10 +- src/slic3r/GUI/Widgets/AMSItem.cpp | 28 ++-- src/slic3r/GUI/Widgets/AMSItem.hpp | 2 + .../libslic3r/test_preset_bundle_loading.cpp | 17 +++ tests/slic3rutils/CMakeLists.txt | 1 + tests/slic3rutils/test_ams_item.cpp | 86 +++++++++++++ .../test_device_manager_integration.cpp | 51 ++++++++ 18 files changed, 376 insertions(+), 74 deletions(-) create mode 100644 tests/slic3rutils/test_ams_item.cpp diff --git a/src/libslic3r/PresetBundle.cpp b/src/libslic3r/PresetBundle.cpp index be2fce983a..7fce63eec5 100644 --- a/src/libslic3r/PresetBundle.cpp +++ b/src/libslic3r/PresetBundle.cpp @@ -4273,9 +4273,21 @@ std::vector> PresetBundle::get_extruder_filament return filament_infos; } -// ORCA TODO: currently, this function assumes the printer name follows the pattern of " ", e.g. -// printer_type: "Bambu Lab X2D", nozzle_diameter_str: "0.4 nozzle" => printer_name: "Bambu Lab X2D 0.4 nozzle". If the printer name does -// not follow this pattern, the function may not work correctly. +std::string PresetBundle::get_printer_model_display_name(const std::string &model_id) const +{ + if (model_id.empty()) + return {}; + for (const auto &vendor_entry : vendors) { + for (const auto &model : vendor_entry.second.models) { + if (model.model_id == model_id) + return model.name; + } + } + return {}; +} + +// ORCA TODO: this assumes printer names follow " ", e.g. +// "Bambu Lab X2D 0.4 nozzle". Other naming schemes may not resolve correctly. std::set PresetBundle::get_printer_names_by_printer_type_and_nozzle(const std::string &printer_type, std::string nozzle_diameter_str, bool system_only) { std::set printer_names; @@ -4297,7 +4309,9 @@ std::set PresetBundle::get_printer_names_by_printer_type_and_nozzle if (printer_it->name.find(nozzle_diameter_str) != std::string::npos) printer_names.insert(printer_it->name); } - assert(printer_names.size() == 1); + // No match is normal for a connected machine the user has not installed; only an + // ambiguous match is a bug (the caller assumes one preset per model and nozzle). + assert(printer_names.size() <= 1); for (auto& printer_name : printer_names) { BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " " << __LINE__ << " printer name: " << printer_name; @@ -4310,8 +4324,8 @@ std::vector PresetBundle::get_filament_presets_for_machine(const std:: const std::string &nozzle_diameter_str, bool include_user_presets) { - // Printer model plus nozzle diameter is expected to resolve to a single system printer preset; - // get_printer_names_by_printer_type_and_nozzle asserts as much in debug builds. + // Printer model plus nozzle diameter normally resolves to a single system printer preset. + // Zero matches is normal for a connected machine the user never installed. const std::set printer_names = get_printer_names_by_printer_type_and_nozzle(printer_type, nozzle_diameter_str); const Preset *printer = printer_names.empty() ? nullptr : printers.find_preset(*printer_names.begin()); if (printer == nullptr) diff --git a/src/libslic3r/PresetBundle.hpp b/src/libslic3r/PresetBundle.hpp index 5a8f34e706..fdb7d13802 100644 --- a/src/libslic3r/PresetBundle.hpp +++ b/src/libslic3r/PresetBundle.hpp @@ -391,6 +391,9 @@ public: std::vector> get_extruder_filament_info() const; + // Resolve a vendor model id (an agent-reported machine identity) to the display name + // printer profiles use as printer_model. Empty when no loaded vendor declares it. + std::string get_printer_model_display_name(const std::string &model_id) const; std::set get_printer_names_by_printer_type_and_nozzle(const std::string &printer_type, std::string nozzle_diameter_str, bool system_only = true); // Orca: the root filament presets a connected machine can use, resolved with the rule the rest // of the app applies (is_compatible_with_printer): an empty compatible_printers means every diff --git a/src/slic3r/GUI/AMSDryControl.cpp b/src/slic3r/GUI/AMSDryControl.cpp index 17d7d62ad8..4284613e2d 100644 --- a/src/slic3r/GUI/AMSDryControl.cpp +++ b/src/slic3r/GUI/AMSDryControl.cpp @@ -7,6 +7,7 @@ #include "slic3r/GUI/DeviceCore/DevExtruderSystem.h" #include "slic3r/GUI/DeviceCore/DevManager.h" +#include "slic3r/GUI/DeviceManager.hpp" #include "slic3r/GUI/MsgDialog.hpp" @@ -1602,8 +1603,16 @@ int AMSDryCtrWin::update_filament_list(DevAms* dev_ams, MachineObject* obj) stream << std::fixed << std::setprecision(1) << obj->GetExtderSystem()->GetNozzleDiameter(extruder_id); std::string nozzle_diameter_str = stream.str(); + // The connected device's model may not resolve (OrcaSonar's is optional); the + // helper falls back to the selected profile so the list is never empty. + const std::string filament_printer_model = resolve_filament_printer_model(obj->printer_type, preset_bundle); + if (filament_printer_model.empty()) { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << " cannot resolve a printer model for the filament list"; + return false; + } + for (Preset *filament_it : preset_bundle->get_filament_presets_for_machine( - DevPrinterConfigUtil::get_printer_display_name(obj->printer_type), nozzle_diameter_str, obj->is_support_user_preset)) { + filament_printer_model, nozzle_diameter_str, obj->is_support_user_preset)) { if (!filament_id_set.insert(filament_it->filament_id).second) continue; const std::string filament_alias = filaments.get_preset_alias(*filament_it, true); diff --git a/src/slic3r/GUI/AMSMaterialsSetting.cpp b/src/slic3r/GUI/AMSMaterialsSetting.cpp index f0fdf950a0..61818deda9 100644 --- a/src/slic3r/GUI/AMSMaterialsSetting.cpp +++ b/src/slic3r/GUI/AMSMaterialsSetting.cpp @@ -431,6 +431,11 @@ void AMSMaterialsSetting::update_filament_editing(bool is_printing) m_button_confirm->Show(true); } + // A third-party tray owns its temp range; BBL RFID trays keep the read-only preset values. + const bool can_edit = !is_printing || obj->is_support_filament_setting_inprinting; + m_input_nozzle_min->Enable(m_is_third && can_edit); + m_input_nozzle_max->Enable(m_is_third && can_edit); + if (!m_is_third) { m_tip_readonly->SetLabelText(wxEmptyString); m_tip_readonly->Hide(); @@ -453,6 +458,8 @@ void AMSMaterialsSetting::update_filament_editing(bool is_printing) if (m_view_only) { // Orca: view-only (2D laser/cut) — lock every edit control and hide apply/reset m_comboBox_filament->Enable(false); m_comboBox_cali_result->Enable(false); + m_input_nozzle_min->Enable(false); + m_input_nozzle_max->Enable(false); m_input_k_val->Enable(false); m_input_n_val->Enable(false); m_button_confirm->Hide(); @@ -502,7 +509,7 @@ void AMSMaterialsSetting::on_select_reset(wxCommandEvent& event) { } // set k / n value - if (obj->cali_version <= -1 && obj->get_printer_series() == PrinterSeries::SERIES_P1P) { + if (obj->supports_extrusion_cali() && obj->cali_version <= -1 && obj->get_printer_series() == PrinterSeries::SERIES_P1P) { // set extrusion cali ratio int cali_tray_id = ams_id * 4 + slot_id; @@ -523,7 +530,7 @@ void AMSMaterialsSetting::on_select_reset(wxCommandEvent& event) { } obj->command_extrusion_cali_set(cali_tray_id, "", "", k, n); } - else { + else if (obj->supports_extrusion_cali()) { PACalibIndexInfo select_index_info; int tray_id = ams_id * 4 + slot_id; if (is_virtual_tray()) { @@ -705,7 +712,7 @@ void AMSMaterialsSetting::on_select_ok(wxCommandEvent &event) wxString k_text = m_input_k_val->GetTextCtrl()->GetValue(); wxString n_text = m_input_n_val->GetTextCtrl()->GetValue(); - if (obj->cali_version <= -1 && (obj->get_printer_series() != PrinterSeries::SERIES_X1) && !ExtrusionCalibration::check_k_validation(k_text)) { + if (obj->supports_extrusion_cali() && obj->cali_version <= -1 && (obj->get_printer_series() != PrinterSeries::SERIES_X1) && !ExtrusionCalibration::check_k_validation(k_text)) { wxString k_tips = wxString::Format(_L("Please input a valid value (K in %.1f~%.1f)"), MIN_PA_K_VALUE, MAX_PA_K_VALUE); wxString kn_tips = wxString::Format(_L("Please input a valid value (K in %.1f~%.1f, N in %.1f~%.1f)"), MIN_PA_K_VALUE, MAX_PA_K_VALUE, 0.6, 2.0); MessageDialog msg_dlg(nullptr, k_tips, wxEmptyString, wxICON_WARNING | wxOK); @@ -735,7 +742,7 @@ void AMSMaterialsSetting::on_select_ok(wxCommandEvent &event) vt_tray = VIRTUAL_TRAY_DEPUTY_ID; } - if (obj->cali_version >= 0) { + if (obj->supports_extrusion_cali() && obj->cali_version >= 0) { PACalibIndexInfo select_index_info; select_index_info.tray_id = vt_tray; select_index_info.ams_id = ams_id; @@ -754,7 +761,7 @@ void AMSMaterialsSetting::on_select_ok(wxCommandEvent &event) CalibUtils::select_PA_calib_result(select_index_info); } - else { + else if (obj->supports_extrusion_cali()) { obj->command_extrusion_cali_set(vt_tray, "", "", k, n); } } @@ -776,7 +783,7 @@ void AMSMaterialsSetting::on_select_ok(wxCommandEvent &event) ; } - if (obj->cali_version >= 0) { + if (obj->supports_extrusion_cali() && obj->cali_version >= 0) { PACalibIndexInfo select_index_info; select_index_info.tray_id = cali_tray_id; select_index_info.ams_id = ams_id; @@ -795,7 +802,7 @@ void AMSMaterialsSetting::on_select_ok(wxCommandEvent &event) CalibUtils::select_PA_calib_result(select_index_info); } - else { + else if (obj->supports_extrusion_cali()) { obj->command_extrusion_cali_set(cali_tray_id, "", "", k, n); } } @@ -891,7 +898,12 @@ bool AMSMaterialsSetting::is_virtual_tray() void AMSMaterialsSetting::update_widgets() { - if (obj && obj->get_printer_series() == PrinterSeries::SERIES_X1 && obj->cali_version <= -1) { + if (obj && !obj->supports_extrusion_cali()) { + // No printer-side K/N records: do not offer them. + m_panel_normal->Show(); + m_panel_kn->Hide(); + } + else if (obj && obj->get_printer_series() == PrinterSeries::SERIES_X1 && obj->cali_version <= -1) { // Low version firmware does not display k value m_panel_kn->Hide(); } @@ -952,6 +964,9 @@ void AMSMaterialsSetting::Popup(wxString filament, wxString sn, wxString temp_mi m_input_k_val->GetTextCtrl()->SetValue(k); m_input_n_val->GetTextCtrl()->SetValue(n); + // Tray values are re-supplied by every caller; clear so a previous popup cannot leak in. + m_input_nozzle_min->GetTextCtrl()->SetValue(wxEmptyString); + m_input_nozzle_max->GetTextCtrl()->SetValue(wxEmptyString); wxArrayString filament_items; wxString bambu_filament_name; @@ -975,11 +990,14 @@ void AMSMaterialsSetting::Popup(wxString filament, wxString sn, wxString temp_mi } stream << std::fixed << std::setprecision(1) << machine_diameter; std::string nozzle_diameter_str = stream.str(); + // OrcaSonar's model id is optional, so the device model alone can resolve to nothing; + // the helper falls back to the selected profile so the dropdown is never empty. + const std::string filament_printer_model = resolve_filament_printer_model(obj->printer_type, preset_bundle); - if (preset_bundle) { + if (preset_bundle && !filament_printer_model.empty()) { BOOST_LOG_TRIVIAL(trace) << "system_preset_bundle filament number=" << preset_bundle->filaments.size(); for (Preset *filament_it : preset_bundle->get_filament_presets_for_machine( - DevPrinterConfigUtil::get_printer_display_name(obj->printer_type), nozzle_diameter_str, obj->is_support_user_preset)) { + filament_printer_model, nozzle_diameter_str, obj->is_support_user_preset)) { if (!filament_id_set.insert(filament_it->filament_id).second) continue; const std::string alias = preset_bundle->filaments.get_preset_alias(*filament_it, true); @@ -1045,6 +1063,12 @@ void AMSMaterialsSetting::Popup(wxString filament, wxString sn, wxString temp_mi else { m_comboBox_filament->Show(); m_readonly_filament->Hide(); + // A printer-set tray carries its own temps; the preset selection only fills + // them when the tray has none. + if (!temp_min.IsEmpty() && temp_min != "0") + m_input_nozzle_min->GetTextCtrl()->SetValue(temp_min); + if (!temp_max.IsEmpty() && temp_max != "0") + m_input_nozzle_max->GetTextCtrl()->SetValue(temp_max); } if (obj->cali_version >= 0) { @@ -1181,7 +1205,12 @@ void AMSMaterialsSetting::Popup(wxString filament, wxString sn, wxString temp_mi } } + if (filament_items.IsEmpty()) + BOOST_LOG_TRIVIAL(warning) << "ams_materials_setting: no filament presets for printer model \"" + << filament_printer_model << "\" at nozzle " << nozzle_diameter_str; + m_comboBox_filament->Set(filament_items); + m_comboBox_from_printer = true; m_comboBox_filament->SetSelection(selection_idx); post_select_event(selection_idx); @@ -1189,9 +1218,6 @@ void AMSMaterialsSetting::Popup(wxString filament, wxString sn, wxString temp_mi m_comboBox_filament->SetValue(wxEmptyString); } - // Set the flag whether to open the filament setting dialog from the device page - m_comboBox_filament->SetClientData(new int(1)); - update(); Layout(); Fit(); @@ -1220,49 +1246,52 @@ 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()); + // True for the popup's own initial selection: the dialog pre-filled the tray's + // temps, so the matching preset must not overwrite them. + const bool initial_printer_selection = m_comboBox_from_printer; m_filament_type = ""; PresetBundle* preset_bundle = wxGetApp().preset_bundle; + std::string filament_printer_model; if (preset_bundle) { std::ostringstream stream; if (obj) { - // Defensive: this dialog is opened only from StatusPanel (BBL-only) today, so the fallback fires - // only during the brief BBL startup window before firmware reports nozzle info. Without this, - // the "0.0" lookup string returns an empty set and filament lookup yields no results. + // Use the selected profile's nozzle diameter until the connected device reports one. float machine_diameter = obj->GetExtderSystem()->GetNozzleDiameter(0); if (machine_diameter == 0.0f) { const ConfigOption *opt = preset_bundle->printers.get_selected_preset().config.option("nozzle_diameter"); if (opt) machine_diameter = static_cast(opt)->values[0]; } stream << std::fixed << std::setprecision(1) << machine_diameter; + filament_printer_model = resolve_filament_printer_model(obj->printer_type, preset_bundle); } std::string nozzle_diameter_str = stream.str(); // Resolve the selection against the same list Popup() built the dropdown from, so the two // halves of the dialog cannot disagree about which filaments this machine can use. const std::string selected = m_comboBox_filament->GetValue().ToStdString(); - if (!selected.empty()) { + if (!selected.empty() && !filament_printer_model.empty()) { const std::string filament_id = map_filament_items[selected].filament_id; for (Preset *it : preset_bundle->get_filament_presets_for_machine( - DevPrinterConfigUtil::get_printer_display_name(obj->printer_type), nozzle_diameter_str, obj->is_support_user_preset)) { + filament_printer_model, nozzle_diameter_str, obj->is_support_user_preset)) { if (it->filament_id != filament_id) continue; - // ) if nozzle_temperature_range is found - ConfigOption* opt_min = it->config.option("nozzle_temperature_range_low"); - if (opt_min) { - ConfigOptionInts* opt_min_ints = dynamic_cast(opt_min); - if (opt_min_ints) { - wxString text_nozzle_temp_min = wxString::Format("%d", opt_min_ints->get_at(0)); - m_input_nozzle_min->GetTextCtrl()->SetValue(text_nozzle_temp_min); + if (!initial_printer_selection) { + // ) if nozzle_temperature_range is found + ConfigOption* opt_min = it->config.option("nozzle_temperature_range_low"); + if (opt_min) { + ConfigOptionInts* opt_min_ints = dynamic_cast(opt_min); + if (opt_min_ints) { + wxString text_nozzle_temp_min = wxString::Format("%d", opt_min_ints->get_at(0)); + m_input_nozzle_min->GetTextCtrl()->SetValue(text_nozzle_temp_min); + } } - } - ConfigOption* opt_max = it->config.option("nozzle_temperature_range_high"); - if (opt_max) { - ConfigOptionInts* opt_max_ints = dynamic_cast(opt_max); - if (opt_max_ints) { - wxString text_nozzle_temp_max = wxString::Format("%d", opt_max_ints->get_at(0)); - m_input_nozzle_max->GetTextCtrl()->SetValue(text_nozzle_temp_max); + ConfigOption* opt_max = it->config.option("nozzle_temperature_range_high"); + if (opt_max) { + ConfigOptionInts* opt_max_ints = dynamic_cast(opt_max); + if (opt_max_ints) { + wxString text_nozzle_temp_max = wxString::Format("%d", opt_max_ints->get_at(0)); + m_input_nozzle_max->GetTextCtrl()->SetValue(text_nozzle_temp_max); + } } } ConfigOption* opt_type = it->config.option("filament_type"); @@ -1301,13 +1330,16 @@ void AMSMaterialsSetting::on_select_filament(wxCommandEvent &evt) m_button_confirm->Disable(); // ORCA No need to change style m_comboBox_cali_result->Clear(); m_comboBox_cali_result->SetValue(wxEmptyString); - m_input_k_val->GetTextCtrl()->SetValue(wxEmptyString); - m_input_n_val->GetTextCtrl()->SetValue(wxEmptyString); - m_comboBox_filament->SetClientData(new int(0)); + if (!initial_printer_selection) { + m_input_k_val->GetTextCtrl()->SetValue(wxEmptyString); + m_input_n_val->GetTextCtrl()->SetValue(wxEmptyString); + } + m_comboBox_from_printer = false; return; } else { - m_button_confirm->Enable(true); // ORCA No need to change style + if (!m_view_only) + m_button_confirm->Enable(true); // ORCA No need to change style } //filament id @@ -1331,6 +1363,9 @@ void AMSMaterialsSetting::on_select_filament(wxCommandEvent &evt) } } + if (!ams_filament_id.empty()) + m_clr_picker->is_empty(false); + wxArrayString items; m_pa_profile_items.clear(); m_comboBox_cali_result->SetValue(wxEmptyString); @@ -1386,7 +1421,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 (initial_printer_selection) { 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); @@ -1403,10 +1438,11 @@ void AMSMaterialsSetting::on_select_filament(wxCommandEvent &evt) } } else { - if (from_printer && (*from_printer == 1)) { + if (initial_printer_selection) { DevAmsTray* selected_tray = this->obj->GetFilaSystem()->GetAmsTray(std::to_string(ams_id), std::to_string(slot_id)); if (!selected_tray) { + m_comboBox_from_printer = false; return; } @@ -1437,7 +1473,7 @@ void AMSMaterialsSetting::on_select_filament(wxCommandEvent &evt) else { if (!ams_filament_id.empty()) { //m_input_k_val->GetTextCtrl()->SetValue("0.00"); - m_input_k_val->Enable(true); + m_input_k_val->Enable(!m_view_only); } else { //m_input_k_val->GetTextCtrl()->SetValue("0.00"); @@ -1445,7 +1481,7 @@ void AMSMaterialsSetting::on_select_filament(wxCommandEvent &evt) } } - m_comboBox_filament->SetClientData(new int(0)); + m_comboBox_from_printer = false; } void AMSMaterialsSetting::on_dpi_changed(const wxRect &suggested_rect) diff --git a/src/slic3r/GUI/AMSMaterialsSetting.hpp b/src/slic3r/GUI/AMSMaterialsSetting.hpp index 2cbd58e9a9..96118d706d 100644 --- a/src/slic3r/GUI/AMSMaterialsSetting.hpp +++ b/src/slic3r/GUI/AMSMaterialsSetting.hpp @@ -122,7 +122,7 @@ public: std::string ams_filament_id; std::string ams_setting_id; - bool m_is_third; + bool m_is_third = false; // 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; @@ -182,6 +182,9 @@ protected: wxStaticText* m_n_param; TextInput* m_input_n_val; int m_filament_selection; + // True while the popup's initial, printer-driven selection is being handled; it + // keeps that selection from overwriting the tray's own temps with preset temps. + bool m_comboBox_from_printer = false; int m_pa_cali_select_id = 0; diff --git a/src/slic3r/GUI/CaliHistoryDialog.cpp b/src/slic3r/GUI/CaliHistoryDialog.cpp index 3ce8c1bb81..8a6422a77f 100644 --- a/src/slic3r/GUI/CaliHistoryDialog.cpp +++ b/src/slic3r/GUI/CaliHistoryDialog.cpp @@ -714,9 +714,15 @@ wxArrayString NewCalibrationHistoryDialog::get_all_filaments(const MachineObject std::string nozzle_diameter_str = stream.str(); if (preset_bundle) { + // OrcaSonar's model id is optional; the helper falls back to the selected profile + // so the list is never empty. + const std::string filament_printer_model = resolve_filament_printer_model(obj->printer_type, preset_bundle); + if (filament_printer_model.empty()) + return filament_items; + BOOST_LOG_TRIVIAL(trace) << "system_preset_bundle filament number=" << preset_bundle->filaments.size(); for (Preset *filament_it : preset_bundle->get_filament_presets_for_machine( - DevPrinterConfigUtil::get_printer_display_name(obj->printer_type), nozzle_diameter_str, obj->is_support_user_preset)) { + filament_printer_model, nozzle_diameter_str, obj->is_support_user_preset)) { if (!filament_id_set.insert(filament_it->filament_id).second) continue; const std::string alias = preset_bundle->filaments.get_preset_alias(*filament_it, true); diff --git a/src/slic3r/GUI/DeviceCore/DevFilaSystem.cpp b/src/slic3r/GUI/DeviceCore/DevFilaSystem.cpp index 2cce41db9d..cb5ad9ffe5 100644 --- a/src/slic3r/GUI/DeviceCore/DevFilaSystem.cpp +++ b/src/slic3r/GUI/DeviceCore/DevFilaSystem.cpp @@ -11,6 +11,8 @@ #include "DevUtil.h" #include "DevUtilBackend.h" +#include + using namespace nlohmann; namespace Slic3r { @@ -42,6 +44,15 @@ void DevAmsTray::UpdateColorFromStr(const std::string& color) } } +void DevAmsTray::UpdateEmptyState(bool material_fields_present) +{ + const auto is_zero_or_empty = [](const std::string& value) { + return value.empty() || std::all_of(value.begin(), value.end(), [](char c) { return c == '0'; }); + }; + is_empty = material_fields_present && setting_id.empty() && m_fila_type.empty() && + is_zero_or_empty(color) && is_zero_or_empty(tag_uid); +} + void DevAmsTray::reset() { tag_uid = ""; @@ -63,6 +74,7 @@ void DevAmsTray::reset() k = 0.0f; n = 0.0f; is_bbl = false; + is_empty = false; hold_count = 0; remain = 0; } @@ -662,7 +674,7 @@ void DevFilaSystemParser::ParseV1_0(const json& jj, MachineObject* obj, DevFilaS //std::string type = (*tray_it)["tray_type"].get(); std::string type = MachineObject::setting_id_to_type(curr_tray->setting_id, (*tray_it)["tray_type"].get()); // curr_tray->setting_id is our OF id; GFS00/GFS01 are the printer's own support-filament ids. - auto* agent = GUI::wxGetApp().getAgent(); + auto* agent = wxTheApp != nullptr ? GUI::wxGetApp().getAgent() : nullptr; const std::string printer_filament_id = agent ? agent->from_orca_filament_id(curr_tray->setting_id) : curr_tray->setting_id; if (printer_filament_id == "GFS00") { @@ -762,6 +774,7 @@ void DevFilaSystemParser::ParseV1_0(const json& jj, MachineObject* obj, DevFilaS { curr_tray->remain = -1; } + curr_tray->UpdateEmptyState(tray_it->contains("tray_info_idx") && tray_it->contains("tray_type")); // The tray objects are reused across status updates. Reset this // state when a previously empty slot receives a filament again. curr_tray->is_slot_placeholder = tray_it->contains("tray_slot_placeholder"); diff --git a/src/slic3r/GUI/DeviceCore/DevFilaSystem.h b/src/slic3r/GUI/DeviceCore/DevFilaSystem.h index 51fb744f72..9fd1ec17cc 100644 --- a/src/slic3r/GUI/DeviceCore/DevFilaSystem.h +++ b/src/slic3r/GUI/DeviceCore/DevFilaSystem.h @@ -82,6 +82,7 @@ public: wxColour wx_color; bool is_bbl; bool is_exists = false; + bool is_empty = false; // Explicitly reported as having no filament. bool is_slot_placeholder = false; // Orca: True for empty tray slots from pull-mode agents int hold_count = 0; int remain = 0; // filament remain: 0 ~ 100 @@ -90,13 +91,15 @@ public: // operators bool operator==(DevAmsTray const& o) const { - return id == o.id && m_fila_type == o.m_fila_type && filament_setting_id == o.filament_setting_id && color == o.color; + return id == o.id && m_fila_type == o.m_fila_type && filament_setting_id == o.filament_setting_id && + color == o.color && is_empty == o.is_empty; } bool operator!=(DevAmsTray const& o) const { return !operator==(o); } // setters void reset(); void UpdateColorFromStr(const std::string& color); + void UpdateEmptyState(bool material_fields_present); void set_hold_count() { hold_count = HOLD_COUNT_MAX; } // getter diff --git a/src/slic3r/GUI/DeviceManager.cpp b/src/slic3r/GUI/DeviceManager.cpp index 859a525f45..d9beec51e1 100644 --- a/src/slic3r/GUI/DeviceManager.cpp +++ b/src/slic3r/GUI/DeviceManager.cpp @@ -374,6 +374,32 @@ NozzleVolumeType convert_to_nozzle_type(const std::string &str) return res; } +std::string resolve_filament_printer_model(const std::string& printer_type, PresetBundle* preset_bundle) +{ + if (!preset_bundle) + return {}; + + // Devices with a shipped printer config (Bambu) resolve directly. + if (const std::string display_name = DevPrinterConfigUtil::get_printer_display_name(printer_type); !display_name.empty()) + return display_name; + + // A vendor model id the device reported (SSDP modelNumber, manual binding). + if (!printer_type.empty()) { + if (const std::string model_name = preset_bundle->get_printer_model_display_name(printer_type); !model_name.empty()) + return model_name; + } + + // OrcaSonar's model id is optional; a generic device resolves against the selected profile. + if (const ConfigOptionString* model = preset_bundle->printers.get_selected_preset().config.opt("printer_model"); + model && !model->value.empty()) { + BOOST_LOG_TRIVIAL(info) << "resolve_filament_printer_model: device type \"" << printer_type + << "\" has no installed model; using the selected profile \"" << model->value << "\""; + return model->value; + } + + return {}; +} + wxString MachineObject::get_printer_type_display_str() const { std::string display_name = DevPrinterConfigUtil::get_printer_display_name(printer_type); @@ -1759,6 +1785,12 @@ bool MachineObject::orca_ams_command_supported(const char* command) const return command != nullptr && supported_commands.count(command) != 0; } +bool MachineObject::supports_extrusion_cali() const +{ + // Devices with no agent id predate the agent split and keep the Bambu path. + return printer_agent_id.empty() || printer_agent_id == BBL_PRINTER_AGENT_ID; +} + int MachineObject::command_ams_filament_settings(int ams_id, int slot_id, std::string filament_id, std::string setting_id, std::string tray_color, std::string tray_type, int nozzle_temp_min, int nozzle_temp_max) { // OrcaSonar: writing slot metadata is gated on the filament_slots capability. @@ -2850,6 +2882,7 @@ int MachineObject::local_publish_json(std::string json_str, int qos, int flag) std::string MachineObject::setting_id_to_type(std::string setting_id, std::string tray_type) { std::string type; + if (wxTheApp == nullptr) return tray_type; PresetBundle* preset_bundle = GUI::wxGetApp().preset_bundle; if (preset_bundle) { for (auto it = preset_bundle->filaments.begin(); it != preset_bundle->filaments.end(); it++) { @@ -4186,6 +4219,9 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_ 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()); + // The ack carries the whole slot; re-derive empty so the panel flips off + // "Empty" without waiting out the hold. + vt_slot[0].UpdateEmptyState(true); // delay update vt_slot[0].set_hold_count(); } else { @@ -4211,6 +4247,9 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_ 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()); + // The ack carries the whole slot; re-derive empty so the panel flips off + // "Empty" without waiting out the hold. + tray_it->second->UpdateEmptyState(true); // delay update tray_it->second->set_hold_count(); } else { @@ -5148,7 +5187,7 @@ DevAmsTray MachineObject::parse_vt_tray(json vtray) //std::string type = vtray["tray_type"].get(); std::string type = setting_id_to_type(vt_tray.setting_id, vtray["tray_type"].get()); // vt_tray.setting_id is our OF id (translated on the way in); the two support ids below are the printer's own. - auto* agent = GUI::wxGetApp().getAgent(); + auto* agent = wxTheApp != nullptr ? GUI::wxGetApp().getAgent() : nullptr; const std::string printer_filament_id = agent ? agent->from_orca_filament_id(vt_tray.setting_id) : vt_tray.setting_id; if (printer_filament_id == "GFS00") { vt_tray.m_fila_type = "PLA-S"; @@ -5243,6 +5282,7 @@ DevAmsTray MachineObject::parse_vt_tray(json vtray) else { vt_tray.remain = -1; } + vt_tray.UpdateEmptyState(vtray.contains("tray_info_idx") && vtray.contains("tray_type")); } return vt_tray; diff --git a/src/slic3r/GUI/DeviceManager.hpp b/src/slic3r/GUI/DeviceManager.hpp index 21608d946a..d470fb6703 100644 --- a/src/slic3r/GUI/DeviceManager.hpp +++ b/src/slic3r/GUI/DeviceManager.hpp @@ -67,6 +67,7 @@ class DeviceErrorDialog; // Previous definitions class NetworkAgent; // Orca: ManualPaCaliMethod now provided by DeviceCore/DevCalib.h (enum class) +class PresetBundle; #define UpgradeNoError 0 #define UpgradeDownloadFailed -1 @@ -102,6 +103,11 @@ struct DevPrintTaskRatingInfo; // given nozzle diameter (mm), bucketed per nozzle size to mirror the printer firmware. bool is_stringing_prone_filament(const std::string& filament_id, float nozzle_diameter); +// The printer model filament presets are matched against for a connected machine. OrcaSonar's +// model id is optional, so a device with no installed vendor model falls back to the selected +// printer profile rather than resolving to no filament list at all. +std::string resolve_filament_printer_model(const std::string& printer_type, PresetBundle* preset_bundle); + class MachineObject { private: @@ -799,6 +805,10 @@ public: // always allow; an Orca device must have advertised the command (unknown // capability is not support). bool orca_ams_command_supported(const char* command) const; + // The per-tray flow-dynamics K/N records are a Bambu firmware feature. Other + // agents have no printer-side calibration to read or write, so the AMS UI must + // not offer K/N for them. + bool supports_extrusion_cali() const; // ams controls //int command_ams_switch(int tray_index, int old_temp = 210, int new_temp = 210); int command_ams_change_filament(bool load, std::string ams_id, std::string slot_id, int old_temp = 210, int new_temp = 210, std::optional extruder_id = std::nullopt); diff --git a/src/slic3r/GUI/DeviceTab/uiAMSBestPositionPopup.cpp b/src/slic3r/GUI/DeviceTab/uiAMSBestPositionPopup.cpp index 8505a559d6..1fe13ef5fe 100644 --- a/src/slic3r/GUI/DeviceTab/uiAMSBestPositionPopup.cpp +++ b/src/slic3r/GUI/DeviceTab/uiAMSBestPositionPopup.cpp @@ -943,7 +943,11 @@ int ReselectMachineDialog::CaculateSwitcherDistribution(MachineObject* obj, cons const auto& can = ams.cans[j]; auto id = getTrayID(obj, ams.ams_id, can.can_id); auto material = can.material_name; - if (can.material_state == AMSCanType::AMS_CAN_TYPE_THIRDBRAND || + if (can.is_empty || can.material_state == AMSCanType::AMS_CAN_TYPE_EMPTY) + { + material = L("Empty"); + } + else if (can.material_state == AMSCanType::AMS_CAN_TYPE_THIRDBRAND || can.material_state == AMSCanType::AMS_CAN_TYPE_BRAND || can.material_state == AMSCanType::AMS_CAN_TYPE_VIRTUAL) { @@ -952,10 +956,6 @@ int ReselectMachineDialog::CaculateSwitcherDistribution(MachineObject* obj, cons material = L("?"); } } - if (can.material_state == AMSCanType::AMS_CAN_TYPE_EMPTY) - { - material = "Empty"; - } auto itOK = std::find_if(posOK.begin(), posOK.end(), [&](const trayHelper& tray){ auto amsID = std::get<0>(tray); diff --git a/src/slic3r/GUI/StatusPanel.cpp b/src/slic3r/GUI/StatusPanel.cpp index da852fe535..1cd4db3f20 100644 --- a/src/slic3r/GUI/StatusPanel.cpp +++ b/src/slic3r/GUI/StatusPanel.cpp @@ -4603,12 +4603,14 @@ void StatusPanel::on_filament_edit(wxCommandEvent &event) } m_filament_setting_dlg->m_is_third = !DevFilaSystem::IsBBL_Filament(tray->tag_uid); + // The tray's own temps pre-fill the dialog for every tray type; a BBL tray + // additionally carries the SN and brand. + temp_max = tray->nozzle_temp_max; + temp_min = tray->nozzle_temp_min; if (!m_filament_setting_dlg->m_is_third) { sn_number = tray->uuid; filament = tray->sub_brands; - temp_max = tray->nozzle_temp_max; - temp_min = tray->nozzle_temp_min; } } @@ -4669,11 +4671,11 @@ void StatusPanel::on_ext_spool_edit(wxCommandEvent &event) } m_filament_setting_dlg->m_is_third = !DevFilaSystem::IsBBL_Filament(obj->vt_slot[nozzle_index].tag_uid); + temp_max = obj->vt_slot[nozzle_index].nozzle_temp_max; + temp_min = obj->vt_slot[nozzle_index].nozzle_temp_min; if (!m_filament_setting_dlg->m_is_third) { sn_number = obj->vt_slot[nozzle_index].uuid; filament = obj->vt_slot[nozzle_index].sub_brands; - temp_max = obj->vt_slot[nozzle_index].nozzle_temp_max; - temp_min = obj->vt_slot[nozzle_index].nozzle_temp_min; } m_filament_setting_dlg->Move(wxPoint(current_position_x,current_position_y)); diff --git a/src/slic3r/GUI/Widgets/AMSItem.cpp b/src/slic3r/GUI/Widgets/AMSItem.cpp index 9d5ec9c1c1..eb1d0fcf3d 100644 --- a/src/slic3r/GUI/Widgets/AMSItem.cpp +++ b/src/slic3r/GUI/Widgets/AMSItem.cpp @@ -72,6 +72,7 @@ bool AMSinfo::parse_ams_info(MachineObject *obj, DevAms *ams, bool remain_flag, Caninfo info; // tray is exists if (it != ams->GetTrays().end() && it->second->is_exists) { + info.is_empty = it->second->is_empty; if (it->second->is_tray_info_ready()) { info.can_id = it->second->id; info.ctype = it->second->ctype; @@ -139,6 +140,7 @@ void AMSinfo::parse_ext_info(MachineObject* obj, DevAmsTray tray) { this->ams_type = AMSModel::EXT_AMS; Caninfo info; info.can_id = std::to_string(0); + info.is_empty = tray.is_empty; this->cans.clear(); if (tray.id == std::to_string(VIRTUAL_TRAY_MAIN_ID)) @@ -1101,9 +1103,9 @@ void AMSLib::render_lite_text(wxDC& dc) dc.SetTextForeground(temp_text_colour); auto libsize = GetSize(); - if (m_info.material_state == AMSCanType::AMS_CAN_TYPE_THIRDBRAND + if (!m_info.is_empty && (m_info.material_state == AMSCanType::AMS_CAN_TYPE_THIRDBRAND || m_info.material_state == AMSCanType::AMS_CAN_TYPE_BRAND - || m_info.material_state == AMSCanType::AMS_CAN_TYPE_VIRTUAL) { + || m_info.material_state == AMSCanType::AMS_CAN_TYPE_VIRTUAL)) { if (m_info.material_name.empty()) { auto tsize = dc.GetMultiLineTextExtent("?"); @@ -1151,7 +1153,7 @@ void AMSLib::render_lite_text(wxDC& dc) } } - if (m_info.material_state == AMSCanType::AMS_CAN_TYPE_EMPTY) { + if (m_info.material_state == AMSCanType::AMS_CAN_TYPE_EMPTY || m_info.is_empty) { auto tsize = dc.GetMultiLineTextExtent(_L("/")); auto pot = wxPoint((libsize.x - tsize.x) / 2 + FromDIP(2), (libsize.y - tsize.y) / 2 + FromDIP(3)); dc.DrawText(_L("/"), pot); @@ -1160,8 +1162,12 @@ void AMSLib::render_lite_text(wxDC& dc) void AMSLib::render_generic_text(wxDC &dc) { - bool show_k_value = true; - if (m_info.material_name.empty()) { + // K/N is Bambu firmware's flow-dynamics calibration; agents with no printer-side + // records (OrcaSonar, Moonraker) must not show a synthesized value. + const bool k_supported = !m_obj || m_obj->supports_extrusion_cali(); + const bool show_kn = m_show_kn && k_supported; + bool show_k_value = k_supported; + if (!k_supported || m_info.material_name.empty()) { show_k_value = false; } else if (m_info.cali_idx == -1 || (m_obj && (CalibUtils::get_selected_calib_idx(m_obj->pa_calib_tab, m_info.cali_idx) == -1))) { @@ -1202,9 +1208,9 @@ void AMSLib::render_generic_text(wxDC &dc) } auto libsize = GetSize(); - if (m_info.material_state == AMSCanType::AMS_CAN_TYPE_THIRDBRAND + if (!m_info.is_empty && (m_info.material_state == AMSCanType::AMS_CAN_TYPE_THIRDBRAND || m_info.material_state == AMSCanType::AMS_CAN_TYPE_BRAND - || m_info.material_state == AMSCanType::AMS_CAN_TYPE_VIRTUAL) { + || m_info.material_state == AMSCanType::AMS_CAN_TYPE_VIRTUAL)) { if (m_info.material_name.empty() /*&& m_info.material_state != AMSCanType::AMS_CAN_TYPE_VIRTUAL*/) { auto tsize = dc.GetMultiLineTextExtent("?"); @@ -1242,7 +1248,7 @@ void AMSLib::render_generic_text(wxDC &dc) auto line_top_tsize = dc.GetMultiLineTextExtent(line_top); auto line_bottom_tsize = dc.GetMultiLineTextExtent(line_bottom); - if (!m_show_kn) { + if (!show_kn) { auto pot_top = wxPoint((libsize.x - line_top_tsize.x) / 2, (libsize.y - line_top_tsize.y) / 2 - line_top_tsize.y + FromDIP(6)); dc.DrawText(line_top, pot_top); @@ -1273,7 +1279,7 @@ void AMSLib::render_generic_text(wxDC &dc) //draw k&n if (m_obj && show_k_value) { - if (m_show_kn) { + if (show_kn) { wxString str_k = wxString::Format("K %1.3f", m_info.k); wxString str_n = wxString::Format("N %1.3f", m_info.n); dc.SetFont(::Label::Body_11); @@ -1284,7 +1290,7 @@ void AMSLib::render_generic_text(wxDC &dc) } } - if (m_info.material_state == AMSCanType::AMS_CAN_TYPE_EMPTY) { + if (m_info.material_state == AMSCanType::AMS_CAN_TYPE_EMPTY || m_info.is_empty) { auto tsize = dc.GetMultiLineTextExtent(_L("Empty")); auto pot = wxPoint((libsize.x - tsize.x) / 2, (libsize.y - tsize.y) / 2 + FromDIP(3)); dc.DrawText(_L("Empty"), pot); @@ -2781,7 +2787,7 @@ void AMSPreview::doRender(wxDC &dc) } else { wxRect rect(left, (size.y - AMS_ITEM_CUBE_SIZE.y) / 2, AMS_ITEM_CUBE_SIZE.x, AMS_ITEM_CUBE_SIZE.y); - if (iter->material_state == AMSCanType::AMS_CAN_TYPE_EMPTY) { + if (iter->material_state == AMSCanType::AMS_CAN_TYPE_EMPTY || iter->is_empty) { dc.SetPen(wxPen(wxColor(0, 0, 0))); dc.DrawLine(rect.GetRight() - FromDIP(1), rect.GetTop() + FromDIP(1), rect.GetLeft() + FromDIP(1), rect.GetBottom() - FromDIP(1)); } diff --git a/src/slic3r/GUI/Widgets/AMSItem.hpp b/src/slic3r/GUI/Widgets/AMSItem.hpp index ac4283b7e8..a7908a456e 100644 --- a/src/slic3r/GUI/Widgets/AMSItem.hpp +++ b/src/slic3r/GUI/Widgets/AMSItem.hpp @@ -183,6 +183,7 @@ struct Caninfo wxString material_name; wxColour material_colour = {*wxWHITE}; AMSCanType material_state; + bool is_empty = false; int ctype=0; int material_remain = 100; int cali_idx = -1; @@ -198,6 +199,7 @@ public: material_name == other.material_name && material_colour == other.material_colour && material_state == other.material_state && + is_empty == other.is_empty && ctype == other.ctype && material_remain == other.material_remain && cali_idx == other.cali_idx && diff --git a/tests/libslic3r/test_preset_bundle_loading.cpp b/tests/libslic3r/test_preset_bundle_loading.cpp index 6ec6ba9b1e..4c104cb7dc 100644 --- a/tests/libslic3r/test_preset_bundle_loading.cpp +++ b/tests/libslic3r/test_preset_bundle_loading.cpp @@ -1407,6 +1407,23 @@ TEST_CASE("Filaments offered for a machine follow the app's compatibility rule", } } +// An agent-reported model id (SSDP modelNumber, manual binding) must resolve to the display +// name printer profiles use as printer_model; the AMS dialogs look filaments up by it. +TEST_CASE("Printer model ids resolve to vendor display names", "[Preset][Bundle]") +{ + PresetBundle bundle; + VendorProfile qidi("Qidi"); + qidi.name = "Qidi"; + VendorProfile::PrinterModel model; + model.model_id = "Qidi-Q1Pro"; + model.name = "Qidi Q1 Pro"; + qidi.models.push_back(model); + bundle.vendors.emplace(qidi.id, qidi); + + CHECK(bundle.get_printer_model_display_name("Qidi-Q1Pro") == "Qidi Q1 Pro"); + CHECK(bundle.get_printer_model_display_name("unknown-model").empty()); +} + namespace { diff --git a/tests/slic3rutils/CMakeLists.txt b/tests/slic3rutils/CMakeLists.txt index 3a20eb02e5..b6f7fe29bc 100644 --- a/tests/slic3rutils/CMakeLists.txt +++ b/tests/slic3rutils/CMakeLists.txt @@ -6,6 +6,7 @@ add_executable(${_TEST_NAME}_tests test_dev_mapping.cpp test_filament_bitmap_utils.cpp test_device_progress.cpp + test_ams_item.cpp test_device_manager_integration.cpp test_web_media_controller.cpp test_network_versions.cpp diff --git a/tests/slic3rutils/test_ams_item.cpp b/tests/slic3rutils/test_ams_item.cpp new file mode 100644 index 0000000000..0c92b551fc --- /dev/null +++ b/tests/slic3rutils/test_ams_item.cpp @@ -0,0 +1,86 @@ +// why: match the GUI include order to avoid rpcndr.h byte/std::byte +// ambiguity in the Windows COM headers. +#ifdef WIN32 + #ifndef WIN32_LEAN_AND_MEAN + #define WIN32_LEAN_AND_MEAN + #endif + #ifndef NOMINMAX + #define NOMINMAX + #endif + #include +#endif + +#include + +#include + +#include + +#include "slic3r/GUI/Widgets/AMSItem.hpp" +#include "slic3r/GUI/DeviceCore/DevFilaSystem.h" + +using json = nlohmann::json; +using namespace Slic3r; +using namespace Slic3r::GUI; + +// A configured slot the wire reports as having no filament (all material fields +// present and empty) is "Empty", not the unknown "?" the UI draws for a tray +// whose info is missing. The distinction must survive into the AMS panel model. +TEST_CASE("Configured empty AMS trays remain distinct from unknown trays", "[AMSItem]") +{ + MachineObject machine(nullptr, nullptr, "test", "test-device", "127.0.0.1"); + machine.printer_agent_id = "orca"; + + const json print_json = json::parse(R"({ + "ams": { + "ams_exist_bits": "1", + "tray_exist_bits": "3", + "ams": [ { "id": "0", "info": "0001", "tray": [ + { "id": "0", "tag_uid": "0000000000000000", "tray_info_idx": "", "tray_type": "", "tray_color": "00000000" }, + { "id": "1" } + ] } ] + } + })"); + DevFilaSystemParser::ParseV1_0(print_json, &machine, machine.GetFilaSystem().get(), false); + + const auto& ams_list = machine.GetFilaSystem()->GetAmsList(); + const auto ams_it = ams_list.find("0"); + REQUIRE(ams_it != ams_list.end()); + auto* ams = ams_it->second; + REQUIRE(ams != nullptr); + REQUIRE(ams->GetTray("0") != nullptr); + CHECK(ams->GetTray("0")->is_empty); + + AMSinfo info; + REQUIRE(info.parse_ams_info(&machine, ams)); + REQUIRE(info.cans.size() == 2); + + CHECK(info.cans[0].is_empty); + CHECK(info.cans[0].material_state == AMSCanType::AMS_CAN_TYPE_THIRDBRAND); + CHECK_FALSE(info.cans[1].is_empty); + CHECK(info.cans[1].material_state == AMSCanType::AMS_CAN_TYPE_THIRDBRAND); +} + +TEST_CASE("Empty external slots remain distinct from unknown slots", "[AMSItem]") +{ + MachineObject machine(nullptr, nullptr, "test", "test-device", "127.0.0.1"); + machine.printer_agent_id = "orca"; + + DevAmsTray empty_slot = machine.parse_vt_tray(json::parse(R"({ + "id": "255", "tag_uid": "0000000000000000", "tray_info_idx": "", "tray_type": "", "tray_color": "00000000" + })")); + CHECK(empty_slot.is_empty); + + AMSinfo empty_info; + empty_info.parse_ext_info(&machine, empty_slot); + CHECK(empty_info.cans[0].is_empty); + CHECK(empty_info.cans[0].material_state == AMSCanType::AMS_CAN_TYPE_VIRTUAL); + + DevAmsTray unknown_slot = machine.parse_vt_tray(json::parse(R"({"id": "254"})")); + CHECK_FALSE(unknown_slot.is_empty); + + AMSinfo unknown_info; + unknown_info.parse_ext_info(&machine, unknown_slot); + CHECK_FALSE(unknown_info.cans[0].is_empty); + CHECK(unknown_info.cans[0].material_state == AMSCanType::AMS_CAN_TYPE_VIRTUAL); +} diff --git a/tests/slic3rutils/test_device_manager_integration.cpp b/tests/slic3rutils/test_device_manager_integration.cpp index 4728418c88..d6f65e8993 100644 --- a/tests/slic3rutils/test_device_manager_integration.cpp +++ b/tests/slic3rutils/test_device_manager_integration.cpp @@ -3,6 +3,8 @@ #include #include #include +#include +#include #include #include #include @@ -273,3 +275,52 @@ TEST_CASE("Orca per-command AMS gate requires fms and the advertised command", " obj->printer_agent_id = "bbl"; CHECK(obj->orca_ams_command_supported("print.anything")); } + +// The AMS dialogs resolve their filament list from the connected device's model. OrcaSonar's +// model id is optional (the agent falls back to "orcasonar"), so the resolver must stand in +// with the selected printer profile instead of yielding no model at all. +TEST_CASE("Filament printer model resolution falls back to the selected profile", "[DeviceManager][integration]") +{ + PresetBundle bundle; + + VendorProfile qidi("Qidi"); + qidi.name = "Qidi"; + VendorProfile::PrinterModel model; + model.model_id = "Qidi-Q1Pro"; + model.name = "Qidi Q1 Pro"; + qidi.models.push_back(model); + bundle.vendors.emplace(qidi.id, qidi); + + // A vendor model id the device reported resolves through the vendor catalog. + CHECK(resolve_filament_printer_model("Qidi-Q1Pro", &bundle) == "Qidi Q1 Pro"); + + // The OrcaSonar fallback id has no vendor model; the selected profile stands in. + CHECK(resolve_filament_printer_model("orcasonar", &bundle).empty()); + bundle.printers.get_selected_preset().config.set_key_value("printer_model", new ConfigOptionString("Generic Klipper Printer")); + CHECK(resolve_filament_printer_model("orcasonar", &bundle) == "Generic Klipper Printer"); + CHECK(resolve_filament_printer_model("", &bundle) == "Generic Klipper Printer"); + + CHECK(resolve_filament_printer_model("orcasonar", nullptr).empty()); +} + +// The per-tray K/N records are Bambu firmware's flow-dynamics calibration. Agents with no +// printer-side records must not offer the AMS K/N controls (they would show a synthesized +// default and then refuse to confirm it). +TEST_CASE("Flow-dynamics K/N is offered for Bambu agents only", "[DeviceManager][integration]") +{ + MachineObject bbl(nullptr, nullptr, "test", "bbl-device", "127.0.0.1"); + bbl.printer_agent_id = "bbl"; + CHECK(bbl.supports_extrusion_cali()); + + MachineObject orca(nullptr, nullptr, "test", "orca-device", "127.0.0.1"); + orca.printer_agent_id = "orca"; + CHECK_FALSE(orca.supports_extrusion_cali()); + + MachineObject moonraker(nullptr, nullptr, "test", "moonraker-device", "127.0.0.1"); + moonraker.printer_agent_id = "moonraker"; + CHECK_FALSE(moonraker.supports_extrusion_cali()); + + // No agent id predates the agent split and keeps the Bambu path. + MachineObject legacy(nullptr, nullptr, "test", "legacy-device", "127.0.0.1"); + CHECK(legacy.supports_extrusion_cali()); +}