Compare commits

...
Author SHA1 Message Date
Lam Wei Lun ad8c6e741e Removal of specific ORCA_PRINTER_AGENT_ID usage in DeviceManager/SelectMachine/SendMultiMachinePage. Generalized the capabilities under IPrinterAgent 2026-09-30 18:01:17 +08:00
Lam Wei Lun e45d52efd1 Merge impl branch 2026-09-30 12:42:51 +08:00
Lam Wei Lun 543ca95059 fix: make the AMS device UI work on non-Bambu printer agents
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.
2026-09-29 17:47:24 +08:00
Lam Wei Lun 799fbee86c feat: enable filament mapping and resolve printer-set AMS trays by material type
Drop the index-correlation gate so mapped prints are gated only on the filament_mapping capability
Resolve AMS trays set on the printer UI to the matching Generic preset using their material type and color
Skip the forced external-spool selection on printers without an AMS and add coverage for the mapping gates
2026-09-29 14:02:40 +08:00
Lam Wei Lun adcc470cff feat: add filament mapping and AMS capability gates for the orca printer agent
Parse connector-scope capabilities from get_capabilities: supported_features, capabilities.protocol.features and supported_commands are read for orca devices even when the reply has no flags block, reset before each parse, and left untouched for Bambu.

Gate macro-backed AMS commands on fms plus the advertised command (change filament, select tray, control, user settings, RFID, drying stop); gate slot metadata writes on filament_slots. Unknown capability is not support.

Serialize PrintParams::ams_mapping2 into print.gcode_file.filament_mapping with the array position as filament_index and {255,255} dropped. The field is omitted when empty, so an unmapped print stays byte-identical. A mapped print is refused visibly while ORCA_FILAMENT_MAPPING_CORRELATION_VERIFIED is false or the connector did not advertise filament_mapping; the GUI predicate shares the serializer's definition so both gates agree. Refuse a partially mapped print that leaves a used filament without a target.

Return a non-success result from start_local_print_with_record so PrintJob falls back to start_print instead of treating an unsent print as success.

