diff --git a/src/slic3r/GUI/AMSSetting.cpp b/src/slic3r/GUI/AMSSetting.cpp index 9ba9031f19..608e8e9f3a 100644 --- a/src/slic3r/GUI/AMSSetting.cpp +++ b/src/slic3r/GUI/AMSSetting.cpp @@ -516,7 +516,8 @@ void AMSSetting::on_insert_material_read(wxCommandEvent &event) bool tray_read_opt = m_checkbox_Insert_material_auto_read->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(); Layout(); @@ -542,7 +543,8 @@ void AMSSetting::on_starting_read(wxCommandEvent &event) bool tray_read_opt = m_checkbox_Insert_material_auto_read->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(); Layout(); @@ -556,7 +558,8 @@ void AMSSetting::on_remain(wxCommandEvent& event) bool start_read_opt = m_checkbox_starting_auto_read->GetValue(); bool tray_read_opt = m_checkbox_Insert_material_auto_read->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(); } @@ -792,4 +795,4 @@ void AMSSettingArrangeAMSOrder::OnBtnRearrangeClicked(wxCommandEvent& event) } #endif -}} // namespace Slic3r::GUI \ No newline at end of file +}} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/DeviceManager.cpp b/src/slic3r/GUI/DeviceManager.cpp index d9beec51e1..18d5e7f14b 100644 --- a/src/slic3r/GUI/DeviceManager.cpp +++ b/src/slic3r/GUI/DeviceManager.cpp @@ -1702,8 +1702,8 @@ 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 extruder_id) { - if (!orca_ams_command_supported("print.ams_change_filament")) { - BOOST_LOG_TRIVIAL(warning) << "command_ams_change_filament: connector does not advertise the command"; + 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; @@ -1749,8 +1749,8 @@ 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) { - if (!orca_ams_command_supported("print.ams_user_setting")) { - BOOST_LOG_TRIVIAL(warning) << "command_ams_user_settings: connector does not advertise the command"; + 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; @@ -1761,28 +1761,44 @@ int MachineObject::command_ams_user_settings(bool start_read_opt, bool tray_read j["print"]["tray_read_option"] = tray_read_opt; j["print"]["calibrate_remain_flag"] = remain_flag; - m_fila_system->GetAmsSystemSetting().SetDetectOnInsertEnabled(tray_read_opt); - m_fila_system->GetAmsSystemSetting().SetDetectOnPowerupEnabled(start_read_opt); - m_fila_system->GetAmsSystemSetting().SetDetectRemainEnabled(remain_flag); - ams_user_setting_start = time(nullptr); - - return this->publish_json(j); + const int rc = this->publish_json(j); + if (rc == 0) { + m_fila_system->GetAmsSystemSetting().SetDetectOnInsertEnabled(tray_read_opt); + m_fila_system->GetAmsSystemSetting().SetDetectOnPowerupEnabled(start_read_opt); + m_fila_system->GetAmsSystemSetting().SetDetectRemainEnabled(remain_flag); + ams_user_setting_start = time(nullptr); + } + return rc; } int MachineObject::command_ams_calibrate(int ams_id) { 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())); } -bool MachineObject::orca_ams_command_supported(const char* command) const +bool MachineObject::printer_supports_command(const char* command) const { - if (printer_agent_id != ORCA_PRINTER_AGENT_ID) - return true; - // fms is the AMS axis; the advertised command set is the per-command gate. - if (!is_support_fms) + if (!command || !m_agent) return false; - return command != nullptr && supported_commands.count(command) != 0; + 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 @@ -1793,10 +1809,8 @@ bool MachineObject::supports_extrusion_cali() const int MachineObject::command_ams_filament_settings(int ams_id, int slot_id, std::string filament_id, std::string setting_id, std::string tray_color, std::string tray_type, int nozzle_temp_min, int nozzle_temp_max) { - // OrcaSonar: writing slot metadata is gated on the filament_slots capability. - // Absent/false means unsupported; Bambu keeps the legacy behaviour. - if (printer_agent_id == ORCA_PRINTER_AGENT_ID && !is_support_filament_slots) { - BOOST_LOG_TRIVIAL(warning) << "command_ams_filament_settings: printer does not advertise filament_slots"; + 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); } @@ -1834,11 +1848,8 @@ 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) { if (!m_agent) return -1; - // OrcaSonar: RFID read requires the connector to advertise the - // `print.ams_get_rfid` command (which already implies fms plus the macro). - // Unknown capability is not support; Bambu is untouched. - if (!orca_ams_command_supported("print.ams_get_rfid")) { - BOOST_LOG_TRIVIAL(warning) << "command_ams_refresh_rfid: connector does not advertise the command"; + 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())); @@ -1854,10 +1865,8 @@ int MachineObject::command_start_camera() int MachineObject::command_ams_select_tray(std::string tray_id) { if (!m_agent) return -1; - // OrcaSonar: this publishes the same macro-backed print.ams_change_filament - // as command_ams_change_filament, so it takes the same capability gate. - if (!orca_ams_command_supported("print.ams_change_filament")) { - BOOST_LOG_TRIVIAL(warning) << "command_ams_select_tray: connector does not advertise the command"; + 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())); @@ -1865,8 +1874,8 @@ int MachineObject::command_ams_select_tray(std::string tray_id) int MachineObject::command_ams_control(std::string action) { - if (!orca_ams_command_supported("print.ams_control")) { - BOOST_LOG_TRIVIAL(warning) << "command_ams_control: connector does not advertise the command"; + 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; @@ -1884,8 +1893,8 @@ int MachineObject::command_ams_control(std::string action) int MachineObject::command_ams_drying_stop() { - if (!orca_ams_command_supported("print.auto_stop_ams_dry")) { - BOOST_LOG_TRIVIAL(warning) << "command_ams_drying_stop: connector does not advertise the command"; + 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; @@ -2838,7 +2847,9 @@ bool MachineObject::is_camera_busy_off() int MachineObject::publish_json(const json& json_item, int qos, int flag) { 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); } else { rtn = cloud_publish_json(json_item.dump(), qos, flag); @@ -5577,14 +5588,10 @@ void MachineObject::parse_new_info2(const json& info) return; const auto flags_it = capabilities_it->find("flags"); const bool has_flags = flags_it != capabilities_it->end() && flags_it->is_object(); - // Bambu keeps the legacy behavior: a reply without flags is ignored. Orca - // connector-scope features/commands must still be parsed, so fall through - // with an empty flags object (every parse_bool below then no-ops). - if (!has_flags && printer_agent_id != ORCA_PRINTER_AGENT_ID) + if (!has_flags) return; - static const json empty_flags = json::object(); - const json& flags = has_flags ? *flags_it : empty_flags; - BOOST_LOG_TRIVIAL(info) << "parse_new_info2: OrcaSonar capability flags=" << flags.dump(); + const json& flags = *flags_it; + BOOST_LOG_TRIVIAL(info) << "parse_new_info2: capability flags=" << flags.dump(); auto parse_bool = [&flags](const char* name, bool& target) { const auto it = flags.find(name); @@ -5649,64 +5656,6 @@ void MachineObject::parse_new_info2(const json& info) m_config->ParseConfig(device_config); m_fan->ParseV2_0(fan_config); - - // OrcaSonar connector-scope capabilities. Absent or non-boolean means - // unsupported. A reply without `flags` still parses them (the fall-through - // above); Bambu (bbl) devices never enter this branch and keep the - // flags-only path. - if (printer_agent_id == ORCA_PRINTER_AGENT_ID) { - auto parse_features = [this](const json& features) { - if (!features.is_object()) - return; - auto set_flag = [&features](const char* name, bool& target) { - const auto it = features.find(name); - if (it != features.end() && it->is_boolean()) - target = it->get(); - }; - set_flag("fms", is_support_fms); - set_flag("filament_slots", is_support_filament_slots); - set_flag("filament_mapping", is_support_filament_mapping); - }; - - auto parse_commands = [this](const json& commands) { - if (!commands.is_array()) - return; - for (const auto& command : commands) { - if (command.is_string()) - supported_commands.insert(command.get()); - } - }; - - // Clear before filling so a reply without commands does not retain stale values. - supported_commands.clear(); - is_support_fms = false; - is_support_filament_slots = false; - is_support_filament_mapping = false; - - const auto supported_features_it = info.find("supported_features"); - if (supported_features_it != info.end()) - parse_features(*supported_features_it); - - const auto supported_commands_it = info.find("supported_commands"); - if (supported_commands_it != info.end()) - parse_commands(*supported_commands_it); - - const auto protocol_it = capabilities_it->find("protocol"); - if (protocol_it != capabilities_it->end() && protocol_it->is_object()) { - const auto protocol_features_it = protocol_it->find("features"); - if (protocol_features_it != protocol_it->end()) - parse_features(*protocol_features_it); - - const auto protocol_commands_it = protocol_it->find("supported_commands"); - if (protocol_commands_it != protocol_it->end()) - parse_commands(*protocol_commands_it); - } - - BOOST_LOG_TRIVIAL(info) << "parse_new_info2: fms=" << is_support_fms - << " filament_slots=" << is_support_filament_slots - << " filament_mapping=" << is_support_filament_mapping - << " supported_commands=" << supported_commands.size(); - } } static bool is_hex_digit(char c) { diff --git a/src/slic3r/GUI/DeviceManager.hpp b/src/slic3r/GUI/DeviceManager.hpp index d470fb6703..d7595bff03 100644 --- a/src/slic3r/GUI/DeviceManager.hpp +++ b/src/slic3r/GUI/DeviceManager.hpp @@ -653,13 +653,6 @@ public: bool is_support_partskip{false}; bool is_support_refresh_nozzle{false}; - // OrcaSonar connector-scope capabilities (printer_agent_id == "orca"). Parsed from the - // get_capabilities reply; never consulted on Bambu paths. - bool is_support_fms{false}; - bool is_support_filament_slots{false}; - bool is_support_filament_mapping{false}; - std::set supported_commands; - // refine printer function options bool is_support_spaghetti_detection{false}; bool is_support_purgechutepileup_detection{false}; @@ -801,10 +794,9 @@ public: int command_refresh_nozzle(); int command_set_chamber(int temp); int check_resume_condition(); - // OrcaSonar: true when a macro-backed AMS command may be sent. Bambu paths - // always allow; an Orca device must have advertised the command (unknown - // capability is not support). - bool orca_ams_command_supported(const char* command) const; + 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. diff --git a/src/slic3r/GUI/FilamentMappingUtils.hpp b/src/slic3r/GUI/FilamentMappingUtils.hpp index ca0d1560ad..377c4190fe 100644 --- a/src/slic3r/GUI/FilamentMappingUtils.hpp +++ b/src/slic3r/GUI/FilamentMappingUtils.hpp @@ -6,6 +6,7 @@ #include #include "libslic3r/ProjectTask.hpp" +#include "DeviceManager.hpp" namespace Slic3r { namespace GUI { @@ -70,5 +71,35 @@ inline bool has_any_mapped_target(const std::vector& result) 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& 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 diff --git a/src/slic3r/GUI/SelectMachine.cpp b/src/slic3r/GUI/SelectMachine.cpp index 7b8ee20bfc..30ccde08d4 100644 --- a/src/slic3r/GUI/SelectMachine.cpp +++ b/src/slic3r/GUI/SelectMachine.cpp @@ -3537,27 +3537,26 @@ void SelectMachineDialog::on_send_print() get_ams_mapping_result(ams_mapping_array,ams_mapping_array2, ams_mapping_info); - // A device with no AMS unit has one source, the external spool: OrcaSlicer's - // auto-mapping force-selects it, which is not a lane choice. Bambu unchanged. - if (obj_->printer_agent_id == ORCA_PRINTER_AGENT_ID) - drop_forced_external_selection(obj_->HasAms(), ams_mapping_array2); - - // OrcaSonar: a mapped print requires the connector to advertise - // filament_mapping. Refuse rather than start with the map silently dropped; - // Bambu keeps its behavior. - if (obj_->printer_agent_id == ORCA_PRINTER_AGENT_ID) { - if (!obj_->is_support_filament_mapping && has_engaged_filament_mapping(ams_mapping_array2)) { - 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; - } - if (has_any_mapped_target(m_ams_mapping_result) && has_used_filament_without_target(m_ams_mapping_result)) { - 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; - } + // 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) { diff --git a/src/slic3r/GUI/SendMultiMachinePage.cpp b/src/slic3r/GUI/SendMultiMachinePage.cpp index 172a124f19..d9452d59b8 100644 --- a/src/slic3r/GUI/SendMultiMachinePage.cpp +++ b/src/slic3r/GUI/SendMultiMachinePage.cpp @@ -744,30 +744,29 @@ void SendMultiMachinePage::on_send(wxCommandEvent& event) if (!wxGetApp().is_blocking_printing(obj)) { PrintParams params = request_params(obj); - // A device with no AMS unit has one source, the external spool: OrcaSlicer's - // auto-mapping force-selects it, which is not a lane choice. - if (obj->printer_agent_id == ORCA_PRINTER_AGENT_ID) - drop_forced_external_selection(obj->HasAms(), params.ams_mapping2); - // OrcaSonar: a mapped print requires the connector to advertise - // filament_mapping, and a partially mapped print must not silently drop a - // used filament. Any entry the serializer would put on the wire engages the - // gate, external slots ({255,0}/{254,0}) included; the extra-spool branch - // rewrites to exactly those. Bambu is unchanged. - if (obj->printer_agent_id == ORCA_PRINTER_AGENT_ID) { - if (!obj->is_support_filament_mapping && has_engaged_filament_mapping(params.ams_mapping2)) { - BOOST_LOG_TRIVIAL(warning) << "SendMultiMachinePage: connector does not advertise filament_mapping; refusing mapped print for " - << obj->get_dev_id(); - MessageDialog msg_wingow(nullptr, _L("AMS filament mapping is not available for this printer. Clear the AMS mapping before printing."), "", wxICON_WARNING | wxOK); - msg_wingow.ShowModal(); - return; - } - if (params.task_use_ams && has_any_mapped_target(m_ams_mapping_result) && - has_used_filament_without_target(m_ams_mapping_result)) { - BOOST_LOG_TRIVIAL(warning) << "SendMultiMachinePage: a used filament has no target; refusing print for " << obj->get_dev_id(); - MessageDialog msg_wingow(nullptr, _L("A filament used by this print has no AMS mapping. Assign it before printing."), "", wxICON_WARNING | wxOK); - msg_wingow.ShowModal(); - return; - } + 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); } diff --git a/src/slic3r/Utils/IPrinterAgent.hpp b/src/slic3r/Utils/IPrinterAgent.hpp index 293b0af2b8..db1ff7ab38 100644 --- a/src/slic3r/Utils/IPrinterAgent.hpp +++ b/src/slic3r/Utils/IPrinterAgent.hpp @@ -109,6 +109,16 @@ public: */ 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 // gcode live on the agent that speaks it; the default is an honest refusal that MachineObject's // publish funnel turns into a dialog. diff --git a/src/slic3r/Utils/NetworkAgent.cpp b/src/slic3r/Utils/NetworkAgent.cpp index 1f7705658e..403dfa0dfe 100644 --- a/src/slic3r/Utils/NetworkAgent.cpp +++ b/src/slic3r/Utils/NetworkAgent.cpp @@ -793,6 +793,36 @@ int NetworkAgent::send_message(std::string dev_id, std::string json_str, int qos 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) { if (m_printer_agent) diff --git a/src/slic3r/Utils/NetworkAgent.hpp b/src/slic3r/Utils/NetworkAgent.hpp index f93178db8b..5e2ad90d62 100644 --- a/src/slic3r/Utils/NetworkAgent.hpp +++ b/src/slic3r/Utils/NetworkAgent.hpp @@ -150,6 +150,10 @@ public: int set_on_local_message_fn(OnMessageFn fn); int set_server_callback(OnServerErrFn fn); 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_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); diff --git a/src/slic3r/Utils/OrcaPrinterAgent.cpp b/src/slic3r/Utils/OrcaPrinterAgent.cpp index 42e52b5510..9ca0309253 100644 --- a/src/slic3r/Utils/OrcaPrinterAgent.cpp +++ b/src/slic3r/Utils/OrcaPrinterAgent.cpp @@ -43,19 +43,43 @@ namespace { namespace fs = boost::filesystem; -// Per-device filament_mapping capability, mirrored from the get_capabilities -// reply in merge_capabilities and read by start_sdcard_print so the field is -// refused defensively when the connector never advertised it. -std::mutex g_filament_mapping_mutex; -std::unordered_map g_filament_mapping_cache; - -// True only when the connector's get_capabilities reply advertised -// filament_mapping for this device. Unknown is not support. -bool filament_mapping_advertised(const std::string& dev_id) +struct OrcaDeviceCapabilities { - std::lock_guard l(g_filament_mapping_mutex); - const auto it = g_filament_mapping_cache.find(dev_id); - return it != g_filament_mapping_cache.end() && it->second; + bool fms = false; + bool filament_slots = false; + bool filament_mapping = false; + std::set supported_commands; +}; + +std::mutex g_capabilities_mutex; +std::unordered_map g_capabilities; + +OrcaDeviceCapabilities capabilities_for(const std::string& dev_id) +{ + std::lock_guard 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 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 @@ -462,6 +486,14 @@ OrcaPrinterAgent::~OrcaPrinterAgent() ++m_lan_generation; // fence any late worker callback ++m_cloud_generation; + std::string lan_dev_id; + std::string cloud_dev_id; + { + std::lock_guard 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 // cloud agent outlives the printer agent (NetworkAgent::set_printer_agent swaps // the printer agent while m_cloud_agents persist). @@ -497,6 +529,8 @@ OrcaPrinterAgent::~OrcaPrinterAgent() std::lock_guard l(state_mutex); lan_mqtt_connection.reset(); } + forget_device_capabilities(lan_dev_id); + forget_device_capabilities(cloud_dev_id); } OrcaCloudServiceAgent* OrcaPrinterAgent::get_orca_cloud_agent() @@ -604,31 +638,50 @@ std::string OrcaPrinterAgent::merge_capabilities(const std::string& dev_id, cons nozzle_diameter_cache[dev_id] = nozzle_dia; } - // Per-device filament_mapping capability. Both feature maps carry it; - // either being true means the connector advertised it. - bool mapping_advertised = false; - { - const auto top_features = info_it->find("supported_features"); - if (top_features != info_it->end() && top_features->is_object()) { - const auto it = top_features->find("filament_mapping"); - if (it != top_features->end() && it->is_boolean()) - mapping_advertised = it->get(); + // 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(); + }; + 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()); } - if (!mapping_advertised && 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 features_it = protocol_it->find("features"); - if (features_it != protocol_it->end() && features_it->is_object()) { - const auto it = features_it->find("filament_mapping"); - if (it != features_it->end() && it->is_boolean()) - mapping_advertised = it->get(); - } - } + }; + + 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 l(g_filament_mapping_mutex); - g_filament_mapping_cache[dev_id] = mapping_advertised; + std::lock_guard lock(g_capabilities_mutex); + g_capabilities[dev_id] = std::move(device_capabilities); } // The capabilities reply itself is forwarded unchanged. } @@ -656,6 +709,14 @@ std::string OrcaPrinterAgent::merge_capabilities(const std::string& dev_id, cons return modified ? envelope.dump() : payload; } +void OrcaPrinterAgent::forget_device_capabilities(const std::string& dev_id) +{ + if (dev_id.empty()) + return; + std::lock_guard 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) { parse_ipcam_info(dev_id, payload); @@ -743,6 +804,56 @@ void OrcaPrinterAgent::set_cloud_agent(std::shared_ptr cloud 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); } +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 = ""; + 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(); + 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) { (void) ams_id; @@ -1053,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; return BAMBU_NETWORK_ERR_INVALID_HANDLE; } + forget_device_capabilities(params.dev_id); disconnect_printer(); const uint64_t gen = ++m_lan_generation; @@ -1109,6 +1221,7 @@ int OrcaPrinterAgent::connect_printer(const PrinterConnectionParams& params) on_connected(params.dev_id, conn, gen); dispatch_local_connect(ConnectStatusOk, params.dev_id, "0"); } else if (!connected && !initial) { + forget_device_capabilities(params.dev_id); dispatch_local_connect(ConnectStatusLost, params.dev_id, "connection_lost"); } }); @@ -1160,6 +1273,7 @@ int OrcaPrinterAgent::disconnect_printer() } 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 << " had_connection=" << (doomed ? "yes" : "no") << " connected=" << (doomed && doomed->is_connected() ? "yes" : "no") @@ -1184,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) { - std::string command = ""; - try { - const nlohmann::json envelope = nlohmann::json::parse(json_str); - 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(); - break; - } - } - } - } catch (const std::exception&) { - // Preserve the transport's existing behavior for malformed payloads; - // the printer will report the protocol error asynchronously. - } + std::string command; + std::string prepared; + const int prepare_rc = prepare_outgoing_request(dev_id, json_str, command, prepared); BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent::route_send is_lan=" << is_lan << " dev_id=" << dev_id << " command=" << command << " payload_bytes=" << json_str.size(); if (dev_id.empty()) 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); if (!conn) 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 << " dev_id=" << dev_id; return queued ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_CONNECTION_TO_SERVER_FAILED; @@ -1338,19 +1442,20 @@ int OrcaPrinterAgent::set_user_selected_machine(std::string dev_id) } 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 << " cloud=" << (cloud ? "set" : "") << " transport=" << connection_type_name(previous_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) { BOOST_LOG_TRIVIAL(warning) << "OrcaPrinterAgent::set_user_selected_machine: no Orca cloud agent"; 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(); if (!previous.empty()) { cloud->del_subscribe({previous}); @@ -1380,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 auto state_handler = [this](bool connected, bool initial) { - if (!connected || initial) + if (initial) return; + if (!connected) { + forget_device_capabilities(get_user_selected_machine()); + return; + } auto* current_cloud = get_orca_cloud_agent(); OrcaMqttConnection* current_conn = current_cloud ? current_cloud->get_mqtt_connection() : nullptr; const std::string selected = get_user_selected_machine(); @@ -1397,6 +1506,12 @@ int OrcaPrinterAgent::set_user_selected_machine(std::string dev_id) return BAMBU_NETWORK_SUCCESS; } +int OrcaPrinterAgent::unbind(std::string dev_id) +{ + forget_device_capabilities(dev_id); + return BAMBU_NETWORK_SUCCESS; +} + // ============================================================================ // Agent Information // ============================================================================ @@ -1711,7 +1826,7 @@ int OrcaPrinterAgent::start_sdcard_print(PrintParams params, OnUpdateStatusFn up // 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 (!filament_mapping_advertised(params.dev_id)) { + 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; } diff --git a/src/slic3r/Utils/OrcaPrinterAgent.hpp b/src/slic3r/Utils/OrcaPrinterAgent.hpp index 808b2b0aea..3489b37514 100644 --- a/src/slic3r/Utils/OrcaPrinterAgent.hpp +++ b/src/slic3r/Utils/OrcaPrinterAgent.hpp @@ -39,6 +39,9 @@ public: // Communication 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 disconnect_printer() 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 std::string get_user_selected_machine() override; int set_user_selected_machine(std::string dev_id) override; + int unbind(std::string dev_id) override; /** * Get agent information. @@ -129,6 +133,9 @@ protected: // once parse_json reads the Orca dialect natively. See the definition for the // per-rule detail. Returns the payload unchanged when no rule applies. 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 // the other printer agents. The transport result cannot be returned by diff --git a/tests/slic3rutils/test_device_manager_integration.cpp b/tests/slic3rutils/test_device_manager_integration.cpp index d6f65e8993..2f63920974 100644 --- a/tests/slic3rutils/test_device_manager_integration.cpp +++ b/tests/slic3rutils/test_device_manager_integration.cpp @@ -2,6 +2,8 @@ #include #include +#include +#include #include #include #include @@ -17,6 +19,8 @@ #include using namespace Slic3r; +using Slic3r::GUI::MappingSendError; +using Slic3r::GUI::prepare_filament_mapping_for_send; using json = nlohmann::json; namespace { @@ -48,6 +52,30 @@ public: 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: AgentInfo m_info; }; @@ -160,120 +188,135 @@ TEST_CASE("Device manager filters and rehomes devices by printer-agent ownership CHECK(manager.get_my_machine_list("integration-agent-b").count(machine.dev_id) == 1); } -TEST_CASE("Orca capability reply populates connector-scope features and commands", "[DeviceManager][integration]") +TEST_CASE("Network agent rejects capability queries for a different device owner", "[DeviceManager][integration]") { - ScopedAppConfig app_config; - NetworkAgent network(nullptr, std::make_shared("orca")); - DeviceManager manager(&network, false, &app_config.config); + NetworkAgent network(nullptr, std::make_shared("bbl")); - BBLocalMachine orca_machine; - orca_machine.dev_id = "orca-device"; - orca_machine.dev_name = "Orca device"; - orca_machine.dev_ip = "192.0.2.21"; - orca_machine.printer_type = "C11"; - MachineObject* orca_obj = manager.insert_local_device(orca_machine, "lan", "free", "", "access-code"); - REQUIRE(orca_obj != nullptr); - orca_obj->printer_agent_id = "orca"; + 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")); - orca_obj->parse_new_info2(json::parse(R"({ - "command": "get_capabilities", - "supported_features": {"fms": true, "filament_slots": true, "filament_mapping": true}, - "supported_commands": ["print.push_status", "print.ams_get_rfid"], - "capabilities": { - "flags": {}, - "protocol": { - "features": {"filament_mapping": true}, - "supported_commands": ["print.ams_change_filament"] - } - } - })")); - CHECK(orca_obj->is_support_fms); - CHECK(orca_obj->is_support_filament_slots); - CHECK(orca_obj->is_support_filament_mapping); - CHECK(orca_obj->supported_commands.count("print.push_status") == 1); - CHECK(orca_obj->supported_commands.count("print.ams_get_rfid") == 1); - CHECK(orca_obj->supported_commands.count("print.ams_change_filament") == 1); + // 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")); - // Absent or false reads as unsupported, and a later reply without commands clears the set. - orca_obj->parse_new_info2(json::parse(R"({ - "command": "get_capabilities", - "supported_features": {"fms": false, "filament_slots": false, "filament_mapping": false}, - "capabilities": {"flags": {}} - })")); - CHECK_FALSE(orca_obj->is_support_fms); - CHECK_FALSE(orca_obj->is_support_filament_slots); - CHECK_FALSE(orca_obj->is_support_filament_mapping); - CHECK(orca_obj->supported_commands.empty()); - - // A reply without capabilities.flags must still parse the Orca features and - // commands (fail-closed: don't retain stale "supported" values). - orca_obj->parse_new_info2(json::parse(R"({ - "command": "get_capabilities", - "supported_features": {"filament_mapping": true}, - "capabilities": { - "protocol": {"supported_commands": ["print.ams_get_rfid"]} - } - })")); - CHECK(orca_obj->is_support_filament_mapping); - CHECK_FALSE(orca_obj->is_support_filament_slots); - CHECK(orca_obj->supported_commands.count("print.ams_get_rfid") == 1); - - // A non-Orca agent id leaves the connector-scope fields untouched. - BBLocalMachine bbl_machine; - bbl_machine.dev_id = "bbl-device"; - bbl_machine.dev_name = "Bambu device"; - bbl_machine.dev_ip = "192.0.2.22"; - bbl_machine.printer_type = "C11"; - MachineObject* bbl_obj = manager.insert_local_device(bbl_machine, "lan", "free", "", "access-code"); - REQUIRE(bbl_obj != nullptr); - bbl_obj->printer_agent_id = "bbl"; - - bbl_obj->parse_new_info2(json::parse(R"({ - "command": "get_capabilities", - "supported_features": {"fms": true, "filament_slots": true, "filament_mapping": true}, - "supported_commands": ["print.push_status"], - "capabilities": { - "flags": {}, - "protocol": { - "features": {"fms": true, "filament_slots": true, "filament_mapping": true}, - "supported_commands": ["print.ams_get_rfid"] - } - } - })")); - CHECK_FALSE(bbl_obj->is_support_fms); - CHECK_FALSE(bbl_obj->is_support_filament_slots); - CHECK_FALSE(bbl_obj->is_support_filament_mapping); - CHECK(bbl_obj->supported_commands.empty()); + NetworkAgent orca_network(nullptr, std::make_shared("orca")); + CHECK(orca_network.uses_filament_mapping("orca")); + CHECK_FALSE(orca_network.uses_filament_mapping("bbl")); } -TEST_CASE("Orca per-command AMS gate requires fms and the advertised command", "[DeviceManager][integration]") +// 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("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("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; - NetworkAgent network(nullptr, std::make_shared("orca")); + auto printer_agent = std::make_shared("bbl"); + NetworkAgent network(nullptr, printer_agent); DeviceManager manager(&network, false, &app_config.config); BBLocalMachine machine; - machine.dev_id = "orca-gate"; - machine.dev_name = "Orca gate"; - machine.dev_ip = "192.0.2.30"; + 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); - obj->printer_agent_id = "orca"; - // fms off: no macro-backed AMS command is allowed even if listed. - obj->is_support_fms = false; - obj->supported_commands.insert("print.ams_control"); - CHECK_FALSE(obj->orca_ams_command_supported("print.ams_control")); + 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"); +} - // fms on: only the commands actually advertised are allowed. - obj->is_support_fms = true; - CHECK(obj->orca_ams_command_supported("print.ams_control")); - CHECK_FALSE(obj->orca_ams_command_supported("print.ams_get_rfid")); +TEST_CASE("AMS user settings do not update local state when publishing fails", "[DeviceManager][integration]") +{ + ScopedAppConfig app_config; + auto printer_agent = std::make_shared("bbl"); + NetworkAgent network(nullptr, printer_agent); + DeviceManager manager(&network, false, &app_config.config); - // Bambu keeps the legacy permissive path. - obj->printer_agent_id = "bbl"; - CHECK(obj->orca_ams_command_supported("print.anything")); + 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 diff --git a/tests/slic3rutils/test_orca_printer_agent.cpp b/tests/slic3rutils/test_orca_printer_agent.cpp index 6fe7b96c8d..867c3e3090 100644 --- a/tests/slic3rutils/test_orca_printer_agent.cpp +++ b/tests/slic3rutils/test_orca_printer_agent.cpp @@ -25,6 +25,7 @@ struct Probe : OrcaPrinterAgent { using OrcaPrinterAgent::lan_connection_target; using OrcaPrinterAgent::build_filament_mapping; using OrcaPrinterAgent::build_gcode_file_payload; + using OrcaPrinterAgent::prepare_outgoing_request; }; } @@ -71,6 +72,91 @@ TEST_CASE("OrcaPrinterAgent stamps the get_capabilities nozzle diameter onto pus 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]") { std::string h, p; REQUIRE(Probe::parse_lan_endpoint("192.168.1.9", h, p));