Tests cover serializer keying and omission, GUI/agent gate agreement, payload shape, the with_record result, capability parsing, and the per-command AMS gate.
2026-09-26 22:50:58 +08:00
33 changed files with 1588 additions and 152 deletions
+39 -7
View File
@@ -3708,6 +3708,13 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
ams_array_maps.push_back(temp); ams_array_maps.push_back(temp);
index++; index++;
if (filament_id.empty()) { if (filament_id.empty()) {
// A tray written by the printer UI carries a material type but no
// OrcaSlicer preset id. Resolve it to the matching Generic preset with
// the tray's own color instead of dropping it (direct sync) or forcing
// "Generic PLA" (mapping sync). Placeholders and typeless trays keep
// the previous behavior.
const auto tray_type = ams.opt_string("filament_type", 0u);
if (is_placeholder || tray_type.empty()) {
if (use_map) { if (use_map) {
for (int j = maps.size() - 1; j >= 0; j--) { for (int j = maps.size() - 1; j >= 0; j--) {
if (maps[j].slot_id == slot_id && maps[j].ams_id == ams_id) { if (maps[j].slot_id == slot_id && maps[j].ams_id == ams_id) {
@@ -3731,19 +3738,28 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
} }
continue; continue;
} }
}
if (!filament_changed && this->filament_presets.size() > ams_filament_presets.size()) { if (!filament_changed && this->filament_presets.size() > ams_filament_presets.size()) {
ams_filament_presets.push_back(this->filament_presets[ams_filament_presets.size()]); ams_filament_presets.push_back(this->filament_presets[ams_filament_presets.size()]);
ams_filament_colors.push_back(filament_color); ams_filament_colors.push_back(filament_color);
ams_filament_color_types.push_back(filament_color_type); ams_filament_color_types.push_back(filament_color_type);
ams_multi_color_filment.push_back(filament_multi_color); ams_multi_color_filment.push_back(filament_multi_color);
ams_infos.back().valid = true;
continue; continue;
} }
bool has_type = false; bool has_type = false;
auto filament_type = ams.opt_string("filament_type", 0u); auto filament_type = ams.opt_string("filament_type", 0u);
auto iter = std::find_if(filaments.begin(), filaments.end(), [this, &filament_id, &has_type, filament_type](auto &f) { auto iter = filaments.end();
if (!filament_id.empty()) {
iter = std::find_if(filaments.begin(), filaments.end(), [this, &filament_id, &has_type, filament_type](auto &f) {
has_type |= f.config.opt_string("filament_type", 0u) == filament_type; has_type |= f.config.opt_string("filament_type", 0u) == filament_type;
return f.is_compatible && filaments.get_preset_base(f) == &f && f.filament_id == filament_id; }); return f.is_compatible && filaments.get_preset_base(f) == &f && f.filament_id == filament_id; });
warn_ambiguous_filament_id_match(filaments, iter, filament_id); warn_ambiguous_filament_id_match(filaments, iter, filament_id);
} else {
// The material type is the only identity a printer-set tray carries.
has_type = std::any_of(filaments.begin(), filaments.end(), [&filament_type](auto &f) {
return f.is_compatible && f.config.opt_string("filament_type", 0u) == filament_type; });
}
if (iter == filaments.end()) { if (iter == filaments.end()) {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": filament_id %1% not found or system or compatible") % filament_id; BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": filament_id %1% not found or system or compatible") % filament_id;
if (!filament_type.empty()) { if (!filament_type.empty()) {
@@ -3790,6 +3806,7 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
ams_filament_colors.push_back(filament_color); ams_filament_colors.push_back(filament_color);
ams_filament_color_types.push_back(filament_color_type); ams_filament_color_types.push_back(filament_color_type);
ams_multi_color_filment.push_back(filament_multi_color); ams_multi_color_filment.push_back(filament_multi_color);
ams_infos.back().valid = true;
unknowns.emplace_back(&ams, has_type ? L("The filament may not be compatible with the current machine settings. Generic filament presets will be used.") : unknowns.emplace_back(&ams, has_type ? L("The filament may not be compatible with the current machine settings. Generic filament presets will be used.") :
L("The filament model is unknown. Still using the previous filament preset.")); L("The filament model is unknown. Still using the previous filament preset."));
continue; continue;
@@ -3816,6 +3833,7 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
ams_filament_colors.push_back(filament_color); ams_filament_colors.push_back(filament_color);
ams_filament_color_types.push_back(filament_color_type); ams_filament_color_types.push_back(filament_color_type);
ams_multi_color_filment.push_back(filament_multi_color); ams_multi_color_filment.push_back(filament_multi_color);
ams_infos.back().valid = true;
} }
if (ams_filament_presets.empty()) if (ams_filament_presets.empty())
return 0; return 0;
@@ -4255,9 +4273,21 @@ std::vector<std::vector<DynamicPrintConfig>> PresetBundle::get_extruder_filament
return filament_infos; return filament_infos;
} }
// ORCA TODO: currently, this function assumes the printer name follows the pattern of "<printer_model> <nozzle_diameter>", e.g. std::string PresetBundle::get_printer_model_display_name(const std::string &model_id) const
// 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. 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 "<printer_model> <nozzle_diameter>", e.g.
// "Bambu Lab X2D 0.4 nozzle". Other naming schemes may not resolve correctly.
std::set<std::string> PresetBundle::get_printer_names_by_printer_type_and_nozzle(const std::string &printer_type, std::string nozzle_diameter_str, bool system_only) std::set<std::string> PresetBundle::get_printer_names_by_printer_type_and_nozzle(const std::string &printer_type, std::string nozzle_diameter_str, bool system_only)
{ {
std::set<std::string> printer_names; std::set<std::string> printer_names;
@@ -4279,7 +4309,9 @@ std::set<std::string> 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); 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) { for (auto& printer_name : printer_names) {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " " << __LINE__ << " printer name: " << printer_name; BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " " << __LINE__ << " printer name: " << printer_name;
@@ -4292,8 +4324,8 @@ std::vector<Preset *> PresetBundle::get_filament_presets_for_machine(const std::
const std::string &nozzle_diameter_str, const std::string &nozzle_diameter_str,
bool include_user_presets) bool include_user_presets)
{ {
// Printer model plus nozzle diameter is expected to resolve to a single system printer preset; // Printer model plus nozzle diameter normally resolves to a single system printer preset.
// get_printer_names_by_printer_type_and_nozzle asserts as much in debug builds. // Zero matches is normal for a connected machine the user never installed.
const std::set<std::string> printer_names = get_printer_names_by_printer_type_and_nozzle(printer_type, nozzle_diameter_str); const std::set<std::string> 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()); const Preset *printer = printer_names.empty() ? nullptr : printers.find_preset(*printer_names.begin());
if (printer == nullptr) if (printer == nullptr)
+3
View File
@@ -391,6 +391,9 @@ public:
std::vector<std::vector<DynamicPrintConfig>> get_extruder_filament_info() const; std::vector<std::vector<DynamicPrintConfig>> 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<std::string> get_printer_names_by_printer_type_and_nozzle(const std::string &printer_type, std::string nozzle_diameter_str, bool system_only = true); std::set<std::string> 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 // 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 // of the app applies (is_compatible_with_printer): an empty compatible_printers means every
+10 -1
View File
@@ -7,6 +7,7 @@
#include "slic3r/GUI/DeviceCore/DevExtruderSystem.h" #include "slic3r/GUI/DeviceCore/DevExtruderSystem.h"
#include "slic3r/GUI/DeviceCore/DevManager.h" #include "slic3r/GUI/DeviceCore/DevManager.h"
#include "slic3r/GUI/DeviceManager.hpp"
#include "slic3r/GUI/MsgDialog.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); stream << std::fixed << std::setprecision(1) << obj->GetExtderSystem()->GetNozzleDiameter(extruder_id);
std::string nozzle_diameter_str = stream.str(); 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( 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) if (!filament_id_set.insert(filament_it->filament_id).second)
continue; continue;
const std::string filament_alias = filaments.get_preset_alias(*filament_it, true); const std::string filament_alias = filaments.get_preset_alias(*filament_it, true);
+61 -25
View File
@@ -431,6 +431,11 @@ void AMSMaterialsSetting::update_filament_editing(bool is_printing)
m_button_confirm->Show(true); 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) { if (!m_is_third) {
m_tip_readonly->SetLabelText(wxEmptyString); m_tip_readonly->SetLabelText(wxEmptyString);
m_tip_readonly->Hide(); 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 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_filament->Enable(false);
m_comboBox_cali_result->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_k_val->Enable(false);
m_input_n_val->Enable(false); m_input_n_val->Enable(false);
m_button_confirm->Hide(); m_button_confirm->Hide();
@@ -502,7 +509,7 @@ void AMSMaterialsSetting::on_select_reset(wxCommandEvent& event) {
} }
// set k / n value // 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 // set extrusion cali ratio
int cali_tray_id = ams_id * 4 + slot_id; 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); obj->command_extrusion_cali_set(cali_tray_id, "", "", k, n);
} }
else { else if (obj->supports_extrusion_cali()) {
PACalibIndexInfo select_index_info; PACalibIndexInfo select_index_info;
int tray_id = ams_id * 4 + slot_id; int tray_id = ams_id * 4 + slot_id;
if (is_virtual_tray()) { 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 k_text = m_input_k_val->GetTextCtrl()->GetValue();
wxString n_text = m_input_n_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 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); 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); 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; vt_tray = VIRTUAL_TRAY_DEPUTY_ID;
} }
if (obj->cali_version >= 0) { if (obj->supports_extrusion_cali() && obj->cali_version >= 0) {
PACalibIndexInfo select_index_info; PACalibIndexInfo select_index_info;
select_index_info.tray_id = vt_tray; select_index_info.tray_id = vt_tray;
select_index_info.ams_id = ams_id; 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); CalibUtils::select_PA_calib_result(select_index_info);
} }
else { else if (obj->supports_extrusion_cali()) {
obj->command_extrusion_cali_set(vt_tray, "", "", k, n); 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; PACalibIndexInfo select_index_info;
select_index_info.tray_id = cali_tray_id; select_index_info.tray_id = cali_tray_id;
select_index_info.ams_id = ams_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); 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); obj->command_extrusion_cali_set(cali_tray_id, "", "", k, n);
} }
} }
@@ -891,7 +898,12 @@ bool AMSMaterialsSetting::is_virtual_tray()
void AMSMaterialsSetting::update_widgets() 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 // Low version firmware does not display k value
m_panel_kn->Hide(); 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_k_val->GetTextCtrl()->SetValue(k);
m_input_n_val->GetTextCtrl()->SetValue(n); 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; wxArrayString filament_items;
wxString bambu_filament_name; 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; stream << std::fixed << std::setprecision(1) << machine_diameter;
std::string nozzle_diameter_str = stream.str(); 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(); BOOST_LOG_TRIVIAL(trace) << "system_preset_bundle filament number=" << preset_bundle->filaments.size();
for (Preset *filament_it : preset_bundle->get_filament_presets_for_machine( 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) if (!filament_id_set.insert(filament_it->filament_id).second)
continue; continue;
const std::string alias = preset_bundle->filaments.get_preset_alias(*filament_it, true); 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 { else {
m_comboBox_filament->Show(); m_comboBox_filament->Show();
m_readonly_filament->Hide(); 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) { 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_filament->Set(filament_items);
m_comboBox_from_printer = true;
m_comboBox_filament->SetSelection(selection_idx); m_comboBox_filament->SetSelection(selection_idx);
post_select_event(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); 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(); update();
Layout(); Layout();
Fit(); Fit();
@@ -1220,34 +1246,36 @@ void AMSMaterialsSetting::on_select_cali_result(wxCommandEvent &evt)
void AMSMaterialsSetting::on_select_filament(wxCommandEvent &evt) void AMSMaterialsSetting::on_select_filament(wxCommandEvent &evt)
{ {
// Get the flag whether to open the filament setting dialog from the device page // True for the popup's own initial selection: the dialog pre-filled the tray's
int* from_printer = static_cast<int*>(m_comboBox_filament->GetClientData()); // temps, so the matching preset must not overwrite them.
const bool initial_printer_selection = m_comboBox_from_printer;
m_filament_type = ""; m_filament_type = "";
PresetBundle* preset_bundle = wxGetApp().preset_bundle; PresetBundle* preset_bundle = wxGetApp().preset_bundle;
std::string filament_printer_model;
if (preset_bundle) { if (preset_bundle) {
std::ostringstream stream; std::ostringstream stream;
if (obj) { if (obj) {
// Defensive: this dialog is opened only from StatusPanel (BBL-only) today, so the fallback fires // Use the selected profile's nozzle diameter until the connected device reports one.
// 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.
float machine_diameter = obj->GetExtderSystem()->GetNozzleDiameter(0); float machine_diameter = obj->GetExtderSystem()->GetNozzleDiameter(0);
if (machine_diameter == 0.0f) { if (machine_diameter == 0.0f) {
const ConfigOption *opt = preset_bundle->printers.get_selected_preset().config.option("nozzle_diameter"); const ConfigOption *opt = preset_bundle->printers.get_selected_preset().config.option("nozzle_diameter");
if (opt) machine_diameter = static_cast<const ConfigOptionFloats *>(opt)->values[0]; if (opt) machine_diameter = static_cast<const ConfigOptionFloats *>(opt)->values[0];
} }
stream << std::fixed << std::setprecision(1) << machine_diameter; 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(); std::string nozzle_diameter_str = stream.str();
// Resolve the selection against the same list Popup() built the dropdown from, so the two // 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. // halves of the dialog cannot disagree about which filaments this machine can use.
const std::string selected = m_comboBox_filament->GetValue().ToStdString(); 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; const std::string filament_id = map_filament_items[selected].filament_id;
for (Preset *it : preset_bundle->get_filament_presets_for_machine( 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) if (it->filament_id != filament_id)
continue; continue;
if (!initial_printer_selection) {
// ) if nozzle_temperature_range is found // ) if nozzle_temperature_range is found
ConfigOption* opt_min = it->config.option("nozzle_temperature_range_low"); ConfigOption* opt_min = it->config.option("nozzle_temperature_range_low");
if (opt_min) { if (opt_min) {
@@ -1265,6 +1293,7 @@ void AMSMaterialsSetting::on_select_filament(wxCommandEvent &evt)
m_input_nozzle_max->GetTextCtrl()->SetValue(text_nozzle_temp_max); m_input_nozzle_max->GetTextCtrl()->SetValue(text_nozzle_temp_max);
} }
} }
}
ConfigOption* opt_type = it->config.option("filament_type"); ConfigOption* opt_type = it->config.option("filament_type");
bool found_filament_type = false; bool found_filament_type = false;
if (opt_type) { if (opt_type) {
@@ -1301,12 +1330,15 @@ void AMSMaterialsSetting::on_select_filament(wxCommandEvent &evt)
m_button_confirm->Disable(); // ORCA No need to change style m_button_confirm->Disable(); // ORCA No need to change style
m_comboBox_cali_result->Clear(); m_comboBox_cali_result->Clear();
m_comboBox_cali_result->SetValue(wxEmptyString); m_comboBox_cali_result->SetValue(wxEmptyString);
if (!initial_printer_selection) {
m_input_k_val->GetTextCtrl()->SetValue(wxEmptyString); m_input_k_val->GetTextCtrl()->SetValue(wxEmptyString);
m_input_n_val->GetTextCtrl()->SetValue(wxEmptyString); m_input_n_val->GetTextCtrl()->SetValue(wxEmptyString);
m_comboBox_filament->SetClientData(new int(0)); }
m_comboBox_from_printer = false;
return; return;
} }
else { else {
if (!m_view_only)
m_button_confirm->Enable(true); // ORCA No need to change style m_button_confirm->Enable(true); // ORCA No need to change style
} }
@@ -1331,6 +1363,9 @@ void AMSMaterialsSetting::on_select_filament(wxCommandEvent &evt)
} }
} }
if (!ams_filament_id.empty())
m_clr_picker->is_empty(false);
wxArrayString items; wxArrayString items;
m_pa_profile_items.clear(); m_pa_profile_items.clear();
m_comboBox_cali_result->SetValue(wxEmptyString); m_comboBox_cali_result->SetValue(wxEmptyString);
@@ -1386,7 +1421,7 @@ void AMSMaterialsSetting::on_select_filament(wxCommandEvent &evt)
m_comboBox_cali_result->Set(items); m_comboBox_cali_result->Set(items);
if (ams_id == VIRTUAL_TRAY_MAIN_ID || ams_id == VIRTUAL_TRAY_DEPUTY_ID) { 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) { for (auto slot : obj->vt_slot) {
if (slot.id == std::to_string(ams_id)) if (slot.id == std::to_string(ams_id))
cali_select_idx = CalibUtils::get_selected_calib_idx(m_pa_profile_items, slot.cali_idx); 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 { 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)); DevAmsTray* selected_tray = this->obj->GetFilaSystem()->GetAmsTray(std::to_string(ams_id), std::to_string(slot_id));
if (!selected_tray) if (!selected_tray)
{ {
m_comboBox_from_printer = false;
return; return;
} }
@@ -1437,7 +1473,7 @@ void AMSMaterialsSetting::on_select_filament(wxCommandEvent &evt)
else { else {
if (!ams_filament_id.empty()) { if (!ams_filament_id.empty()) {
//m_input_k_val->GetTextCtrl()->SetValue("0.00"); //m_input_k_val->GetTextCtrl()->SetValue("0.00");
m_input_k_val->Enable(true); m_input_k_val->Enable(!m_view_only);
} }
else { else {
//m_input_k_val->GetTextCtrl()->SetValue("0.00"); //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) void AMSMaterialsSetting::on_dpi_changed(const wxRect &suggested_rect)
+4 -1
View File
@@ -122,7 +122,7 @@ public:
std::string ams_filament_id; std::string ams_filament_id;
std::string ams_setting_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 // Orca: view-only mode (laser/cut). When set, the dialog is inspectable but every
// editing control is disabled and no command is sent. // editing control is disabled and no command is sent.
bool m_view_only = false; bool m_view_only = false;
@@ -182,6 +182,9 @@ protected:
wxStaticText* m_n_param; wxStaticText* m_n_param;
TextInput* m_input_n_val; TextInput* m_input_n_val;
int m_filament_selection; 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; int m_pa_cali_select_id = 0;
+6 -3
View File
@@ -516,7 +516,8 @@ void AMSSetting::on_insert_material_read(wxCommandEvent &event)
bool tray_read_opt = m_checkbox_Insert_material_auto_read->GetValue(); bool tray_read_opt = m_checkbox_Insert_material_auto_read->GetValue();
bool remain_opt = m_checkbox_remain->GetValue(); bool remain_opt = m_checkbox_remain->GetValue();
m_obj->command_ams_user_settings(start_read_opt, tray_read_opt, remain_opt); if (m_obj->command_ams_user_settings(start_read_opt, tray_read_opt, remain_opt) != 0)
UpdateByObj(m_obj);
m_sizer_Insert_material_tip_inline->Layout(); m_sizer_Insert_material_tip_inline->Layout();
Layout(); Layout();
@@ -542,7 +543,8 @@ void AMSSetting::on_starting_read(wxCommandEvent &event)
bool tray_read_opt = m_checkbox_Insert_material_auto_read->GetValue(); bool tray_read_opt = m_checkbox_Insert_material_auto_read->GetValue();
bool remain_opt = m_checkbox_remain->GetValue(); bool remain_opt = m_checkbox_remain->GetValue();
m_obj->command_ams_user_settings(start_read_opt, tray_read_opt, remain_opt); if (m_obj->command_ams_user_settings(start_read_opt, tray_read_opt, remain_opt) != 0)
UpdateByObj(m_obj);
m_sizer_starting_tip_inline->Layout(); m_sizer_starting_tip_inline->Layout();
Layout(); Layout();
@@ -556,7 +558,8 @@ void AMSSetting::on_remain(wxCommandEvent& event)
bool start_read_opt = m_checkbox_starting_auto_read->GetValue(); bool start_read_opt = m_checkbox_starting_auto_read->GetValue();
bool tray_read_opt = m_checkbox_Insert_material_auto_read->GetValue(); bool tray_read_opt = m_checkbox_Insert_material_auto_read->GetValue();
bool remain_opt = m_checkbox_remain->GetValue(); bool remain_opt = m_checkbox_remain->GetValue();
m_obj->command_ams_user_settings(start_read_opt, tray_read_opt, remain_opt); if (m_obj->command_ams_user_settings(start_read_opt, tray_read_opt, remain_opt) != 0)
UpdateByObj(m_obj);
event.Skip(); event.Skip();
} }
+7 -1
View File
@@ -714,9 +714,15 @@ wxArrayString NewCalibrationHistoryDialog::get_all_filaments(const MachineObject
std::string nozzle_diameter_str = stream.str(); std::string nozzle_diameter_str = stream.str();
if (preset_bundle) { 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(); BOOST_LOG_TRIVIAL(trace) << "system_preset_bundle filament number=" << preset_bundle->filaments.size();
for (Preset *filament_it : preset_bundle->get_filament_presets_for_machine( 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) if (!filament_id_set.insert(filament_it->filament_id).second)
continue; continue;
const std::string alias = preset_bundle->filaments.get_preset_alias(*filament_it, true); const std::string alias = preset_bundle->filaments.get_preset_alias(*filament_it, true);
+14 -1
View File
@@ -11,6 +11,8 @@
#include "DevUtil.h" #include "DevUtil.h"
#include "DevUtilBackend.h" #include "DevUtilBackend.h"
#include <algorithm>
using namespace nlohmann; using namespace nlohmann;
namespace Slic3r { 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() void DevAmsTray::reset()
{ {
tag_uid = ""; tag_uid = "";
@@ -63,6 +74,7 @@ void DevAmsTray::reset()
k = 0.0f; k = 0.0f;
n = 0.0f; n = 0.0f;
is_bbl = false; is_bbl = false;
is_empty = false;
hold_count = 0; hold_count = 0;
remain = 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>(); //std::string type = (*tray_it)["tray_type"].get<std::string>();
std::string type = MachineObject::setting_id_to_type(curr_tray->setting_id, (*tray_it)["tray_type"].get<std::string>()); std::string type = MachineObject::setting_id_to_type(curr_tray->setting_id, (*tray_it)["tray_type"].get<std::string>());
// curr_tray->setting_id is our OF id; GFS00/GFS01 are the printer's own support-filament ids. // 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; 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") 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->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 // The tray objects are reused across status updates. Reset this
// state when a previously empty slot receives a filament again. // state when a previously empty slot receives a filament again.
curr_tray->is_slot_placeholder = tray_it->contains("tray_slot_placeholder"); curr_tray->is_slot_placeholder = tray_it->contains("tray_slot_placeholder");
+4 -1
View File
@@ -82,6 +82,7 @@ public:
wxColour wx_color; wxColour wx_color;
bool is_bbl; bool is_bbl;
bool is_exists = false; 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 bool is_slot_placeholder = false; // Orca: True for empty tray slots from pull-mode agents
int hold_count = 0; int hold_count = 0;
int remain = 0; // filament remain: 0 ~ 100 int remain = 0; // filament remain: 0 ~ 100
@@ -90,13 +91,15 @@ public:
// operators // operators
bool operator==(DevAmsTray const& o) const 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); } bool operator!=(DevAmsTray const& o) const { return !operator==(o); }
// setters // setters
void reset(); void reset();
void UpdateColorFromStr(const std::string& color); void UpdateColorFromStr(const std::string& color);
void UpdateEmptyState(bool material_fields_present);
void set_hold_count() { hold_count = HOLD_COUNT_MAX; } void set_hold_count() { hold_count = HOLD_COUNT_MAX; }
// getter // getter
+104 -7
View File
@@ -374,6 +374,32 @@ NozzleVolumeType convert_to_nozzle_type(const std::string &str)
return res; 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<ConfigOptionString>("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 wxString MachineObject::get_printer_type_display_str() const
{ {
std::string display_name = DevPrinterConfigUtil::get_printer_display_name(printer_type); std::string display_name = DevPrinterConfigUtil::get_printer_display_name(printer_type);
@@ -1676,6 +1702,10 @@ int MachineObject::check_resume_condition()
} }
int MachineObject::command_ams_change_filament(bool load, std::string ams_id, std::string slot_id, int old_temp, int new_temp, std::optional<int> extruder_id) int MachineObject::command_ams_change_filament(bool load, std::string ams_id, std::string slot_id, int old_temp, int new_temp, std::optional<int> extruder_id)
{ {
if (!printer_supports_command("print.ams_change_filament")) {
BOOST_LOG_TRIVIAL(warning) << "command_ams_change_filament: printer agent does not support the command";
return command_with_dialog(ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED);
}
json j; json j;
try { try {
auto tray_id = 0; auto tray_id = 0;
@@ -1719,6 +1749,10 @@ int MachineObject::command_ams_change_filament(bool load, std::string ams_id, st
int MachineObject::command_ams_user_settings(bool start_read_opt, bool tray_read_opt, bool remain_flag) int MachineObject::command_ams_user_settings(bool start_read_opt, bool tray_read_opt, bool remain_flag)
{ {
if (!printer_supports_command("print.ams_user_setting")) {
BOOST_LOG_TRIVIAL(warning) << "command_ams_user_settings: printer agent does not support the command";
return command_with_dialog(ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED);
}
json j; json j;
j["print"]["command"] = "ams_user_setting"; j["print"]["command"] = "ams_user_setting";
j["print"]["sequence_id"] = std::to_string(MachineObject::m_sequence_id++); j["print"]["sequence_id"] = std::to_string(MachineObject::m_sequence_id++);
@@ -1727,22 +1761,59 @@ int MachineObject::command_ams_user_settings(bool start_read_opt, bool tray_read
j["print"]["tray_read_option"] = tray_read_opt; j["print"]["tray_read_option"] = tray_read_opt;
j["print"]["calibrate_remain_flag"] = remain_flag; j["print"]["calibrate_remain_flag"] = remain_flag;
const int rc = this->publish_json(j);
if (rc == 0) {
m_fila_system->GetAmsSystemSetting().SetDetectOnInsertEnabled(tray_read_opt); m_fila_system->GetAmsSystemSetting().SetDetectOnInsertEnabled(tray_read_opt);
m_fila_system->GetAmsSystemSetting().SetDetectOnPowerupEnabled(start_read_opt); m_fila_system->GetAmsSystemSetting().SetDetectOnPowerupEnabled(start_read_opt);
m_fila_system->GetAmsSystemSetting().SetDetectRemainEnabled(remain_flag); m_fila_system->GetAmsSystemSetting().SetDetectRemainEnabled(remain_flag);
ams_user_setting_start = time(nullptr); ams_user_setting_start = time(nullptr);
}
return this->publish_json(j); return rc;
} }
int MachineObject::command_ams_calibrate(int ams_id) int MachineObject::command_ams_calibrate(int ams_id)
{ {
if (!m_agent) return -1; if (!m_agent) return -1;
if (!m_agent->owns_agent(printer_agent_id))
return command_with_dialog(ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED);
return command_with_dialog(m_agent->command_ams_calibrate(get_dev_id(), ams_id, MachineObject::m_sequence_id++, is_lan_mode_printer())); return command_with_dialog(m_agent->command_ams_calibrate(get_dev_id(), ams_id, MachineObject::m_sequence_id++, is_lan_mode_printer()));
} }
bool MachineObject::printer_supports_command(const char* command) const
{
if (!command || !m_agent)
return false;
return m_agent->supports_command(printer_agent_id, get_dev_id(), command);
}
bool MachineObject::printer_uses_filament_mapping() const
{
if (!m_agent)
return false;
// An unrecorded owner goes through owns_agent() like any other, because
// whichever agent is active is also the one formatting the payload. Only a
// recorded owner that differs from the active agent is a mismatch.
return m_agent->uses_filament_mapping(printer_agent_id);
}
bool MachineObject::printer_supports_feature(const char* feature) const
{
return feature && m_agent && m_agent->supports_feature(printer_agent_id, get_dev_id(), feature);
}
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) 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)
{ {
if (!printer_supports_command("print.ams_filament_setting")) {
BOOST_LOG_TRIVIAL(warning) << "command_ams_filament_settings: printer agent does not support slot metadata updates";
return command_with_dialog(ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED);
}
int tag_tray_id = 0; int tag_tray_id = 0;
int tag_ams_id = ams_id; int tag_ams_id = ams_id;
int tag_slot_id = slot_id; int tag_slot_id = slot_id;
@@ -1777,6 +1848,10 @@ int MachineObject::command_ams_filament_settings(int ams_id, int slot_id, std::s
int MachineObject::command_ams_refresh_rfid(int ams_id, int slot_id) int MachineObject::command_ams_refresh_rfid(int ams_id, int slot_id)
{ {
if (!m_agent) return -1; if (!m_agent) return -1;
if (!printer_supports_command("print.ams_get_rfid")) {
BOOST_LOG_TRIVIAL(warning) << "command_ams_refresh_rfid: printer agent does not support the command";
return command_with_dialog(ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED);
}
return command_with_dialog(m_agent->command_ams_refresh_rfid(get_dev_id(), ams_id, slot_id, MachineObject::m_sequence_id++, is_lan_mode_printer())); return command_with_dialog(m_agent->command_ams_refresh_rfid(get_dev_id(), ams_id, slot_id, MachineObject::m_sequence_id++, is_lan_mode_printer()));
} }
@@ -1790,11 +1865,19 @@ int MachineObject::command_start_camera()
int MachineObject::command_ams_select_tray(std::string tray_id) int MachineObject::command_ams_select_tray(std::string tray_id)
{ {
if (!m_agent) return -1; if (!m_agent) return -1;
if (!printer_supports_command("print.ams_change_filament")) {
BOOST_LOG_TRIVIAL(warning) << "command_ams_select_tray: printer agent does not support the command";
return command_with_dialog(ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED);
}
return command_with_dialog(m_agent->command_ams_select_tray(get_dev_id(), tray_id, MachineObject::m_sequence_id++, is_lan_mode_printer())); return command_with_dialog(m_agent->command_ams_select_tray(get_dev_id(), tray_id, MachineObject::m_sequence_id++, is_lan_mode_printer()));
} }
int MachineObject::command_ams_control(std::string action) int MachineObject::command_ams_control(std::string action)
{ {
if (!printer_supports_command("print.ams_control")) {
BOOST_LOG_TRIVIAL(warning) << "command_ams_control: printer agent does not support the command";
return command_with_dialog(ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED);
}
if (action == "resume" && check_resume_condition()) return 0; if (action == "resume" && check_resume_condition()) return 0;
//valid actions //valid actions
@@ -1810,6 +1893,10 @@ int MachineObject::command_ams_control(std::string action)
int MachineObject::command_ams_drying_stop() int MachineObject::command_ams_drying_stop()
{ {
if (!printer_supports_command("print.auto_stop_ams_dry")) {
BOOST_LOG_TRIVIAL(warning) << "command_ams_drying_stop: printer agent does not support the command";
return command_with_dialog(ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED);
}
json j; json j;
j["print"]["command"] = "auto_stop_ams_dry"; j["print"]["command"] = "auto_stop_ams_dry";
j["print"]["sequence_id"] = std::to_string(MachineObject::m_sequence_id++); j["print"]["sequence_id"] = std::to_string(MachineObject::m_sequence_id++);
@@ -2760,7 +2847,9 @@ bool MachineObject::is_camera_busy_off()
int MachineObject::publish_json(const json& json_item, int qos, int flag) int MachineObject::publish_json(const json& json_item, int qos, int flag)
{ {
int rtn = 0; int rtn = 0;
if (is_lan_mode_printer()) { if (m_agent && !m_agent->owns_agent(printer_agent_id)) {
rtn = ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED;
} else if (is_lan_mode_printer()) {
rtn = local_publish_json(json_item.dump(), qos, flag); rtn = local_publish_json(json_item.dump(), qos, flag);
} else { } else {
rtn = cloud_publish_json(json_item.dump(), qos, flag); rtn = cloud_publish_json(json_item.dump(), qos, flag);
@@ -2804,6 +2893,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 MachineObject::setting_id_to_type(std::string setting_id, std::string tray_type)
{ {
std::string type; std::string type;
if (wxTheApp == nullptr) return tray_type;
PresetBundle* preset_bundle = GUI::wxGetApp().preset_bundle; PresetBundle* preset_bundle = GUI::wxGetApp().preset_bundle;
if (preset_bundle) { if (preset_bundle) {
for (auto it = preset_bundle->filaments.begin(); it != preset_bundle->filaments.end(); it++) { for (auto it = preset_bundle->filaments.begin(); it != preset_bundle->filaments.end(); it++) {
@@ -4140,6 +4230,9 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_
vt_slot[0].setting_id = jj["tray_info_idx"].get<std::string>(); vt_slot[0].setting_id = jj["tray_info_idx"].get<std::string>();
//vt_tray.type = jj["tray_type"].get<std::string>(); //vt_tray.type = jj["tray_type"].get<std::string>();
vt_slot[0].m_fila_type = setting_id_to_type(vt_slot[0].setting_id, jj["tray_type"].get<std::string>()); vt_slot[0].m_fila_type = setting_id_to_type(vt_slot[0].setting_id, jj["tray_type"].get<std::string>());
// 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 // delay update
vt_slot[0].set_hold_count(); vt_slot[0].set_hold_count();
} else { } else {
@@ -4165,6 +4258,9 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_
tray_it->second->setting_id = jj["tray_info_idx"].get<std::string>(); tray_it->second->setting_id = jj["tray_info_idx"].get<std::string>();
tray_it->second->m_fila_type = setting_id_to_type(tray_it->second->setting_id, jj["tray_type"].get<std::string>()); tray_it->second->m_fila_type = setting_id_to_type(tray_it->second->setting_id, jj["tray_type"].get<std::string>());
// 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 // delay update
tray_it->second->set_hold_count(); tray_it->second->set_hold_count();
} else { } else {
@@ -5102,7 +5198,7 @@ DevAmsTray MachineObject::parse_vt_tray(json vtray)
//std::string type = vtray["tray_type"].get<std::string>(); //std::string type = vtray["tray_type"].get<std::string>();
std::string type = setting_id_to_type(vt_tray.setting_id, vtray["tray_type"].get<std::string>()); std::string type = setting_id_to_type(vt_tray.setting_id, vtray["tray_type"].get<std::string>());
// vt_tray.setting_id is our OF id (translated on the way in); the two support ids below are the printer's own. // 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; 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") { if (printer_filament_id == "GFS00") {
vt_tray.m_fila_type = "PLA-S"; vt_tray.m_fila_type = "PLA-S";
@@ -5197,6 +5293,7 @@ DevAmsTray MachineObject::parse_vt_tray(json vtray)
else { else {
vt_tray.remain = -1; vt_tray.remain = -1;
} }
vt_tray.UpdateEmptyState(vtray.contains("tray_info_idx") && vtray.contains("tray_type"));
} }
return vt_tray; return vt_tray;
@@ -5490,11 +5587,11 @@ void MachineObject::parse_new_info2(const json& info)
if (capabilities_it == info.end() || !capabilities_it->is_object()) if (capabilities_it == info.end() || !capabilities_it->is_object())
return; return;
const auto flags_it = capabilities_it->find("flags"); const auto flags_it = capabilities_it->find("flags");
if (flags_it == capabilities_it->end() || !flags_it->is_object()) const bool has_flags = flags_it != capabilities_it->end() && flags_it->is_object();
if (!has_flags)
return; return;
const json& flags = *flags_it; const json& flags = *flags_it;
BOOST_LOG_TRIVIAL(info) << "parse_new_info2: OrcaSonar capability flags=" << flags.dump(); BOOST_LOG_TRIVIAL(info) << "parse_new_info2: capability flags=" << flags.dump();
auto parse_bool = [&flags](const char* name, bool& target) { auto parse_bool = [&flags](const char* name, bool& target) {
const auto it = flags.find(name); const auto it = flags.find(name);
+14
View File
@@ -8,6 +8,7 @@
#include <string> #include <string>
#include <memory> #include <memory>
#include <chrono> #include <chrono>
#include <set>
#include <unordered_set> #include <unordered_set>
#include <optional> #include <optional>
#include <boost/thread.hpp> #include <boost/thread.hpp>
@@ -66,6 +67,7 @@ class DeviceErrorDialog; // Previous definitions
class NetworkAgent; class NetworkAgent;
// Orca: ManualPaCaliMethod now provided by DeviceCore/DevCalib.h (enum class) // Orca: ManualPaCaliMethod now provided by DeviceCore/DevCalib.h (enum class)
class PresetBundle;
#define UpgradeNoError 0 #define UpgradeNoError 0
#define UpgradeDownloadFailed -1 #define UpgradeDownloadFailed -1
@@ -101,6 +103,11 @@ struct DevPrintTaskRatingInfo;
// given nozzle diameter (mm), bucketed per nozzle size to mirror the printer firmware. // 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); 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 class MachineObject
{ {
private: private:
@@ -787,6 +794,13 @@ public:
int command_refresh_nozzle(); int command_refresh_nozzle();
int command_set_chamber(int temp); int command_set_chamber(int temp);
int check_resume_condition(); int check_resume_condition();
bool printer_supports_command(const char* command) const;
bool printer_supports_feature(const char* feature) const;
bool printer_uses_filament_mapping() 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 // ams controls
//int command_ams_switch(int tray_index, int old_temp = 210, int new_temp = 210); //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<int> extruder_id = std::nullopt); 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<int> extruder_id = std::nullopt);
@@ -943,7 +943,11 @@ int ReselectMachineDialog::CaculateSwitcherDistribution(MachineObject* obj, cons
const auto& can = ams.cans[j]; const auto& can = ams.cans[j];
auto id = getTrayID(obj, ams.ams_id, can.can_id); auto id = getTrayID(obj, ams.ams_id, can.can_id);
auto material = can.material_name; 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_BRAND ||
can.material_state == AMSCanType::AMS_CAN_TYPE_VIRTUAL) can.material_state == AMSCanType::AMS_CAN_TYPE_VIRTUAL)
{ {
@@ -952,10 +956,6 @@ int ReselectMachineDialog::CaculateSwitcherDistribution(MachineObject* obj, cons
material = L("?"); 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 itOK = std::find_if(posOK.begin(), posOK.end(), [&](const trayHelper& tray){
auto amsID = std::get<0>(tray); auto amsID = std::get<0>(tray);
+105
View File
@@ -0,0 +1,105 @@
#pragma once
#include <string>
#include <vector>
#include <nlohmann/json.hpp>
#include "libslic3r/ProjectTask.hpp"
#include "DeviceManager.hpp"
namespace Slic3r {
namespace GUI {
// True when the normalized ams_mapping2 carries any entry the agent's serializer
// would put on the wire: every integer pair except the {255,255} unmatched
// sentinel, external slots ({255,0}/{254,0}) included. This mirrors
// OrcaPrinterAgent::build_filament_mapping exactly, so the GUI gate and the
// agent gate agree; a mismatch lets an entry reach the agent and be refused late
// with a generic publish error instead of the designed message.
inline bool has_engaged_filament_mapping(const std::string& ams_mapping2)
{
const nlohmann::json mapping = nlohmann::json::parse(ams_mapping2, nullptr, false);
if (mapping.is_discarded() || !mapping.is_array())
return false;
for (const auto& entry : mapping) {
if (!entry.is_object())
continue;
const auto ams_id_it = entry.find("ams_id");
const auto slot_id_it = entry.find("slot_id");
if (ams_id_it == entry.end() || slot_id_it == entry.end())
continue;
if (!ams_id_it->is_number_integer() || !slot_id_it->is_number_integer())
continue;
if (ams_id_it->get<int>() == 255 && slot_id_it->get<int>() == 255)
continue; // unmatched/unused sentinel: the serializer drops it
return true;
}
return false;
}
// A device with no AMS units has a single source: the external spool. OrcaSlicer's
// auto-mapping force-selects it for every filament (DevMapping.cpp), but that is not
// a lane choice: keep it out of the send gate and off print.gcode_file.
inline void drop_forced_external_selection(bool device_has_ams, std::string& ams_mapping2)
{
if (!device_has_ams)
ams_mapping2.clear();
}
// A used filament with no target would be silently dropped from the wire
// mapping, so the print must be refused rather than run the wrong material.
// m_ams_mapping_result carries exactly the filaments the slice uses.
inline bool has_used_filament_without_target(const std::vector<FilamentInfo>& result)
{
for (const auto& f : result) {
if (f.get_ams_id() < 0 || f.get_slot_id() < 0)
return true;
}
return false;
}
// True when at least one used filament already has a target. The used-unmapped
// refusal applies to a partially mapped print; a print with no mapping at all
// is handled by the existing all-invalid send-path flow.
inline bool has_any_mapped_target(const std::vector<FilamentInfo>& result)
{
for (const auto& f : result) {
if (f.get_ams_id() >= 0 && f.get_slot_id() >= 0)
return true;
}
return false;
}
// Refusal reason for a printer agent that serializes lane selection into
// print.gcode_file's per-print filament_mapping field.
enum class MappingSendError {
none, // no refusal; ams_mapping2 is normalized for send
unsupported, // a mapping is engaged but the connector did not advertise it
incomplete, // a used filament has no target while others do
};
// Applies the send-time mapping policy and normalizes ams_mapping2 in place.
// Agents that do not speak filament_mapping keep their legacy payload untouched.
inline MappingSendError prepare_filament_mapping_for_send(MachineObject* obj,
std::string& ams_mapping2,
const std::vector<FilamentInfo>& mapping_result)
{
if (!obj || !obj->printer_uses_filament_mapping())
return MappingSendError::none;
// A device with no AMS units has one source, the external spool:
// auto-mapping force-selects it, which is not a lane choice. Drop it before
// the capability gate so it cannot refuse a print nobody mapped.
drop_forced_external_selection(obj->HasAms(), ams_mapping2);
if (!obj->printer_supports_feature("filament_mapping") && has_engaged_filament_mapping(ams_mapping2))
return MappingSendError::unsupported;
if (has_any_mapped_target(mapping_result) && has_used_filament_without_target(mapping_result))
return MappingSendError::incomplete;
return MappingSendError::none;
}
} // namespace GUI
} // namespace Slic3r
+27
View File
@@ -37,6 +37,8 @@
#include "libslic3r/MultiNozzleUtils.hpp" // filament-change-gap model for the best-position popup #include "libslic3r/MultiNozzleUtils.hpp" // filament-change-gap model for the best-position popup
#include "BackgroundSlicingProcess.hpp" // complete type for background_process().get_current_gcode_result() #include "BackgroundSlicingProcess.hpp" // complete type for background_process().get_current_gcode_result()
#include "DeviceCore/DevStorage.h" #include "DeviceCore/DevStorage.h"
#include "slic3r/Utils/NetworkAgentFactory.hpp"
#include "FilamentMappingUtils.hpp"
#include <wx/progdlg.h> #include <wx/progdlg.h>
#include <wx/clipbrd.h> #include <wx/clipbrd.h>
@@ -3478,6 +3480,9 @@ void SelectMachineDialog::navigate_to_timelapse_page()
this->EndModal(wxID_CANCEL); this->EndModal(wxID_CANCEL);
} }
// Mapping helpers live in FilamentMappingUtils.hpp (shared with
// SendMultiMachinePage); they mirror the agent serializer exactly.
void SelectMachineDialog::on_send_print() void SelectMachineDialog::on_send_print()
{ {
BOOST_LOG_TRIVIAL(info) << "print_job: on_ok to send"; BOOST_LOG_TRIVIAL(info) << "print_job: on_ok to send";
@@ -3532,6 +3537,28 @@ void SelectMachineDialog::on_send_print()
get_ams_mapping_result(ams_mapping_array,ams_mapping_array2, ams_mapping_info); get_ams_mapping_result(ams_mapping_array,ams_mapping_array2, ams_mapping_info);
// The policy lives in FilamentMappingUtils.hpp; this dialog only differs
// from the multi-device page in how it reports a refusal.
switch (prepare_filament_mapping_for_send(obj_, ams_mapping_array2, m_ams_mapping_result)) {
case MappingSendError::unsupported:
BOOST_LOG_TRIVIAL(warning) << "print_job: connector does not advertise filament_mapping; refusing mapped print";
m_status_bar->set_status_text(_L("AMS filament mapping is not available for this printer. Clear the AMS mapping before printing."));
Enable_Send_Button(true);
return;
case MappingSendError::incomplete:
BOOST_LOG_TRIVIAL(warning) << "print_job: a used filament has no AMS target; refusing print";
m_status_bar->set_status_text(_L("A filament used by this print has no AMS mapping. Assign it before printing."));
Enable_Send_Button(true);
return;
case MappingSendError::none:
break;
default:
// A refusal added without a handler here must not silently send.
BOOST_LOG_TRIVIAL(warning) << "print_job: unrecognized mapping refusal; refusing print";
Enable_Send_Button(true);
return;
}
if (m_print_type == PrintFromType::FROM_NORMAL) { if (m_print_type == PrintFromType::FROM_NORMAL) {
result = m_plater->send_gcode(m_print_plate_idx, [this](int export_stage, int current, int total, bool& cancel) { result = m_plater->send_gcode(m_print_plate_idx, [this](int export_stage, int current, int total, bool& cancel) {
if (this->m_is_canceled) return; if (this->m_is_canceled) return;
+28
View File
@@ -10,6 +10,8 @@
#include "DeviceCore/DevManager.h" #include "DeviceCore/DevManager.h"
#include "DeviceCore/DevStorage.h" #include "DeviceCore/DevStorage.h"
#include "slic3r/Utils/NetworkAgentFactory.hpp"
#include "FilamentMappingUtils.hpp"
namespace Slic3r { namespace Slic3r {
namespace GUI { namespace GUI {
@@ -692,6 +694,8 @@ bool SendMultiMachinePage::get_ams_mapping_result(std::string &mapping_array_str
return true; return true;
} }
// Mapping helpers live in FilamentMappingUtils.hpp, shared with SelectMachine.
void SendMultiMachinePage::on_send(wxCommandEvent& event) void SendMultiMachinePage::on_send(wxCommandEvent& event)
{ {
event.Skip(); event.Skip();
@@ -740,6 +744,30 @@ void SendMultiMachinePage::on_send(wxCommandEvent& event)
if (!wxGetApp().is_blocking_printing(obj)) { if (!wxGetApp().is_blocking_printing(obj)) {
PrintParams params = request_params(obj); PrintParams params = request_params(obj);
switch (prepare_filament_mapping_for_send(obj, params.ams_mapping2, m_ams_mapping_result)) {
case MappingSendError::unsupported:
{
BOOST_LOG_TRIVIAL(warning) << "SendMultiMachinePage: connector does not advertise filament_mapping; refusing mapped print for "
<< obj->get_dev_id();
MessageDialog unavailable_msg(nullptr, _L("AMS filament mapping is not available for this printer. Clear the AMS mapping before printing."), "", wxICON_WARNING | wxOK);
unavailable_msg.ShowModal();
return;
}
case MappingSendError::incomplete:
{
BOOST_LOG_TRIVIAL(warning) << "SendMultiMachinePage: a used filament has no target; refusing print for " << obj->get_dev_id();
MessageDialog incomplete_msg(nullptr, _L("A filament used by this print has no AMS mapping. Assign it before printing."), "", wxICON_WARNING | wxOK);
incomplete_msg.ShowModal();
return;
}
case MappingSendError::none:
break;
default:
// A refusal added without a handler here must not silently send.
BOOST_LOG_TRIVIAL(warning) << "SendMultiMachinePage: unrecognized mapping refusal for "
<< obj->get_dev_id();
return;
}
print_params.push_back(params); print_params.push_back(params);
} }
} }
+6 -4
View File
@@ -4634,12 +4634,14 @@ void StatusPanel::on_filament_edit(wxCommandEvent &event)
} }
m_filament_setting_dlg->m_is_third = !DevFilaSystem::IsBBL_Filament(tray->tag_uid); 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) if (!m_filament_setting_dlg->m_is_third)
{ {
sn_number = tray->uuid; sn_number = tray->uuid;
filament = tray->sub_brands; filament = tray->sub_brands;
temp_max = tray->nozzle_temp_max;
temp_min = tray->nozzle_temp_min;
} }
} }
@@ -4700,11 +4702,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); 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) { if (!m_filament_setting_dlg->m_is_third) {
sn_number = obj->vt_slot[nozzle_index].uuid; sn_number = obj->vt_slot[nozzle_index].uuid;
filament = obj->vt_slot[nozzle_index].sub_brands; 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)); m_filament_setting_dlg->Move(wxPoint(current_position_x,current_position_y));
+6
View File
@@ -1373,6 +1373,12 @@ bool SyncAmsInfoDialog::get_ams_mapping_result(std::string &mapping_array_str, s
BOOST_LOG_TRIVIAL(error) << "get_ams_mapping_result, plater is nullptr"; BOOST_LOG_TRIVIAL(error) << "get_ams_mapping_result, plater is nullptr";
} }
// mapping_v1_json is built one entry per filament preset, in preset order, so the
// array position is the logical filament index the generated G-code toolchange
// references (the identifier handed to the Klipper toolchange macro; see
// OrcaPrinterAgent::build_filament_mapping and the index-correlation test in
// tests/slic3rutils/test_orca_printer_agent.cpp). Never re-densify after dropping
// sentinel entries, or a used filament would be aimed at the wrong lane.
for (int i = 0; i < wxGetApp().preset_bundle->filament_presets.size(); i++) { for (int i = 0; i < wxGetApp().preset_bundle->filament_presets.size(); i++) {
int tray_id = -1; int tray_id = -1;
json mapping_item_v1; json mapping_item_v1;
+17 -11
View File
@@ -72,6 +72,7 @@ bool AMSinfo::parse_ams_info(MachineObject *obj, DevAms *ams, bool remain_flag,
Caninfo info; Caninfo info;
// tray is exists // tray is exists
if (it != ams->GetTrays().end() && it->second->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()) { if (it->second->is_tray_info_ready()) {
info.can_id = it->second->id; info.can_id = it->second->id;
info.ctype = it->second->ctype; info.ctype = it->second->ctype;
@@ -139,6 +140,7 @@ void AMSinfo::parse_ext_info(MachineObject* obj, DevAmsTray tray) {
this->ams_type = AMSModel::EXT_AMS; this->ams_type = AMSModel::EXT_AMS;
Caninfo info; Caninfo info;
info.can_id = std::to_string(0); info.can_id = std::to_string(0);
info.is_empty = tray.is_empty;
this->cans.clear(); this->cans.clear();
if (tray.id == std::to_string(VIRTUAL_TRAY_MAIN_ID)) 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); dc.SetTextForeground(temp_text_colour);
auto libsize = GetSize(); 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_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()) { if (m_info.material_name.empty()) {
auto tsize = dc.GetMultiLineTextExtent("?"); 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 tsize = dc.GetMultiLineTextExtent(_L("/"));
auto pot = wxPoint((libsize.x - tsize.x) / 2 + FromDIP(2), (libsize.y - tsize.y) / 2 + FromDIP(3)); auto pot = wxPoint((libsize.x - tsize.x) / 2 + FromDIP(2), (libsize.y - tsize.y) / 2 + FromDIP(3));
dc.DrawText(_L("/"), pot); dc.DrawText(_L("/"), pot);
@@ -1160,8 +1162,12 @@ void AMSLib::render_lite_text(wxDC& dc)
void AMSLib::render_generic_text(wxDC &dc) void AMSLib::render_generic_text(wxDC &dc)
{ {
bool show_k_value = true; // K/N is Bambu firmware's flow-dynamics calibration; agents with no printer-side
if (m_info.material_name.empty()) { // 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; 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))) { 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(); 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_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*/) { if (m_info.material_name.empty() /*&& m_info.material_state != AMSCanType::AMS_CAN_TYPE_VIRTUAL*/) {
auto tsize = dc.GetMultiLineTextExtent("?"); 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_top_tsize = dc.GetMultiLineTextExtent(line_top);
auto line_bottom_tsize = dc.GetMultiLineTextExtent(line_bottom); 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)); 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); dc.DrawText(line_top, pot_top);
@@ -1273,7 +1279,7 @@ void AMSLib::render_generic_text(wxDC &dc)
//draw k&n //draw k&n
if (m_obj && show_k_value) { 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_k = wxString::Format("K %1.3f", m_info.k);
wxString str_n = wxString::Format("N %1.3f", m_info.n); wxString str_n = wxString::Format("N %1.3f", m_info.n);
dc.SetFont(::Label::Body_11); 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 tsize = dc.GetMultiLineTextExtent(_L("Empty"));
auto pot = wxPoint((libsize.x - tsize.x) / 2, (libsize.y - tsize.y) / 2 + FromDIP(3)); auto pot = wxPoint((libsize.x - tsize.x) / 2, (libsize.y - tsize.y) / 2 + FromDIP(3));
dc.DrawText(_L("Empty"), pot); dc.DrawText(_L("Empty"), pot);
@@ -2781,7 +2787,7 @@ void AMSPreview::doRender(wxDC &dc)
} }
else { else {
wxRect rect(left, (size.y - AMS_ITEM_CUBE_SIZE.y) / 2, AMS_ITEM_CUBE_SIZE.x, AMS_ITEM_CUBE_SIZE.y); 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.SetPen(wxPen(wxColor(0, 0, 0)));
dc.DrawLine(rect.GetRight() - FromDIP(1), rect.GetTop() + FromDIP(1), rect.GetLeft() + FromDIP(1), rect.GetBottom() - FromDIP(1)); dc.DrawLine(rect.GetRight() - FromDIP(1), rect.GetTop() + FromDIP(1), rect.GetLeft() + FromDIP(1), rect.GetBottom() - FromDIP(1));
} }
+2
View File
@@ -183,6 +183,7 @@ struct Caninfo
wxString material_name; wxString material_name;
wxColour material_colour = {*wxWHITE}; wxColour material_colour = {*wxWHITE};
AMSCanType material_state; AMSCanType material_state;
bool is_empty = false;
int ctype=0; int ctype=0;
int material_remain = 100; int material_remain = 100;
int cali_idx = -1; int cali_idx = -1;
@@ -198,6 +199,7 @@ public:
material_name == other.material_name && material_name == other.material_name &&
material_colour == other.material_colour && material_colour == other.material_colour &&
material_state == other.material_state && material_state == other.material_state &&
is_empty == other.is_empty &&
ctype == other.ctype && ctype == other.ctype &&
material_remain == other.material_remain && material_remain == other.material_remain &&
cali_idx == other.cali_idx && cali_idx == other.cali_idx &&
+10
View File
@@ -109,6 +109,16 @@ public:
*/ */
virtual int send_message(std::string dev_id, std::string json_str, int qos, int flag) = 0; virtual int send_message(std::string dev_id, std::string json_str, int qos, int flag) = 0;
// Capability queries are per-device because one agent may own many printers.
// Legacy agents keep their existing behavior unless they override these.
virtual bool supports_command(const std::string& /*dev_id*/, const std::string& /*command*/) const { return true; }
virtual bool supports_feature(const std::string& /*dev_id*/, const std::string& /*feature*/) const { return false; }
// Whether this agent serializes AMS lane selection into print.gcode_file's
// per-print filament_mapping field. A dialect question, not a capability one:
// the no-AMS external-spool normalization runs before capabilities are known.
virtual bool uses_filament_mapping() const { return false; }
// why: gcode is firmware dialect, not a waist concept - commands whose body is Bambu-dialect // why: gcode is firmware dialect, not a waist concept - commands whose body is Bambu-dialect
// gcode live on the agent that speaks it; the default is an honest refusal that MachineObject's // gcode live on the agent that speaks it; the default is an honest refusal that MachineObject's
// publish funnel turns into a dialog. // publish funnel turns into a dialog.
+30
View File
@@ -793,6 +793,36 @@ int NetworkAgent::send_message(std::string dev_id, std::string json_str, int qos
return -1; return -1;
} }
bool NetworkAgent::owns_agent(const std::string& expected_agent_id) const
{
if (!m_printer_agent)
return false;
return expected_agent_id.empty() || m_printer_agent->get_agent_info().id == expected_agent_id;
}
bool NetworkAgent::supports_command(const std::string& expected_agent_id, const std::string& dev_id,
const std::string& command) const
{
if (!owns_agent(expected_agent_id))
return false;
return m_printer_agent->supports_command(dev_id, command);
}
bool NetworkAgent::supports_feature(const std::string& expected_agent_id, const std::string& dev_id,
const std::string& feature) const
{
if (!owns_agent(expected_agent_id))
return false;
return m_printer_agent->supports_feature(dev_id, feature);
}
bool NetworkAgent::uses_filament_mapping(const std::string& expected_agent_id) const
{
if (!owns_agent(expected_agent_id))
return false;
return m_printer_agent->uses_filament_mapping();
}
int NetworkAgent::command_ams_refresh_rfid(std::string dev_id, int ams_id, int slot_id, int sequence_id, bool lan_mode) int NetworkAgent::command_ams_refresh_rfid(std::string dev_id, int ams_id, int slot_id, int sequence_id, bool lan_mode)
{ {
if (m_printer_agent) if (m_printer_agent)
+4
View File
@@ -150,6 +150,10 @@ public:
int set_on_local_message_fn(OnMessageFn fn); int set_on_local_message_fn(OnMessageFn fn);
int set_server_callback(OnServerErrFn fn); int set_server_callback(OnServerErrFn fn);
int send_message(std::string dev_id, std::string json_str, int qos, int flag); int send_message(std::string dev_id, std::string json_str, int qos, int flag);
bool owns_agent(const std::string& expected_agent_id) const;
bool supports_command(const std::string& expected_agent_id, const std::string& dev_id, const std::string& command) const;
bool supports_feature(const std::string& expected_agent_id, const std::string& dev_id, const std::string& feature) const;
bool uses_filament_mapping(const std::string& expected_agent_id) const;
int command_ams_refresh_rfid(std::string dev_id, int ams_id, int slot_id, int sequence_id, bool lan_mode); int command_ams_refresh_rfid(std::string dev_id, int ams_id, int slot_id, int sequence_id, bool lan_mode);
int command_ams_calibrate(std::string dev_id, int ams_id, int sequence_id, bool lan_mode); int command_ams_calibrate(std::string dev_id, int ams_id, int sequence_id, bool lan_mode);
int command_ams_select_tray(std::string dev_id, std::string tray_id, int sequence_id, bool lan_mode); int command_ams_select_tray(std::string dev_id, std::string tray_id, int sequence_id, bool lan_mode);
+4 -1
View File
@@ -34,7 +34,10 @@ struct PrinterAgentInfo
: id(id_), display_name(display_name_), factory(std::move(factory_)) : id(id_), display_name(display_name_), factory(std::move(factory_))
{} {}
PrinterAgentInfo(const std::string& id_, const std::string& display_name_, const std::string& plugin_identifier, PrinterAgentFactory factory_) PrinterAgentInfo(const std::string& id_,
const std::string& display_name_,
const std::string& plugin_identifier,
PrinterAgentFactory factory_)
: id(id_), display_name(display_name_), plugin_identifier(plugin_identifier), factory(std::move(factory_)) : id(id_), display_name(display_name_), plugin_identifier(plugin_identifier), factory(std::move(factory_))
{} {}
}; };
+259 -33
View File
@@ -43,6 +43,45 @@ namespace {
namespace fs = boost::filesystem; namespace fs = boost::filesystem;
struct OrcaDeviceCapabilities
{
bool fms = false;
bool filament_slots = false;
bool filament_mapping = false;
std::set<std::string> supported_commands;
};
std::mutex g_capabilities_mutex;
std::unordered_map<std::string, OrcaDeviceCapabilities> g_capabilities;
OrcaDeviceCapabilities capabilities_for(const std::string& dev_id)
{
std::lock_guard<std::mutex> lock(g_capabilities_mutex);
const auto it = g_capabilities.find(dev_id);
return it == g_capabilities.end() ? OrcaDeviceCapabilities{} : it->second;
}
bool command_supported(const OrcaDeviceCapabilities& capabilities, const std::string& command)
{
if (command == "print.ams_filament_setting")
return capabilities.filament_slots;
static const std::set<std::string> macro_backed_commands = {
"print.ams_change_filament",
"print.ams_control",
"print.ams_user_setting",
"print.ams_get_rfid",
"print.auto_stop_ams_dry",
};
if (macro_backed_commands.count(command) != 0)
return capabilities.fms && capabilities.supported_commands.count(command) != 0;
if (command.rfind("print.ams_", 0) == 0)
return capabilities.supported_commands.count(command) != 0;
return true;
}
// params.filename is normally the exported .3mf archive; the sliced G-code sits // params.filename is normally the exported .3mf archive; the sliced G-code sits
// beside it with the same stem (".12345.0.3mf" -> ".12345.0.gcode"). params.dst_file, // beside it with the same stem (".12345.0.3mf" -> ".12345.0.gcode"). params.dst_file,
// when set, already points straight at a file (the "print a file already on the // when set, already points straight at a file (the "print a file already on the
@@ -447,6 +486,14 @@ OrcaPrinterAgent::~OrcaPrinterAgent()
++m_lan_generation; // fence any late worker callback ++m_lan_generation; // fence any late worker callback
++m_cloud_generation; ++m_cloud_generation;
std::string lan_dev_id;
std::string cloud_dev_id;
{
std::lock_guard<std::mutex> lock(state_mutex);
lan_dev_id = m_lan_dev_id;
cloud_dev_id = selected_machine;
}
// Drop the cloud status callback before anything else: it holds `this`, and the // Drop the cloud status callback before anything else: it holds `this`, and the
// cloud agent outlives the printer agent (NetworkAgent::set_printer_agent swaps // cloud agent outlives the printer agent (NetworkAgent::set_printer_agent swaps
// the printer agent while m_cloud_agents persist). // the printer agent while m_cloud_agents persist).
@@ -482,6 +529,8 @@ OrcaPrinterAgent::~OrcaPrinterAgent()
std::lock_guard<std::mutex> l(state_mutex); std::lock_guard<std::mutex> l(state_mutex);
lan_mqtt_connection.reset(); lan_mqtt_connection.reset();
} }
forget_device_capabilities(lan_dev_id);
forget_device_capabilities(cloud_dev_id);
} }
OrcaCloudServiceAgent* OrcaPrinterAgent::get_orca_cloud_agent() OrcaCloudServiceAgent* OrcaPrinterAgent::get_orca_cloud_agent()
@@ -588,6 +637,52 @@ std::string OrcaPrinterAgent::merge_capabilities(const std::string& dev_id, cons
std::lock_guard<std::mutex> l(nozzle_diameter_cache_mutex); std::lock_guard<std::mutex> l(nozzle_diameter_cache_mutex);
nozzle_diameter_cache[dev_id] = nozzle_dia; nozzle_diameter_cache[dev_id] = nozzle_dia;
} }
// Connector capabilities are retained per device for command dispatch
// and GUI capability queries. A fresh reply replaces stale support.
OrcaDeviceCapabilities device_capabilities;
auto parse_features = [&device_capabilities](const nlohmann::json& features) {
if (!features.is_object())
return;
auto read_bool = [&features](const char* key, bool& output) {
const auto it = features.find(key);
if (it != features.end() && it->is_boolean())
output = it->get<bool>();
};
read_bool("fms", device_capabilities.fms);
read_bool("filament_slots", device_capabilities.filament_slots);
read_bool("filament_mapping", device_capabilities.filament_mapping);
};
auto parse_commands = [&device_capabilities](const nlohmann::json& commands) {
if (!commands.is_array())
return;
for (const auto& item : commands) {
if (item.is_string())
device_capabilities.supported_commands.insert(item.get<std::string>());
}
};
const auto top_features = info_it->find("supported_features");
if (top_features != info_it->end())
parse_features(*top_features);
const auto top_commands = info_it->find("supported_commands");
if (top_commands != info_it->end())
parse_commands(*top_commands);
if (caps_it != info_it->end() && caps_it->is_object()) {
const auto protocol_it = caps_it->find("protocol");
if (protocol_it != caps_it->end() && protocol_it->is_object()) {
const auto protocol_features = protocol_it->find("features");
if (protocol_features != protocol_it->end())
parse_features(*protocol_features);
const auto protocol_commands = protocol_it->find("supported_commands");
if (protocol_commands != protocol_it->end())
parse_commands(*protocol_commands);
}
}
{
std::lock_guard<std::mutex> lock(g_capabilities_mutex);
g_capabilities[dev_id] = std::move(device_capabilities);
}
// The capabilities reply itself is forwarded unchanged. // The capabilities reply itself is forwarded unchanged.
} }
else { else {
@@ -614,6 +709,14 @@ std::string OrcaPrinterAgent::merge_capabilities(const std::string& dev_id, cons
return modified ? envelope.dump() : payload; return modified ? envelope.dump() : payload;
} }
void OrcaPrinterAgent::forget_device_capabilities(const std::string& dev_id)
{
if (dev_id.empty())
return;
std::lock_guard<std::mutex> lock(g_capabilities_mutex);
g_capabilities.erase(dev_id);
}
void OrcaPrinterAgent::deliver_to_sink(const std::string& dev_id, const std::string& payload, bool local) void OrcaPrinterAgent::deliver_to_sink(const std::string& dev_id, const std::string& payload, bool local)
{ {
parse_ipcam_info(dev_id, payload); parse_ipcam_info(dev_id, payload);
@@ -701,6 +804,56 @@ void OrcaPrinterAgent::set_cloud_agent(std::shared_ptr<ICloudServiceAgent> cloud
int OrcaPrinterAgent::send_message(std::string dev_id, std::string json_str, int /*qos*/, int /*flag*/) int OrcaPrinterAgent::send_message(std::string dev_id, std::string json_str, int /*qos*/, int /*flag*/)
{ return route_send(/*is_lan=*/false, dev_id, json_str); } { return route_send(/*is_lan=*/false, dev_id, json_str); }
bool OrcaPrinterAgent::supports_command(const std::string& dev_id, const std::string& command) const
{
return command_supported(capabilities_for(dev_id), command);
}
bool OrcaPrinterAgent::supports_feature(const std::string& dev_id, const std::string& feature) const
{
const OrcaDeviceCapabilities capabilities = capabilities_for(dev_id);
if (feature == "fms")
return capabilities.fms;
if (feature == "filament_slots")
return capabilities.filament_slots;
if (feature == "filament_mapping")
return capabilities.filament_mapping;
return false;
}
int OrcaPrinterAgent::prepare_outgoing_request(const std::string& dev_id, const std::string& payload,
std::string& command, std::string& prepared) const
{
command = "<unparsed>";
prepared = payload;
nlohmann::json envelope = nlohmann::json::parse(payload, nullptr, false);
if (envelope.is_discarded() || !envelope.is_object())
return BAMBU_NETWORK_SUCCESS;
nlohmann::json* print = nullptr;
for (const char* namespace_name : {"pushing", "info", "print", "system", "camera", "xcam", "upgrade", "event", "files"}) {
const auto namespace_it = envelope.find(namespace_name);
if (namespace_it == envelope.end() || !namespace_it->is_object())
continue;
const auto command_it = namespace_it->find("command");
if (command_it == namespace_it->end() || !command_it->is_string())
continue;
command = std::string(namespace_name) + "." + command_it->get<std::string>();
if (std::string(namespace_name) == "print")
print = &*namespace_it;
break;
}
if (!supports_command(dev_id, command))
return ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED;
if (command == "print.ams_filament_setting" && print && print->contains("setting_id")) {
print->erase("setting_id");
prepared = envelope.dump();
}
return BAMBU_NETWORK_SUCCESS;
}
int OrcaPrinterAgent::command_ams_refresh_rfid(std::string dev_id, int ams_id, int tray_id, int sequence_id, bool lan_mode) int OrcaPrinterAgent::command_ams_refresh_rfid(std::string dev_id, int ams_id, int tray_id, int sequence_id, bool lan_mode)
{ {
(void) ams_id; (void) ams_id;
@@ -1011,6 +1164,7 @@ int OrcaPrinterAgent::connect_printer(const PrinterConnectionParams& params)
BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: connect_printer rejected unparsable LAN endpoint dev_ip=" << params.host; BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: connect_printer rejected unparsable LAN endpoint dev_ip=" << params.host;
return BAMBU_NETWORK_ERR_INVALID_HANDLE; return BAMBU_NETWORK_ERR_INVALID_HANDLE;
} }
forget_device_capabilities(params.dev_id);
disconnect_printer(); disconnect_printer();
const uint64_t gen = ++m_lan_generation; const uint64_t gen = ++m_lan_generation;
@@ -1067,6 +1221,7 @@ int OrcaPrinterAgent::connect_printer(const PrinterConnectionParams& params)
on_connected(params.dev_id, conn, gen); on_connected(params.dev_id, conn, gen);
dispatch_local_connect(ConnectStatusOk, params.dev_id, "0"); dispatch_local_connect(ConnectStatusOk, params.dev_id, "0");
} else if (!connected && !initial) { } else if (!connected && !initial) {
forget_device_capabilities(params.dev_id);
dispatch_local_connect(ConnectStatusLost, params.dev_id, "connection_lost"); dispatch_local_connect(ConnectStatusLost, params.dev_id, "connection_lost");
} }
}); });
@@ -1118,6 +1273,7 @@ int OrcaPrinterAgent::disconnect_printer()
} }
current_connection = m_current_connection; current_connection = m_current_connection;
} }
forget_device_capabilities(prev_dev);
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: LAN disconnect generation=" << m_lan_generation.load() << " previous_dev_id=" << prev_dev BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: LAN disconnect generation=" << m_lan_generation.load() << " previous_dev_id=" << prev_dev
<< " had_connection=" << (doomed ? "yes" : "no") << " had_connection=" << (doomed ? "yes" : "no")
<< " connected=" << (doomed && doomed->is_connected() ? "yes" : "no") << " connected=" << (doomed && doomed->is_connected() ? "yes" : "no")
@@ -1142,31 +1298,21 @@ int OrcaPrinterAgent::send_message_to_printer(std::string dev_id, std::string js
int OrcaPrinterAgent::route_send(bool is_lan, const std::string& dev_id, const std::string& json_str) int OrcaPrinterAgent::route_send(bool is_lan, const std::string& dev_id, const std::string& json_str)
{ {
std::string command = "<unparsed>"; std::string command;
try { std::string prepared;
const nlohmann::json envelope = nlohmann::json::parse(json_str); const int prepare_rc = prepare_outgoing_request(dev_id, json_str, command, prepared);
for (const char* namespace_name : {"pushing", "info", "print", "system", "camera", "xcam", "upgrade", "event", "files"}) {
const auto namespace_it = envelope.find(namespace_name);
if (namespace_it != envelope.end() && namespace_it->is_object()) {
const auto command_it = namespace_it->find("command");
if (command_it != namespace_it->end() && command_it->is_string()) {
command = std::string(namespace_name) + "." + command_it->get<std::string>();
break;
}
}
}
} catch (const std::exception&) {
// Preserve the transport's existing behavior for malformed payloads;
// the printer will report the protocol error asynchronously.
}
BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent::route_send is_lan=" << is_lan << " dev_id=" << dev_id << " command=" << command BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent::route_send is_lan=" << is_lan << " dev_id=" << dev_id << " command=" << command
<< " payload_bytes=" << json_str.size(); << " payload_bytes=" << json_str.size();
if (dev_id.empty()) if (dev_id.empty())
return BAMBU_NETWORK_ERR_INVALID_HANDLE; return BAMBU_NETWORK_ERR_INVALID_HANDLE;
if (prepare_rc != BAMBU_NETWORK_SUCCESS) {
BOOST_LOG_TRIVIAL(warning) << "OrcaPrinterAgent: refusing unsupported command " << command << " for dev_id=" << dev_id;
return prepare_rc;
}
OrcaMqttConnection* conn = get_appropriate_mqtt_connection(is_lan); OrcaMqttConnection* conn = get_appropriate_mqtt_connection(is_lan);
if (!conn) if (!conn)
return BAMBU_NETWORK_ERR_INVALID_HANDLE; return BAMBU_NETWORK_ERR_INVALID_HANDLE;
const bool queued = conn->send_request(dev_id, json_str); const bool queued = conn->send_request(dev_id, prepared);
BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent::route_send command=" << command << " queued=" << queued << " is_lan=" << is_lan BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent::route_send command=" << command << " queued=" << queued << " is_lan=" << is_lan
<< " dev_id=" << dev_id; << " dev_id=" << dev_id;
return queued ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_CONNECTION_TO_SERVER_FAILED; return queued ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_CONNECTION_TO_SERVER_FAILED;
@@ -1296,19 +1442,20 @@ int OrcaPrinterAgent::set_user_selected_machine(std::string dev_id)
} }
current_connection = m_current_connection; current_connection = m_current_connection;
} }
// Fence in-flight cloud callbacks before discarding their capability state.
const uint64_t gen = ++m_cloud_generation;
BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent::set_user_selected_machine: previous=" << previous << " new=" << dev_id BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent::set_user_selected_machine: previous=" << previous << " new=" << dev_id
<< " cloud=" << (cloud ? "set" : "<null>") << " transport=" << connection_type_name(previous_connection) << "->" << " cloud=" << (cloud ? "set" : "<null>") << " transport=" << connection_type_name(previous_connection) << "->"
<< connection_type_name(current_connection); << connection_type_name(current_connection);
if (previous != dev_id || previous_connection != current_connection) {
forget_device_capabilities(previous);
forget_device_capabilities(dev_id);
}
if (!cloud) { if (!cloud) {
BOOST_LOG_TRIVIAL(warning) << "OrcaPrinterAgent::set_user_selected_machine: no Orca cloud agent"; BOOST_LOG_TRIVIAL(warning) << "OrcaPrinterAgent::set_user_selected_machine: no Orca cloud agent";
return BAMBU_NETWORK_SUCCESS; return BAMBU_NETWORK_SUCCESS;
} }
// Bump ONCE at the top for any change (select or deselect) so a deselect also
// fences an in-flight configure thread started by the previous selection. This is
// the CLOUD epoch only — a cloud selection must not fence a live LAN session.
const uint64_t gen = ++m_cloud_generation;
auto* conn = cloud->get_mqtt_connection(); auto* conn = cloud->get_mqtt_connection();
if (!previous.empty()) { if (!previous.empty()) {
cloud->del_subscribe({previous}); cloud->del_subscribe({previous});
@@ -1338,8 +1485,12 @@ int OrcaPrinterAgent::set_user_selected_machine(std::string dev_id)
return; // superseded before we ran: do not raise a socket nobody owns return; // superseded before we ran: do not raise a socket nobody owns
auto state_handler = [this](bool connected, bool initial) { auto state_handler = [this](bool connected, bool initial) {
if (!connected || initial) if (initial)
return; return;
if (!connected) {
forget_device_capabilities(get_user_selected_machine());
return;
}
auto* current_cloud = get_orca_cloud_agent(); auto* current_cloud = get_orca_cloud_agent();
OrcaMqttConnection* current_conn = current_cloud ? current_cloud->get_mqtt_connection() : nullptr; OrcaMqttConnection* current_conn = current_cloud ? current_cloud->get_mqtt_connection() : nullptr;
const std::string selected = get_user_selected_machine(); const std::string selected = get_user_selected_machine();
@@ -1355,6 +1506,12 @@ int OrcaPrinterAgent::set_user_selected_machine(std::string dev_id)
return BAMBU_NETWORK_SUCCESS; return BAMBU_NETWORK_SUCCESS;
} }
int OrcaPrinterAgent::unbind(std::string dev_id)
{
forget_device_capabilities(dev_id);
return BAMBU_NETWORK_SUCCESS;
}
// ============================================================================ // ============================================================================
// Agent Information // Agent Information
// ============================================================================ // ============================================================================
@@ -1415,11 +1572,17 @@ int OrcaPrinterAgent::start_print(PrintParams params, OnUpdateStatusFn update_fn
return BAMBU_NETWORK_SUCCESS; return BAMBU_NETWORK_SUCCESS;
} }
int OrcaPrinterAgent::start_local_print_with_record(PrintParams params, int OrcaPrinterAgent::start_local_print_with_record(PrintParams /*params*/,
OnUpdateStatusFn update_fn, OnUpdateStatusFn /*update_fn*/,
WasCancelledFn cancel_fn, WasCancelledFn /*cancel_fn*/,
OnWaitFn wait_fn) OnWaitFn /*wait_fn*/)
{ return BAMBU_NETWORK_SUCCESS; } {
// OrcaSonar has no FTP "send with record" path. Report a non-success result so
// PrintJob falls back to start_print() (cloud upload + start_sdcard_print) instead
// of treating a print that was never sent as successful.
BOOST_LOG_TRIVIAL(warning) << "OrcaPrinterAgent: start_local_print_with_record is unimplemented; deferring to start_print";
return BAMBU_NETWORK_ERR_FTP_UPLOAD_FAILED;
}
// Upload one G-code file to the printer's `gcodes` root over OrcaSonar's // Upload one G-code file to the printer's `gcodes` root over OrcaSonar's
// Moonraker-compatible HTTP facade. No print is started here (print=false); the // Moonraker-compatible HTTP facade. No print is started here (print=false); the
@@ -1587,6 +1750,59 @@ int OrcaPrinterAgent::start_local_print(PrintParams params, OnUpdateStatusFn upd
return start_sdcard_print(params, update_fn, cancel_fn); return start_sdcard_print(params, update_fn, cancel_fn);
} }
// Serialize PrintParams::ams_mapping2 (the dialog's mapping_v1_json, one entry per logical
// filament in preset order) into the print.gcode_file `filament_mapping` array. The array
// position becomes `filament_index`; {255,255} (unmatched/unused) is dropped. Returns an
// empty array when nothing usable remains so the caller can omit the field entirely.
nlohmann::json OrcaPrinterAgent::build_filament_mapping(const std::string& ams_mapping2)
{
nlohmann::json mapping = nlohmann::json::array();
if (ams_mapping2.empty())
return mapping;
const nlohmann::json parsed = nlohmann::json::parse(ams_mapping2, nullptr, false);
if (parsed.is_discarded() || !parsed.is_array()) {
BOOST_LOG_TRIVIAL(warning) << "OrcaPrinterAgent: ams_mapping2 is not a JSON array; not sending filament_mapping";
return mapping;
}
for (std::size_t i = 0; i < parsed.size(); ++i) {
const nlohmann::json& entry = parsed[i];
if (!entry.is_object())
continue;
const auto ams_id_it = entry.find("ams_id");
const auto slot_id_it = entry.find("slot_id");
if (ams_id_it == entry.end() || slot_id_it == entry.end())
continue;
if (!ams_id_it->is_number_integer() || !slot_id_it->is_number_integer())
continue;
const int ams_id = ams_id_it->get<int>();
const int slot_id = slot_id_it->get<int>();
if (ams_id == 255 && slot_id == 255)
continue;
mapping.push_back({{"filament_index", static_cast<int>(i)}, {"ams_id", ams_id}, {"slot_id", slot_id}});
}
return mapping;
}
// Pure builder for the print.gcode_file payload: the base command plus, only
// when non-empty, the filament_mapping array. An empty mapping leaves the
// payload byte-identical to today's unmapped command.
nlohmann::json OrcaPrinterAgent::build_gcode_file_payload(const std::string& sequence_id,
const std::string& target,
const nlohmann::json& filament_mapping)
{
nlohmann::json j;
j["print"]["command"] = "gcode_file";
j["print"]["sequence_id"] = sequence_id;
j["print"]["param"] = target;
if (filament_mapping.is_array() && !filament_mapping.empty())
j["print"]["filament_mapping"] = filament_mapping;
return j;
}
// Start a file that already lives on the printer by publishing the canonical // Start a file that already lives on the printer by publishing the canonical
// OPCP print.gcode_file command to device/<dev_id>/request. The acknowledgement // OPCP print.gcode_file command to device/<dev_id>/request. The acknowledgement
// and lifecycle progress arrive asynchronously as print.push_status on the // and lifecycle progress arrive asynchronously as print.push_status on the
@@ -1604,10 +1820,20 @@ int OrcaPrinterAgent::start_sdcard_print(PrintParams params, OnUpdateStatusFn up
// otherwise start what start_send_gcode_to_sdcard just uploaded to `gcodes`. // otherwise start what start_send_gcode_to_sdcard just uploaded to `gcodes`.
const std::string target = params.dst_file.empty() ? remote_gcode_name(params) : fs::path(params.dst_file).filename().string(); const std::string target = params.dst_file.empty() ? remote_gcode_name(params) : fs::path(params.dst_file).filename().string();
nlohmann::json j; // Per-print mapping. A mapped print is refused when the connector did not
j["print"]["command"] = "gcode_file"; // advertise filament_mapping: the GUI send gates make this visible first, and
j["print"]["sequence_id"] = next_gcode_file_sequence_id(); // this is the defensive gate for callers that bypass them (calibration,
j["print"]["param"] = target; // plugin). Never start a mapped print with the map silently dropped.
const nlohmann::json filament_mapping = build_filament_mapping(params.ams_mapping2);
if (!filament_mapping.empty()) {
if (!supports_feature(params.dev_id, "filament_mapping")) {
BOOST_LOG_TRIVIAL(warning) << "OrcaPrinterAgent: refusing mapped print, connector does not advertise filament_mapping";
return ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED;
}
BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent: start_sdcard_print emitting filament_mapping entries=" << filament_mapping.size();
}
nlohmann::json j = build_gcode_file_payload(next_gcode_file_sequence_id(), target, filament_mapping);
if (update_fn) if (update_fn)
update_fn(PrintingStageSending, 0, "Starting print..."); update_fn(PrintingStageSending, 0, "Starting print...");
+21
View File
@@ -39,6 +39,9 @@ public:
// Communication // Communication
int send_message(std::string dev_id, std::string json_str, int qos, int flag) override; int send_message(std::string dev_id, std::string json_str, int qos, int flag) override;
bool supports_command(const std::string& dev_id, const std::string& command) const override;
bool supports_feature(const std::string& dev_id, const std::string& feature) const override;
bool uses_filament_mapping() const override { return true; }
int connect_printer(const PrinterConnectionParams& params) override; int connect_printer(const PrinterConnectionParams& params) override;
int disconnect_printer() override; int disconnect_printer() override;
int send_message_to_printer(std::string dev_id, std::string json_str, int qos, int flag) override; int send_message_to_printer(std::string dev_id, std::string json_str, int qos, int flag) override;
@@ -53,6 +56,7 @@ public:
// Machine Selection // Machine Selection
std::string get_user_selected_machine() override; std::string get_user_selected_machine() override;
int set_user_selected_machine(std::string dev_id) override; int set_user_selected_machine(std::string dev_id) override;
int unbind(std::string dev_id) override;
/** /**
* Get agent information. * Get agent information.
@@ -64,6 +68,8 @@ public:
// Print Job Operations // Print Job Operations
int start_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) override; int start_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) override;
// Unimplemented on OrcaSonar: reports a non-success result so callers fall back to
// start_print() instead of treating the missing send as success.
int start_local_print_with_record(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) override; int start_local_print_with_record(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) override;
int start_send_gcode_to_sdcard(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) override; int start_send_gcode_to_sdcard(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) override;
int start_local_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn) override; int start_local_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn) override;
@@ -127,6 +133,9 @@ protected:
// once parse_json reads the Orca dialect natively. See the definition for the // once parse_json reads the Orca dialect natively. See the definition for the
// per-rule detail. Returns the payload unchanged when no rule applies. // per-rule detail. Returns the payload unchanged when no rule applies.
std::string merge_capabilities(const std::string& dev_id, const std::string& payload); std::string merge_capabilities(const std::string& dev_id, const std::string& payload);
void forget_device_capabilities(const std::string& dev_id);
int prepare_outgoing_request(const std::string& dev_id, const std::string& payload,
std::string& command, std::string& prepared) const;
// Report the asynchronous LAN connection state using the same callback contract as // Report the asynchronous LAN connection state using the same callback contract as
// the other printer agents. The transport result cannot be returned by // the other printer agents. The transport result cannot be returned by
@@ -140,6 +149,18 @@ protected:
// Pure LAN-address parsing + client-id. protected static so the test Probe reaches them. // Pure LAN-address parsing + client-id. protected static so the test Probe reaches them.
static bool parse_lan_endpoint(const std::string& dev_ip, std::string& host, std::string& port); static bool parse_lan_endpoint(const std::string& dev_ip, std::string& host, std::string& port);
static std::string make_lan_client_id(const std::string& dev_id); static std::string make_lan_client_id(const std::string& dev_id);
// Pure serializer for PrintParams::ams_mapping2 -> print.gcode_file.filament_mapping.
// Entries are re-keyed by their array position; {255,255} is dropped. Empty when the
// input is empty, malformed, or has no usable entries. protected static for the test Probe.
static nlohmann::json build_filament_mapping(const std::string& ams_mapping2);
// Pure builder for the print.gcode_file command payload. A non-empty mapping is
// included as filament_mapping; an empty one is omitted so the payload is
// byte-identical to an unmapped print. protected static for the test Probe.
static nlohmann::json build_gcode_file_payload(const std::string& sequence_id,
const std::string& target,
const nlohmann::json& filament_mapping);
// Test hook: the ws:// URL connect_printer built for the current LAN session ("" if none). // Test hook: the ws:// URL connect_printer built for the current LAN session ("" if none).
std::string lan_connection_target() const; std::string lan_connection_target() const;
// Shared post-connect sequence: SUBSCRIBE, then pushing.start, pushall, // Shared post-connect sequence: SUBSCRIBE, then pushing.start, pushall,
+37
View File
@@ -40,6 +40,20 @@ static std::set<int> tools_for_role(const std::string& gcode, const std::string&
return tools; return tools;
} }
// Every id named by a Tn command anywhere in `gcode`. A non-BBL single-extruder multi-filament
// setup emits T<filament_id>, and that id is what the Klipper toolchange macro consumes.
static std::set<int> tool_ids(const std::string& gcode)
{
std::set<int> tools;
GCodeReader reader;
reader.parse_buffer(gcode, [&tools](GCodeReader&, const GCodeReader::GCodeLine& line) {
const std::string cmd(line.cmd());
if (cmd.size() >= 2 && cmd[0] == 'T' && std::isdigit((unsigned char)cmd[1]))
tools.insert(std::stoi(cmd.substr(1)));
});
return tools;
}
// X where the nozzle sits while each tagged _WAIT_FOR_TEMP_ON_WIPE_TOWER M109 blocks: // X where the nozzle sits while each tagged _WAIT_FOR_TEMP_ON_WIPE_TOWER M109 blocks:
// the nearest preceding G1 carrying an X (the park travel emitted just before the wait). // the nearest preceding G1 carrying an X (the park travel emitted just before the wait).
static std::vector<double> wait_park_xs(const std::string& gcode) static std::vector<double> wait_park_xs(const std::string& gcode)
@@ -287,6 +301,29 @@ TEST_CASE("Each feature prints with its assigned filament (three filaments)", "[
CHECK(tools_for_role(gcode, "infill") == std::set<int>{ 1 }); // filament 2 CHECK(tools_for_role(gcode, "infill") == std::set<int>{ 1 }); // filament 2
} }
// The per-print wire mapping keys each entry by the filament's index in the config filament
// arrays (the ams_mapping2 position). Pin that the id the toolchange emits is that index even
// when the used filaments skip a middle one: filament 2 (index 1) is configured but unused, so
// a renumbering to the used set would emit {0, 1} instead of {0, 2} and aim the map at the
// wrong lane.
TEST_CASE("Toolchange ids keep the config index of the used filament when a middle filament is unused", "[MultiFilament]")
{
const std::string gcode = slice({ cube(20) },
multifilament_config(3, {
{ "sparse_infill_filament_id", 1 },
{ "internal_solid_filament_id", 1 },
{ "top_surface_filament_id", 1 },
{ "bottom_surface_filament_id", 1 },
{ "outer_wall_filament_id", 3 },
{ "inner_wall_filament_id", 3 },
{ "skirt_loops", 0 },
{ "brim_type", "no_brim" },
}));
CHECK(tools_for_role(gcode, "perimeter") == std::set<int>{ 2 }); // filament 3
CHECK(tools_for_role(gcode, "infill") == std::set<int>{ 0 }); // filament 1
CHECK(tool_ids(gcode) == std::set<int>{ 0, 2 });
}
// The override must survive tool ordering: object 1's walls print on their filament's // The override must survive tool ordering: object 1's walls print on their filament's
// tool, object 0 stays on the first. If dropped, every wall prints on tool 0. // tool, object 0 stays on the first. If dropped, every wall prints on tool 0.
TEST_CASE("Per-object wall filament override is honored", "[MultiFilament]") TEST_CASE("Per-object wall filament override is honored", "[MultiFilament]")
+1
View File
@@ -16,6 +16,7 @@ add_executable(${_TEST_NAME}_tests
test_config_variant_expansion.cpp test_config_variant_expansion.cpp
test_toolordering_nozzle_group.cpp test_toolordering_nozzle_group.cpp
test_preset_bundle_loading.cpp test_preset_bundle_loading.cpp
test_preset_bundle_ams_sync.cpp
test_preset_setting_id.cpp test_preset_setting_id.cpp
test_preset_diff.cpp test_preset_diff.cpp
test_vendor_cache.cpp test_vendor_cache.cpp
@@ -0,0 +1,155 @@
#include <catch2/catch_all.hpp>
#include <map>
#include <string>
#include <utility>
#include <vector>
#include "libslic3r/Preset.hpp"
#include "libslic3r/PresetBundle.hpp"
using namespace Slic3r;
namespace {
// In-memory system filament preset. Mirrors the helper in test_preset_bundle_loading.cpp.
Preset &add_system_filament(PresetBundle &bundle, const std::string &name, const std::string &filament_id, const std::string &filament_type)
{
DynamicPrintConfig config(bundle.filaments.default_preset().config);
config.option<ConfigOptionString>(BBL_JSON_KEY_INHERITS, true)->value = "";
config.option<ConfigOptionStrings>("filament_type", true)->values = {filament_type};
Preset &preset = bundle.filaments.load_preset(std::string(), name, config, /*select=*/false);
preset.is_system = true;
preset.filament_id = filament_id;
return preset;
}
// One AMS tray entry as build_filament_ams_list produces it.
DynamicPrintConfig make_tray(const std::string &filament_id, const std::string &filament_type, const std::string &color, const std::string &ams_id,
const std::string &slot_id)
{
DynamicPrintConfig tray;
tray.set_key_value("filament_id", new ConfigOptionStrings{filament_id});
tray.set_key_value("filament_type", new ConfigOptionStrings{filament_type});
tray.set_key_value("filament_colour", new ConfigOptionStrings{color});
tray.set_key_value("filament_colour_type", new ConfigOptionStrings{"1"});
tray.set_key_value("filament_multi_colour", new ConfigOptionStrings{});
tray.set_key_value("ams_id", new ConfigOptionStrings{ams_id});
tray.set_key_value("slot_id", new ConfigOptionStrings{slot_id});
return tray;
}
} // namespace
// A tray the printer UI wrote carries a material type but no OrcaSlicer preset id. It must still
// sync, resolved to Generic <type> with the tray's own color.
TEST_CASE("AMS sync resolves a printer-set tray to Generic by material type", "[Preset][AMS]")
{
PresetBundle bundle;
add_system_filament(bundle, "Generic PETG @Q2", "OFYPdQJh", "PETG");
add_system_filament(bundle, "Generic PLA @Q2", "GFL99", "PLA");
bundle.filament_ams_list[0] = make_tray("OFYPdQJh", "PETG", "#FE717A", "0", "0"); // preset-backed
bundle.filament_ams_list[1] = make_tray("", "PETG", "#898F9B", "0", "1"); // printer-set, no id
bundle.filament_ams_list[2] = make_tray("", "PLA", "#FAFAFA", "0", "2"); // printer-set, no id
bundle.filament_ams_list[3] = make_tray("", "", "#000000", "0", "3"); // empty slot
std::vector<std::pair<DynamicPrintConfig *, std::string>> unknowns;
std::map<int, AMSMapInfo> maps;
MergeFilamentInfo merge;
const unsigned int count = bundle.sync_ams_list(unknowns, /*use_map=*/false, maps, /*enable_append=*/false, merge);
CHECK(count == 3);
REQUIRE(bundle.filament_presets.size() == 3);
CHECK(bundle.filament_presets[0] == "Generic PETG @Q2");
CHECK(bundle.filament_presets[1] == "Generic PETG @Q2");
CHECK(bundle.filament_presets[2] == "Generic PLA @Q2");
const ConfigOptionStrings *colors = bundle.project_config.option<ConfigOptionStrings>("filament_colour");
REQUIRE(colors != nullptr);
REQUIRE(colors->values.size() == 3);
CHECK(colors->values[0] == "#FE717A");
CHECK(colors->values[1] == "#898F9B");
CHECK(colors->values[2] == "#FAFAFA");
}
// Without a preset id and without a material type there is nothing to resolve; the tray stays
// skipped in a direct sync.
TEST_CASE("AMS sync skips a tray without a preset id or a material type", "[Preset][AMS]")
{
PresetBundle bundle;
add_system_filament(bundle, "Generic PETG @Q2", "OFYPdQJh", "PETG");
bundle.filament_ams_list[0] = make_tray("", "", "#000000", "0", "0");
std::vector<std::pair<DynamicPrintConfig *, std::string>> unknowns;
std::map<int, AMSMapInfo> maps;
MergeFilamentInfo merge;
CHECK(bundle.sync_ams_list(unknowns, /*use_map=*/false, maps, /*enable_append=*/false, merge) == 0);
CHECK(bundle.filament_presets.empty());
}
// Guard the pre-existing behavior: a non-empty id that matches no preset still resolves by type.
TEST_CASE("AMS sync still resolves an unmatched preset id by material type", "[Preset][AMS]")
{
PresetBundle bundle;
add_system_filament(bundle, "Generic PETG @Q2", "OFYPdQJh", "PETG");
bundle.filament_ams_list[0] = make_tray("REMOVED_ID", "PETG", "#123456", "0", "0");
std::vector<std::pair<DynamicPrintConfig *, std::string>> unknowns;
std::map<int, AMSMapInfo> maps;
MergeFilamentInfo merge;
CHECK(bundle.sync_ams_list(unknowns, /*use_map=*/false, maps, /*enable_append=*/false, merge) == 1);
REQUIRE(bundle.filament_presets.size() == 1);
CHECK(bundle.filament_presets[0] == "Generic PETG @Q2");
}
// Mapping-mode sync used to force "Generic PLA" for an id-less tray; it must use the type instead.
TEST_CASE("AMS sync in mapping mode resolves a printer-set tray to Generic, not Generic PLA", "[Preset][AMS]")
{
PresetBundle bundle;
add_system_filament(bundle, "Generic PETG @Q2", "OFYPdQJh", "PETG");
add_system_filament(bundle, "Generic PLA @Q2", "GFL99", "PLA");
bundle.filament_presets = {"Generic PLA @Q2"};
bundle.project_config.option<ConfigOptionStrings>("filament_colour")->values = {"#000000"};
bundle.project_config.option<ConfigOptionStrings>("filament_colour_type")->values = {"1"};
bundle.filament_ams_list[0] = make_tray("", "PETG", "#898F9B", "0", "1");
std::map<int, AMSMapInfo> maps;
maps[0] = AMSMapInfo{"0", "1"};
std::vector<std::pair<DynamicPrintConfig *, std::string>> unknowns;
MergeFilamentInfo merge;
const unsigned int count = bundle.sync_ams_list(unknowns, /*use_map=*/true, maps, /*enable_append=*/false, merge);
CHECK(count == 1);
REQUIRE(bundle.filament_presets.size() == 1);
CHECK(bundle.filament_presets[0] == "Generic PETG @Q2");
CHECK(bundle.project_config.option<ConfigOptionStrings>("filament_colour")->values[0] == "#898F9B");
}
// The dialog builds ams_mapping2 one entry per filament_presets index, while the generated
// G-code's toolchange names the filament's index in the slice's filament config arrays. Those
// are the same index only if full_config() collects the filament arrays in filament_presets
// order; if a refactor reorders one side, every mapped print aims at the wrong lane.
TEST_CASE("Full config filament arrays follow the selected filament preset order", "[Preset][AMS]")
{
PresetBundle bundle;
add_system_filament(bundle, "Generic PETG @Q2", "OFYPdQJh", "PETG");
add_system_filament(bundle, "Generic PLA @Q2", "GFL99", "PLA");
// Deliberately not the load order: the arrays must follow this selection.
bundle.filament_presets = {"Generic PLA @Q2", "Generic PETG @Q2"};
const DynamicPrintConfig full = bundle.full_config(/*apply_extruder=*/false);
REQUIRE(full.option<ConfigOptionStrings>("filament_type") != nullptr);
CHECK(full.option<ConfigOptionStrings>("filament_settings_id")->values == std::vector<std::string>{"Generic PLA @Q2", "Generic PETG @Q2"});
CHECK(full.option<ConfigOptionStrings>("filament_type")->values == std::vector<std::string>{"PLA", "PETG"});
// The AMS sync matches trays by filament_id, so this array must follow the same order.
CHECK(full.option<ConfigOptionStrings>("filament_ids")->values == std::vector<std::string>{"GFL99", "OFYPdQJh"});
}
@@ -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 { namespace {
+1
View File
@@ -6,6 +6,7 @@ add_executable(${_TEST_NAME}_tests
test_dev_mapping.cpp test_dev_mapping.cpp
test_filament_bitmap_utils.cpp test_filament_bitmap_utils.cpp
test_device_progress.cpp test_device_progress.cpp
test_ams_item.cpp
test_device_manager_integration.cpp test_device_manager_integration.cpp
test_web_media_controller.cpp test_web_media_controller.cpp
test_scene_raycaster.cpp test_scene_raycaster.cpp
+86
View File
@@ -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 <Windows.h>
#endif
#include <catch2/catch_all.hpp>
#include <wx/timer.h>
#include <nlohmann/json.hpp>
#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);
}
@@ -2,7 +2,11 @@
#include <slic3r/GUI/DeviceCore/DevManager.h> #include <slic3r/GUI/DeviceCore/DevManager.h>
#include <slic3r/GUI/DeviceManager.hpp> #include <slic3r/GUI/DeviceManager.hpp>
#include <slic3r/GUI/DeviceCore/DevFilaSystem.h>
#include <slic3r/GUI/FilamentMappingUtils.hpp>
#include <libslic3r/AppConfig.hpp> #include <libslic3r/AppConfig.hpp>
#include <libslic3r/PresetBundle.hpp>
#include <libslic3r/PrintConfig.hpp>
#include <slic3r/Utils/NetworkAgent.hpp> #include <slic3r/Utils/NetworkAgent.hpp>
#include <slic3r/Utils/OrcaCloudServiceAgent.hpp> #include <slic3r/Utils/OrcaCloudServiceAgent.hpp>
#include <slic3r/Utils/OrcaPrinterAgent.hpp> #include <slic3r/Utils/OrcaPrinterAgent.hpp>
@@ -15,6 +19,8 @@
#include <utility> #include <utility>
using namespace Slic3r; using namespace Slic3r;
using Slic3r::GUI::MappingSendError;
using Slic3r::GUI::prepare_filament_mapping_for_send;
using json = nlohmann::json; using json = nlohmann::json;
namespace { namespace {
@@ -46,6 +52,30 @@ public:
AgentInfo get_agent_info() override { return m_info; } AgentInfo get_agent_info() override { return m_info; }
// This test double represents a legacy agent, not OrcaPrinterAgent behavior.
bool supports_command(const std::string&, const std::string&) const override { return true; }
// It models whichever agent is registered under its id: only the Orca agent
// serializes per-print filament_mapping.
bool uses_filament_mapping() const override { return m_info.id == "orca"; }
using OrcaPrinterAgent::deliver_to_sink;
int send_message(std::string, std::string json_str, int, int) override
{
last_message = std::move(json_str);
return send_result;
}
int send_message_to_printer(std::string, std::string json_str, int, int) override
{
last_message = std::move(json_str);
return send_result;
}
std::string last_message;
int send_result = 0;
private: private:
AgentInfo m_info; AgentInfo m_info;
}; };
@@ -157,3 +187,183 @@ TEST_CASE("Device manager filters and rehomes devices by printer-agent ownership
CHECK(manager.get_my_machine_list("integration-agent-a").empty()); CHECK(manager.get_my_machine_list("integration-agent-a").empty());
CHECK(manager.get_my_machine_list("integration-agent-b").count(machine.dev_id) == 1); CHECK(manager.get_my_machine_list("integration-agent-b").count(machine.dev_id) == 1);
} }
TEST_CASE("Network agent rejects capability queries for a different device owner", "[DeviceManager][integration]")
{
NetworkAgent network(nullptr, std::make_shared<TestPrinterAgent>("bbl"));
CHECK(network.owns_agent("bbl"));
CHECK_FALSE(network.owns_agent("orca"));
CHECK(network.supports_command("bbl", "device", "print.ams_control"));
CHECK_FALSE(network.supports_command("orca", "device", "print.ams_control"));
CHECK_FALSE(network.supports_feature("orca", "device", "filament_mapping"));
// The mapping dialect is queried the same way: a legacy owner never inherits
// Orca's, and a non-owner is never queried at all.
CHECK_FALSE(network.uses_filament_mapping("bbl"));
CHECK_FALSE(network.uses_filament_mapping("orca"));
NetworkAgent orca_network(nullptr, std::make_shared<TestPrinterAgent>("orca"));
CHECK(orca_network.uses_filament_mapping("orca"));
CHECK_FALSE(orca_network.uses_filament_mapping("bbl"));
}
// Send-time mapping policy belongs to the printer agent's dialect, not to an
// agent id: the Orca dialect gets the no-AMS normalization plus both refusals.
TEST_CASE("Per-print mapping send policy follows the owning agent's dialect", "[DeviceManager][integration]")
{
auto orca_agent = std::make_shared<TestPrinterAgent>("orca");
NetworkAgent network(nullptr, orca_agent);
MachineObject obj(nullptr, &network, "test", "orca-mapping-policy", "127.0.0.1");
obj.printer_agent_id = "orca";
// No AMS: the auto-selected external spool is dropped before the capability
// gate, so it cannot refuse a print nobody mapped.
std::string external_only = R"([{"ams_id":255,"slot_id":0}])";
CHECK(prepare_filament_mapping_for_send(&obj, external_only, {}) == MappingSendError::none);
CHECK(external_only.empty());
// An AMS makes that a real target, but nothing advertised the capability yet.
obj.GetFilaSystem()->GetAmsList()["0"] = new DevAms("0", 0, DevAms::AMS);
std::string mapped = R"([{"ams_id":0,"slot_id":0}])";
CHECK(prepare_filament_mapping_for_send(&obj, mapped, {}) == MappingSendError::unsupported);
CHECK(mapped == R"([{"ams_id":0,"slot_id":0}])"); // the refusal leaves it intact
// Once the connector advertises the capability the mapping passes the gate.
orca_agent->deliver_to_sink(obj.get_dev_id(),
R"({"info":{"command":"get_capabilities","supported_features":{"filament_mapping":true}}})",
false);
CHECK(obj.printer_supports_feature("filament_mapping"));
CHECK(prepare_filament_mapping_for_send(&obj, mapped, {}) == MappingSendError::none);
// An unrecorded owner still sends through the active agent, so it takes
// that agent's dialect too rather than falling back to the legacy payload.
MachineObject unowned(nullptr, &network, "test", "orca-unowned", "127.0.0.1");
CHECK(unowned.printer_agent_id.empty());
CHECK(unowned.printer_uses_filament_mapping());
std::string unowned_external = R"([{"ams_id":255,"slot_id":0}])";
CHECK(prepare_filament_mapping_for_send(&unowned, unowned_external, {}) == MappingSendError::none);
CHECK(unowned_external.empty()); // the Orca normalization ran
// A partially mapped print is refused even while the capability holds.
FilamentInfo mapped_entry;
mapped_entry.ams_id = "0";
mapped_entry.slot_id = "0";
FilamentInfo unmapped_entry;
CHECK(prepare_filament_mapping_for_send(&obj, mapped, {mapped_entry, unmapped_entry}) ==
MappingSendError::incomplete);
}
// The legacy payload is the thing being protected: no normalization, no refusal.
TEST_CASE("A legacy printer agent keeps its mapping payload untouched", "[DeviceManager][integration]")
{
NetworkAgent network(nullptr, std::make_shared<TestPrinterAgent>("bbl"));
MachineObject obj(nullptr, &network, "test", "bbl-mapping-policy", "127.0.0.1");
obj.printer_agent_id = "bbl";
FilamentInfo mapped_entry;
mapped_entry.ams_id = "0";
mapped_entry.slot_id = "0";
FilamentInfo unmapped_entry;
std::string external_only = R"([{"ams_id":255,"slot_id":0}])";
CHECK(prepare_filament_mapping_for_send(&obj, external_only, {mapped_entry, unmapped_entry}) ==
MappingSendError::none);
CHECK(external_only == R"([{"ams_id":255,"slot_id":0}])");
}
TEST_CASE("AMS metadata retains setting_id for legacy printer agents", "[DeviceManager][integration]")
{
ScopedAppConfig app_config;
auto printer_agent = std::make_shared<TestPrinterAgent>("bbl");
NetworkAgent network(nullptr, printer_agent);
DeviceManager manager(&network, false, &app_config.config);
BBLocalMachine machine;
machine.dev_id = "bbl-setting-id";
machine.dev_name = "Bambu setting id";
machine.dev_ip = "192.0.2.32";
machine.printer_type = "C11";
MachineObject* obj = manager.insert_local_device(machine, "lan", "free", "", "access-code");
REQUIRE(obj != nullptr);
REQUIRE(obj->command_ams_filament_settings(0, 1, "GFL99", "preset-setting", "00FF00FF", "PLA", 190, 220) == 0);
const json payload = json::parse(printer_agent->last_message);
CHECK(payload["print"]["setting_id"] == "preset-setting");
}
TEST_CASE("AMS user settings do not update local state when publishing fails", "[DeviceManager][integration]")
{
ScopedAppConfig app_config;
auto printer_agent = std::make_shared<TestPrinterAgent>("bbl");
NetworkAgent network(nullptr, printer_agent);
DeviceManager manager(&network, false, &app_config.config);
BBLocalMachine machine;
machine.dev_id = "bbl-ams-setting-failure";
machine.dev_name = "Bambu AMS settings";
machine.dev_ip = "192.0.2.33";
machine.printer_type = "C11";
MachineObject* obj = manager.insert_local_device(machine, "lan", "free", "", "access-code");
REQUIRE(obj != nullptr);
auto& settings = obj->GetFilaSystem()->GetAmsSystemSetting();
settings.SetDetectOnInsertEnabled(false);
settings.SetDetectOnPowerupEnabled(false);
settings.SetDetectRemainEnabled(false);
printer_agent->send_result = -1;
CHECK(obj->command_ams_user_settings(true, true, true) != 0);
CHECK(settings.IsDetectOnInsertEnabled() == false);
CHECK(settings.IsDetectOnPowerupEnabled() == false);
CHECK(settings.IsDetectRemainEnabled() == false);
}
// 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());
}
@@ -1,4 +1,5 @@
#include <catch2/catch_test_macros.hpp> #include <catch2/catch_test_macros.hpp>
#include <slic3r/GUI/FilamentMappingUtils.hpp>
#include <slic3r/Utils/IPrinterAgent.hpp> #include <slic3r/Utils/IPrinterAgent.hpp>
#include <slic3r/Utils/OrcaCloudServiceAgent.hpp> #include <slic3r/Utils/OrcaCloudServiceAgent.hpp>
#include <slic3r/Utils/OrcaPrinterAgent.hpp> #include <slic3r/Utils/OrcaPrinterAgent.hpp>
@@ -22,6 +23,9 @@ struct Probe : OrcaPrinterAgent {
using OrcaPrinterAgent::parse_lan_endpoint; using OrcaPrinterAgent::parse_lan_endpoint;
using OrcaPrinterAgent::make_lan_client_id; using OrcaPrinterAgent::make_lan_client_id;
using OrcaPrinterAgent::lan_connection_target; using OrcaPrinterAgent::lan_connection_target;
using OrcaPrinterAgent::build_filament_mapping;
using OrcaPrinterAgent::build_gcode_file_payload;
using OrcaPrinterAgent::prepare_outgoing_request;
}; };
} }
@@ -68,6 +72,91 @@ TEST_CASE("OrcaPrinterAgent stamps the get_capabilities nozzle diameter onto pus
CHECK(last_payload.find("N/A") == std::string::npos); CHECK(last_payload.find("N/A") == std::string::npos);
} }
TEST_CASE("OrcaPrinterAgent owns connector capabilities and fails closed for AMS commands", "[OrcaPrinterAgent]") {
Probe agent("/tmp");
agent.deliver_to_sink("orca-caps", R"({
"info": {
"command": "get_capabilities",
"supported_features": {"fms": true, "filament_slots": true, "filament_mapping": true},
"supported_commands": ["print.ams_get_rfid"],
"capabilities": {
"protocol": {
"features": {"filament_mapping": true},
"supported_commands": ["print.ams_change_filament"]
}
}
}
})", false);
CHECK(agent.supports_feature("orca-caps", "filament_mapping"));
CHECK(agent.supports_command("orca-caps", "print.ams_filament_setting"));
CHECK(agent.supports_command("orca-caps", "print.ams_change_filament"));
CHECK(agent.supports_command("orca-caps", "print.ams_get_rfid"));
CHECK_FALSE(agent.supports_command("orca-caps", "print.ams_control"));
agent.deliver_to_sink("orca-caps", R"({
"info": {
"command": "get_capabilities",
"supported_features": {"fms": false, "filament_slots": false, "filament_mapping": false},
"supported_commands": ["print.ams_control"],
"capabilities": {"protocol": {"features": {}, "supported_commands": []}}
}
})", false);
CHECK_FALSE(agent.supports_feature("orca-caps", "filament_mapping"));
CHECK_FALSE(agent.supports_command("orca-caps", "print.ams_filament_setting"));
CHECK_FALSE(agent.supports_command("orca-caps", "print.ams_change_filament"));
CHECK_FALSE(agent.supports_command("orca-caps", "print.ams_control"));
CHECK_FALSE(agent.supports_command("orca-caps", "print.ams_calibrate"));
CHECK(agent.supports_command("orca-caps", "print.gcode_file"));
}
TEST_CASE("OrcaPrinterAgent clears cached capabilities when a device is unbound", "[OrcaPrinterAgent]") {
Probe agent("/tmp");
agent.deliver_to_sink("orca-forget", R"({
"info": {
"command": "get_capabilities",
"supported_features": {"filament_slots": true},
"supported_commands": [],
"capabilities": {"protocol": {"features": {}, "supported_commands": []}}
}
})", false);
REQUIRE(agent.supports_command("orca-forget", "print.ams_filament_setting"));
agent.unbind("orca-forget");
CHECK_FALSE(agent.supports_command("orca-forget", "print.ams_filament_setting"));
}
TEST_CASE("OrcaPrinterAgent removes setting_id from AMS metadata and gates the request", "[OrcaPrinterAgent]") {
Probe agent("/tmp");
agent.deliver_to_sink("orca-ams-write", R"({
"info": {
"command": "get_capabilities",
"supported_features": {"fms": false, "filament_slots": true},
"supported_commands": [],
"capabilities": {"protocol": {"features": {"filament_slots": true}, "supported_commands": []}}
}
})", false);
const std::string request = R"({"print":{"command":"ams_filament_setting","sequence_id":"9","tray_info_idx":"GFL99","setting_id":"preset-setting"}})";
std::string command;
std::string prepared;
CHECK(agent.prepare_outgoing_request("orca-ams-write", request, command, prepared) == BAMBU_NETWORK_SUCCESS);
CHECK(command == "print.ams_filament_setting");
const nlohmann::json parsed = nlohmann::json::parse(prepared);
CHECK_FALSE(parsed["print"].contains("setting_id"));
CHECK(parsed["print"]["tray_info_idx"] == "GFL99");
agent.deliver_to_sink("orca-ams-write", R"({
"info": {
"command": "get_capabilities",
"supported_features": {"filament_slots": false},
"capabilities": {"protocol": {"features": {"filament_slots": false}}}
}
})", false);
CHECK(agent.prepare_outgoing_request("orca-ams-write", request, command, prepared) == ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED);
}
TEST_CASE("OrcaPrinterAgent::parse_lan_endpoint", "[OrcaPrinterAgent]") { TEST_CASE("OrcaPrinterAgent::parse_lan_endpoint", "[OrcaPrinterAgent]") {
std::string h, p; std::string h, p;
REQUIRE(Probe::parse_lan_endpoint("192.168.1.9", h, p)); REQUIRE(Probe::parse_lan_endpoint("192.168.1.9", h, p));
@@ -84,6 +173,157 @@ TEST_CASE("OrcaPrinterAgent::make_lan_client_id is stable and prefixed", "[OrcaP
CHECK(a.rfind("orcaslicer-lan-dev-1-", 0) == 0); CHECK(a.rfind("orcaslicer-lan-dev-1-", 0) == 0);
} }
TEST_CASE("filament mapping is keyed by the ams_mapping2 array position", "[OrcaPrinterAgent]") {
const nlohmann::json mapping = Probe::build_filament_mapping(
R"([{"ams_id":1,"slot_id":5},{"ams_id":255,"slot_id":255},{"ams_id":255,"slot_id":0}])");
REQUIRE(mapping.is_array());
REQUIRE(mapping.size() == 2);
CHECK(mapping[0]["filament_index"] == 0);
CHECK(mapping[0]["ams_id"] == 1);
CHECK(mapping[0]["slot_id"] == 5);
// The unmatched middle entry is dropped; the third entry keeps index 2.
CHECK(mapping[1]["filament_index"] == 2);
CHECK(mapping[1]["ams_id"] == 255);
CHECK(mapping[1]["slot_id"] == 0);
}
// Index-correlation merge gate (plan PR 3). `ams_mapping2` is built one entry
// per logical filament, so its array position is the identifier the generated
// G-code toolchange passes to the Klipper macro (`next_filament_id`). The
// serializer must key `filament_index` by that position and must never
// re-densify after dropping unused/sentinel entries, or a used filament would
// be aimed at the wrong lane. This test covers the plan's matrix: preset order
// differing from used order, a middle filament unused, and external slots.
TEST_CASE("filament mapping index correlates with the ams_mapping2 position", "[OrcaPrinterAgent]") {
// Positions 0..4. Used filaments are 0, 2 and 4; 1 and 3 are unused.
const nlohmann::json mapping = Probe::build_filament_mapping(
R"([{"ams_id":0,"slot_id":1},{"ams_id":255,"slot_id":255},{"ams_id":2,"slot_id":3},{"ams_id":255,"slot_id":255},{"ams_id":255,"slot_id":0}])");
REQUIRE(mapping.size() == 3);
CHECK(mapping[0]["filament_index"] == 0);
CHECK(mapping[1]["filament_index"] == 2);
CHECK(mapping[2]["filament_index"] == 4); // external slot keeps its position
CHECK(mapping[2]["ams_id"] == 255);
CHECK(mapping[2]["slot_id"] == 0);
// No re-densification: a used filament after a dropped sentinel keeps its
// original logical index.
for (const auto& entry : mapping)
CHECK(entry.contains("filament_index"));
}
TEST_CASE("filament mapping is omitted when nothing remains", "[OrcaPrinterAgent]") {
CHECK(Probe::build_filament_mapping("").empty());
CHECK(Probe::build_filament_mapping(R"([{"ams_id":255,"slot_id":255}])").empty());
CHECK(Probe::build_filament_mapping("not json").empty());
CHECK(Probe::build_filament_mapping(R"({"ams_id":1,"slot_id":0})").empty()); // not an array
}
// The GUI capability gate and the agent serializer must classify the same
// entries as engaged. External slots ({255,0}/{254,0}) are normalized as-is, so
// they engage; only the {255,255} unmatched sentinel is dropped. A mismatch lets
// an entry past the GUI and refused late with a generic publish error.
TEST_CASE("the GUI mapping gate engages exactly the entries the serializer sends", "[OrcaPrinterAgent]") {
using Slic3r::GUI::has_engaged_filament_mapping;
CHECK_FALSE(has_engaged_filament_mapping(""));
CHECK_FALSE(has_engaged_filament_mapping("[]"));
CHECK_FALSE(has_engaged_filament_mapping("not json"));
CHECK_FALSE(has_engaged_filament_mapping(R"([{"ams_id":255,"slot_id":255}])"));
CHECK(has_engaged_filament_mapping(R"([{"ams_id":255,"slot_id":0}])")); // external main
CHECK(has_engaged_filament_mapping(R"([{"ams_id":254,"slot_id":0}])")); // external deputy
CHECK(has_engaged_filament_mapping(R"([{"ams_id":0,"slot_id":0}])")); // box slot
CHECK(has_engaged_filament_mapping(R"([{"ams_id":255,"slot_id":255},{"ams_id":1,"slot_id":2}])"));
for (const char* s : {"", "[]", "not json", R"([{"ams_id":255,"slot_id":255}])",
R"([{"ams_id":255,"slot_id":0}])", R"([{"ams_id":254,"slot_id":0}])",
R"([{"ams_id":0,"slot_id":0}])",
R"([{"ams_id":255,"slot_id":255},{"ams_id":1,"slot_id":2}])"}) {
CHECK(has_engaged_filament_mapping(s) == !Probe::build_filament_mapping(s).empty());
}
}
// The used-unmapped refusal reads m_ams_mapping_result: an entry with no target carries
// empty ams_id/slot_id, while an external-spool assignment (ams_id 255/254) is a real
// target. A wholly unmapped print reports no target and falls to the existing send flow.
TEST_CASE("used-filament targets split mapped from unmapped", "[OrcaPrinterAgent]") {
using Slic3r::GUI::has_any_mapped_target;
using Slic3r::GUI::has_used_filament_without_target;
Slic3r::FilamentInfo box; box.ams_id = "0"; box.slot_id = "1"; // box slot
Slic3r::FilamentInfo external; external.ams_id = "255"; external.slot_id = "0"; // external spool
Slic3r::FilamentInfo unmapped; // no target
CHECK_FALSE(has_used_filament_without_target({box, external}));
CHECK(has_used_filament_without_target({box, unmapped}));
CHECK(has_used_filament_without_target({external, unmapped}));
CHECK(has_used_filament_without_target({unmapped})); // wholly unmapped: all-invalid flow, not this refusal
CHECK(has_any_mapped_target({box, unmapped}));
CHECK(has_any_mapped_target({external, unmapped}));
CHECK_FALSE(has_any_mapped_target({unmapped}));
}
// A device with no AMS units has one source, the external spool. OrcaSlicer's
// auto-mapping force-selects it for every filament; that is not a lane choice, so it
// must not gate the print or reach print.gcode_file.
TEST_CASE("a no-AMS device drops the forced external-spool selection", "[OrcaPrinterAgent]") {
using Slic3r::GUI::drop_forced_external_selection;
using Slic3r::GUI::has_engaged_filament_mapping;
std::string external_only = R"([{"ams_id":255,"slot_id":0}])";
drop_forced_external_selection(/*device_has_ams=*/false, external_only);
CHECK(external_only.empty());
CHECK_FALSE(has_engaged_filament_mapping(external_only));
CHECK(Probe::build_filament_mapping(external_only).empty());
std::string box_mapping = R"([{"ams_id":0,"slot_id":2}])";
drop_forced_external_selection(/*device_has_ams=*/true, box_mapping);
CHECK(box_mapping == R"([{"ams_id":0,"slot_id":2}])");
CHECK(has_engaged_filament_mapping(box_mapping));
}
// An empty mapping must leave the gcode_file payload byte-identical to today:
// exactly command, sequence_id and param, with no filament_mapping key.
TEST_CASE("gcode_file payload omits filament_mapping when the map is empty", "[OrcaPrinterAgent]") {
const nlohmann::json empty = Probe::build_gcode_file_payload("7", "job.gcode", nlohmann::json::array());
REQUIRE(empty.contains("print"));
CHECK(empty["print"].size() == 3);
CHECK(empty["print"]["command"] == "gcode_file");
CHECK(empty["print"]["sequence_id"] == "7");
CHECK(empty["print"]["param"] == "job.gcode");
CHECK_FALSE(empty["print"].contains("filament_mapping"));
const nlohmann::json mapping = Probe::build_filament_mapping(R"([{"ams_id":1,"slot_id":0},{"ams_id":255,"slot_id":255}])");
const nlohmann::json with = Probe::build_gcode_file_payload("8", "job.gcode", mapping);
REQUIRE(with["print"].contains("filament_mapping"));
REQUIRE(with["print"]["filament_mapping"].size() == 1);
CHECK(with["print"]["filament_mapping"][0]["filament_index"] == 0);
}
// The defensive gate: a mapped print is refused before anything is published when the
// connector never advertised filament_mapping. The GUI send gates make this visible first;
// this covers callers that bypass them (calibration, plugin). A sentinel-only mapping does
// not engage the gate and falls through to the normal publish path.
TEST_CASE("an engaged mapping is refused when the connector never advertised filament_mapping", "[OrcaPrinterAgent]") {
OrcaPrinterAgent agent("/tmp");
Slic3r::PrintParams params;
params.dev_id = "dev-no-mapping-cap";
params.dst_file = "/tmp/job.gcode";
params.ams_mapping2 = R"([{"ams_id":0,"slot_id":2}])";
CHECK(agent.start_sdcard_print(params, {}, {}) == ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED);
params.ams_mapping2 = R"([{"ams_id":255,"slot_id":255}])";
CHECK(agent.start_sdcard_print(params, {}, {}) == BAMBU_NETWORK_ERR_PRINT_LP_PUBLISH_MSG_FAILED);
}
// The FTP "send with record" transport does not exist on OrcaSonar. It must
// report a non-success result so PrintJob falls back to start_print() rather
// than treating a print that was never sent as successful.
TEST_CASE("start_local_print_with_record never reports silent success", "[OrcaPrinterAgent]") {
Probe agent("/tmp");
Slic3r::PrintParams params;
const int rc = agent.start_local_print_with_record(params, {}, {}, {});
CHECK(rc < 0);
}
TEST_CASE("connect_printer wires up a LAN Config", "[OrcaPrinterAgent][.integration]") { TEST_CASE("connect_printer wires up a LAN Config", "[OrcaPrinterAgent][.integration]") {
Probe agent("/tmp"); Probe agent("/tmp");
Slic3r::PrinterConnectionParams params{ Slic3r::PrinterConnectionParams params{