Removal of specific ORCA_PRINTER_AGENT_ID usage in DeviceManager/SelectMachine/SendMultiMachinePage. Generalized the capabilities under IPrinterAgent

This commit is contained in:
Lam Wei Lun
2026-09-30 18:01:17 +08:00
parent e45d52efd1
commit ad8c6e741e
13 changed files with 582 additions and 314 deletions
+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();
} }
+48 -99
View File
@@ -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<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 (!orca_ams_command_supported("print.ams_change_filament")) { if (!printer_supports_command("print.ams_change_filament")) {
BOOST_LOG_TRIVIAL(warning) << "command_ams_change_filament: connector does not advertise the command"; 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); return command_with_dialog(ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED);
} }
json j; 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) 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")) { if (!printer_supports_command("print.ams_user_setting")) {
BOOST_LOG_TRIVIAL(warning) << "command_ams_user_settings: connector does not advertise the command"; 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); return command_with_dialog(ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED);
} }
json j; 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"]["tray_read_option"] = tray_read_opt;
j["print"]["calibrate_remain_flag"] = remain_flag; j["print"]["calibrate_remain_flag"] = remain_flag;
m_fila_system->GetAmsSystemSetting().SetDetectOnInsertEnabled(tray_read_opt); const int rc = this->publish_json(j);
m_fila_system->GetAmsSystemSetting().SetDetectOnPowerupEnabled(start_read_opt); if (rc == 0) {
m_fila_system->GetAmsSystemSetting().SetDetectRemainEnabled(remain_flag); m_fila_system->GetAmsSystemSetting().SetDetectOnInsertEnabled(tray_read_opt);
ams_user_setting_start = time(nullptr); m_fila_system->GetAmsSystemSetting().SetDetectOnPowerupEnabled(start_read_opt);
m_fila_system->GetAmsSystemSetting().SetDetectRemainEnabled(remain_flag);
return this->publish_json(j); ams_user_setting_start = time(nullptr);
}
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::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) if (!command || !m_agent)
return true;
// fms is the AMS axis; the advertised command set is the per-command gate.
if (!is_support_fms)
return false; 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 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) 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. if (!printer_supports_command("print.ams_filament_setting")) {
// Absent/false means unsupported; Bambu keeps the legacy behaviour. BOOST_LOG_TRIVIAL(warning) << "command_ams_filament_settings: printer agent does not support slot metadata updates";
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";
return command_with_dialog(ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED); 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) int MachineObject::command_ams_refresh_rfid(int ams_id, int slot_id)
{ {
if (!m_agent) return -1; if (!m_agent) return -1;
// OrcaSonar: RFID read requires the connector to advertise the if (!printer_supports_command("print.ams_get_rfid")) {
// `print.ams_get_rfid` command (which already implies fms plus the macro). BOOST_LOG_TRIVIAL(warning) << "command_ams_refresh_rfid: printer agent does not support the command";
// 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";
return command_with_dialog(ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED); 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()));
@@ -1854,10 +1865,8 @@ 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;
// OrcaSonar: this publishes the same macro-backed print.ams_change_filament if (!printer_supports_command("print.ams_change_filament")) {
// as command_ams_change_filament, so it takes the same capability gate. BOOST_LOG_TRIVIAL(warning) << "command_ams_select_tray: printer agent does not support the command";
if (!orca_ams_command_supported("print.ams_change_filament")) {
BOOST_LOG_TRIVIAL(warning) << "command_ams_select_tray: connector does not advertise the command";
return command_with_dialog(ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED); 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()));
@@ -1865,8 +1874,8 @@ int MachineObject::command_ams_select_tray(std::string tray_id)
int MachineObject::command_ams_control(std::string action) int MachineObject::command_ams_control(std::string action)
{ {
if (!orca_ams_command_supported("print.ams_control")) { if (!printer_supports_command("print.ams_control")) {
BOOST_LOG_TRIVIAL(warning) << "command_ams_control: connector does not advertise the command"; BOOST_LOG_TRIVIAL(warning) << "command_ams_control: printer agent does not support the command";
return command_with_dialog(ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED); 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;
@@ -1884,8 +1893,8 @@ int MachineObject::command_ams_control(std::string action)
int MachineObject::command_ams_drying_stop() int MachineObject::command_ams_drying_stop()
{ {
if (!orca_ams_command_supported("print.auto_stop_ams_dry")) { if (!printer_supports_command("print.auto_stop_ams_dry")) {
BOOST_LOG_TRIVIAL(warning) << "command_ams_drying_stop: connector does not advertise the command"; 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); return command_with_dialog(ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED);
} }
json j; 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 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);
@@ -5577,14 +5588,10 @@ void MachineObject::parse_new_info2(const json& info)
return; return;
const auto flags_it = capabilities_it->find("flags"); const auto flags_it = capabilities_it->find("flags");
const bool has_flags = flags_it != capabilities_it->end() && flags_it->is_object(); 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 if (!has_flags)
// 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)
return; return;
static const json empty_flags = json::object(); const json& flags = *flags_it;
const json& flags = has_flags ? *flags_it : empty_flags; BOOST_LOG_TRIVIAL(info) << "parse_new_info2: capability flags=" << flags.dump();
BOOST_LOG_TRIVIAL(info) << "parse_new_info2: OrcaSonar 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);
@@ -5649,64 +5656,6 @@ void MachineObject::parse_new_info2(const json& info)
m_config->ParseConfig(device_config); m_config->ParseConfig(device_config);
m_fan->ParseV2_0(fan_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<bool>();
};
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<std::string>());
}
};
// 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) { static bool is_hex_digit(char c) {
+3 -11
View File
@@ -653,13 +653,6 @@ public:
bool is_support_partskip{false}; bool is_support_partskip{false};
bool is_support_refresh_nozzle{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<std::string> supported_commands;
// refine printer function options // refine printer function options
bool is_support_spaghetti_detection{false}; bool is_support_spaghetti_detection{false};
bool is_support_purgechutepileup_detection{false}; bool is_support_purgechutepileup_detection{false};
@@ -801,10 +794,9 @@ 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();
// OrcaSonar: true when a macro-backed AMS command may be sent. Bambu paths bool printer_supports_command(const char* command) const;
// always allow; an Orca device must have advertised the command (unknown bool printer_supports_feature(const char* feature) const;
// capability is not support). bool printer_uses_filament_mapping() const;
bool orca_ams_command_supported(const char* command) const;
// The per-tray flow-dynamics K/N records are a Bambu firmware feature. Other // 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 // agents have no printer-side calibration to read or write, so the AMS UI must
// not offer K/N for them. // not offer K/N for them.
+31
View File
@@ -6,6 +6,7 @@
#include <nlohmann/json.hpp> #include <nlohmann/json.hpp>
#include "libslic3r/ProjectTask.hpp" #include "libslic3r/ProjectTask.hpp"
#include "DeviceManager.hpp"
namespace Slic3r { namespace Slic3r {
namespace GUI { namespace GUI {
@@ -70,5 +71,35 @@ inline bool has_any_mapped_target(const std::vector<FilamentInfo>& result)
return false; 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 GUI
} // namespace Slic3r } // namespace Slic3r
+20 -21
View File
@@ -3537,27 +3537,26 @@ 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);
// A device with no AMS unit has one source, the external spool: OrcaSlicer's // The policy lives in FilamentMappingUtils.hpp; this dialog only differs
// auto-mapping force-selects it, which is not a lane choice. Bambu unchanged. // from the multi-device page in how it reports a refusal.
if (obj_->printer_agent_id == ORCA_PRINTER_AGENT_ID) switch (prepare_filament_mapping_for_send(obj_, ams_mapping_array2, m_ams_mapping_result)) {
drop_forced_external_selection(obj_->HasAms(), ams_mapping_array2); case MappingSendError::unsupported:
BOOST_LOG_TRIVIAL(warning) << "print_job: connector does not advertise filament_mapping; refusing mapped print";
// OrcaSonar: a mapped print requires the connector to advertise m_status_bar->set_status_text(_L("AMS filament mapping is not available for this printer. Clear the AMS mapping before printing."));
// filament_mapping. Refuse rather than start with the map silently dropped; Enable_Send_Button(true);
// Bambu keeps its behavior. return;
if (obj_->printer_agent_id == ORCA_PRINTER_AGENT_ID) { case MappingSendError::incomplete:
if (!obj_->is_support_filament_mapping && has_engaged_filament_mapping(ams_mapping_array2)) { BOOST_LOG_TRIVIAL(warning) << "print_job: a used filament has no AMS target; refusing print";
BOOST_LOG_TRIVIAL(warning) << "print_job: connector does not advertise filament_mapping; refusing mapped print"; m_status_bar->set_status_text(_L("A filament used by this print has no AMS mapping. Assign it before printing."));
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);
Enable_Send_Button(true); return;
return; case MappingSendError::none:
} break;
if (has_any_mapped_target(m_ams_mapping_result) && has_used_filament_without_target(m_ams_mapping_result)) { default:
BOOST_LOG_TRIVIAL(warning) << "print_job: a used filament has no AMS target; refusing print"; // A refusal added without a handler here must not silently send.
m_status_bar->set_status_text(_L("A filament used by this print has no AMS mapping. Assign it before printing.")); BOOST_LOG_TRIVIAL(warning) << "print_job: unrecognized mapping refusal; refusing print";
Enable_Send_Button(true); Enable_Send_Button(true);
return; return;
}
} }
if (m_print_type == PrintFromType::FROM_NORMAL) { if (m_print_type == PrintFromType::FROM_NORMAL) {
+23 -24
View File
@@ -744,30 +744,29 @@ 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);
// A device with no AMS unit has one source, the external spool: OrcaSlicer's switch (prepare_filament_mapping_for_send(obj, params.ams_mapping2, m_ams_mapping_result)) {
// auto-mapping force-selects it, which is not a lane choice. case MappingSendError::unsupported:
if (obj->printer_agent_id == ORCA_PRINTER_AGENT_ID) {
drop_forced_external_selection(obj->HasAms(), params.ams_mapping2); BOOST_LOG_TRIVIAL(warning) << "SendMultiMachinePage: connector does not advertise filament_mapping; refusing mapped print for "
// OrcaSonar: a mapped print requires the connector to advertise << obj->get_dev_id();
// filament_mapping, and a partially mapped print must not silently drop a MessageDialog unavailable_msg(nullptr, _L("AMS filament mapping is not available for this printer. Clear the AMS mapping before printing."), "", wxICON_WARNING | wxOK);
// used filament. Any entry the serializer would put on the wire engages the unavailable_msg.ShowModal();
// gate, external slots ({255,0}/{254,0}) included; the extra-spool branch return;
// rewrites to exactly those. Bambu is unchanged. }
if (obj->printer_agent_id == ORCA_PRINTER_AGENT_ID) { case MappingSendError::incomplete:
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 " BOOST_LOG_TRIVIAL(warning) << "SendMultiMachinePage: a used filament has no target; refusing print for " << obj->get_dev_id();
<< 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);
MessageDialog msg_wingow(nullptr, _L("AMS filament mapping is not available for this printer. Clear the AMS mapping before printing."), "", wxICON_WARNING | wxOK); incomplete_msg.ShowModal();
msg_wingow.ShowModal(); return;
return; }
} case MappingSendError::none:
if (params.task_use_ams && has_any_mapped_target(m_ams_mapping_result) && break;
has_used_filament_without_target(m_ams_mapping_result)) { default:
BOOST_LOG_TRIVIAL(warning) << "SendMultiMachinePage: a used filament has no target; refusing print for " << obj->get_dev_id(); // A refusal added without a handler here must not silently send.
MessageDialog msg_wingow(nullptr, _L("A filament used by this print has no AMS mapping. Assign it before printing."), "", wxICON_WARNING | wxOK); BOOST_LOG_TRIVIAL(warning) << "SendMultiMachinePage: unrecognized mapping refusal for "
msg_wingow.ShowModal(); << obj->get_dev_id();
return; return;
}
} }
print_params.push_back(params); print_params.push_back(params);
} }
+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);
+173 -58
View File
@@ -43,19 +43,43 @@ namespace {
namespace fs = boost::filesystem; namespace fs = boost::filesystem;
// Per-device filament_mapping capability, mirrored from the get_capabilities struct OrcaDeviceCapabilities
// 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<std::string, bool> 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)
{ {
std::lock_guard<std::mutex> l(g_filament_mapping_mutex); bool fms = false;
const auto it = g_filament_mapping_cache.find(dev_id); bool filament_slots = false;
return it != g_filament_mapping_cache.end() && it->second; 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
@@ -462,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).
@@ -497,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()
@@ -604,31 +638,50 @@ std::string OrcaPrinterAgent::merge_capabilities(const std::string& dev_id, cons
nozzle_diameter_cache[dev_id] = nozzle_dia; nozzle_diameter_cache[dev_id] = nozzle_dia;
} }
// Per-device filament_mapping capability. Both feature maps carry it; // Connector capabilities are retained per device for command dispatch
// either being true means the connector advertised it. // and GUI capability queries. A fresh reply replaces stale support.
bool mapping_advertised = false; OrcaDeviceCapabilities device_capabilities;
{ auto parse_features = [&device_capabilities](const nlohmann::json& features) {
const auto top_features = info_it->find("supported_features"); if (!features.is_object())
if (top_features != info_it->end() && top_features->is_object()) { return;
const auto it = top_features->find("filament_mapping"); auto read_bool = [&features](const char* key, bool& output) {
if (it != top_features->end() && it->is_boolean()) const auto it = features.find(key);
mapping_advertised = it->get<bool>(); 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>());
} }
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 top_features = info_it->find("supported_features");
const auto features_it = protocol_it->find("features"); if (top_features != info_it->end())
if (features_it != protocol_it->end() && features_it->is_object()) { parse_features(*top_features);
const auto it = features_it->find("filament_mapping"); const auto top_commands = info_it->find("supported_commands");
if (it != features_it->end() && it->is_boolean()) if (top_commands != info_it->end())
mapping_advertised = it->get<bool>(); 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> l(g_filament_mapping_mutex); std::lock_guard<std::mutex> lock(g_capabilities_mutex);
g_filament_mapping_cache[dev_id] = mapping_advertised; g_capabilities[dev_id] = std::move(device_capabilities);
} }
// The capabilities reply itself is forwarded unchanged. // 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; 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);
@@ -743,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;
@@ -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; 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;
@@ -1109,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");
} }
}); });
@@ -1160,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")
@@ -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) 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;
@@ -1338,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});
@@ -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 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();
@@ -1397,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
// ============================================================================ // ============================================================================
@@ -1711,7 +1826,7 @@ int OrcaPrinterAgent::start_sdcard_print(PrintParams params, OnUpdateStatusFn up
// plugin). Never start a mapped print with the map silently dropped. // plugin). Never start a mapped print with the map silently dropped.
const nlohmann::json filament_mapping = build_filament_mapping(params.ams_mapping2); const nlohmann::json filament_mapping = build_filament_mapping(params.ams_mapping2);
if (!filament_mapping.empty()) { 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"; BOOST_LOG_TRIVIAL(warning) << "OrcaPrinterAgent: refusing mapped print, connector does not advertise filament_mapping";
return ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED; return ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED;
} }
+7
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.
@@ -129,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
@@ -2,6 +2,8 @@
#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/PresetBundle.hpp>
#include <libslic3r/PrintConfig.hpp> #include <libslic3r/PrintConfig.hpp>
@@ -17,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 {
@@ -48,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;
}; };
@@ -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); 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<TestPrinterAgent>("bbl"));
NetworkAgent network(nullptr, std::make_shared<TestPrinterAgent>("orca"));
DeviceManager manager(&network, false, &app_config.config);
BBLocalMachine orca_machine; CHECK(network.owns_agent("bbl"));
orca_machine.dev_id = "orca-device"; CHECK_FALSE(network.owns_agent("orca"));
orca_machine.dev_name = "Orca device"; CHECK(network.supports_command("bbl", "device", "print.ams_control"));
orca_machine.dev_ip = "192.0.2.21"; CHECK_FALSE(network.supports_command("orca", "device", "print.ams_control"));
orca_machine.printer_type = "C11"; CHECK_FALSE(network.supports_feature("orca", "device", "filament_mapping"));
MachineObject* orca_obj = manager.insert_local_device(orca_machine, "lan", "free", "", "access-code");
REQUIRE(orca_obj != nullptr);
orca_obj->printer_agent_id = "orca";
orca_obj->parse_new_info2(json::parse(R"({ // The mapping dialect is queried the same way: a legacy owner never inherits
"command": "get_capabilities", // Orca's, and a non-owner is never queried at all.
"supported_features": {"fms": true, "filament_slots": true, "filament_mapping": true}, CHECK_FALSE(network.uses_filament_mapping("bbl"));
"supported_commands": ["print.push_status", "print.ams_get_rfid"], CHECK_FALSE(network.uses_filament_mapping("orca"));
"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);
// Absent or false reads as unsupported, and a later reply without commands clears the set. NetworkAgent orca_network(nullptr, std::make_shared<TestPrinterAgent>("orca"));
orca_obj->parse_new_info2(json::parse(R"({ CHECK(orca_network.uses_filament_mapping("orca"));
"command": "get_capabilities", CHECK_FALSE(orca_network.uses_filament_mapping("bbl"));
"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());
} }
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<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; ScopedAppConfig app_config;
NetworkAgent network(nullptr, std::make_shared<TestPrinterAgent>("orca")); auto printer_agent = std::make_shared<TestPrinterAgent>("bbl");
NetworkAgent network(nullptr, printer_agent);
DeviceManager manager(&network, false, &app_config.config); DeviceManager manager(&network, false, &app_config.config);
BBLocalMachine machine; BBLocalMachine machine;
machine.dev_id = "orca-gate"; machine.dev_id = "bbl-setting-id";
machine.dev_name = "Orca gate"; machine.dev_name = "Bambu setting id";
machine.dev_ip = "192.0.2.30"; machine.dev_ip = "192.0.2.32";
machine.printer_type = "C11"; machine.printer_type = "C11";
MachineObject* obj = manager.insert_local_device(machine, "lan", "free", "", "access-code"); MachineObject* obj = manager.insert_local_device(machine, "lan", "free", "", "access-code");
REQUIRE(obj != nullptr); REQUIRE(obj != nullptr);
obj->printer_agent_id = "orca";
// fms off: no macro-backed AMS command is allowed even if listed. REQUIRE(obj->command_ams_filament_settings(0, 1, "GFL99", "preset-setting", "00FF00FF", "PLA", 190, 220) == 0);
obj->is_support_fms = false; const json payload = json::parse(printer_agent->last_message);
obj->supported_commands.insert("print.ams_control"); CHECK(payload["print"]["setting_id"] == "preset-setting");
CHECK_FALSE(obj->orca_ams_command_supported("print.ams_control")); }
// fms on: only the commands actually advertised are allowed. TEST_CASE("AMS user settings do not update local state when publishing fails", "[DeviceManager][integration]")
obj->is_support_fms = true; {
CHECK(obj->orca_ams_command_supported("print.ams_control")); ScopedAppConfig app_config;
CHECK_FALSE(obj->orca_ams_command_supported("print.ams_get_rfid")); auto printer_agent = std::make_shared<TestPrinterAgent>("bbl");
NetworkAgent network(nullptr, printer_agent);
DeviceManager manager(&network, false, &app_config.config);
// Bambu keeps the legacy permissive path. BBLocalMachine machine;
obj->printer_agent_id = "bbl"; machine.dev_id = "bbl-ams-setting-failure";
CHECK(obj->orca_ams_command_supported("print.anything")); 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 // The AMS dialogs resolve their filament list from the connected device's model. OrcaSonar's
@@ -25,6 +25,7 @@ struct Probe : OrcaPrinterAgent {
using OrcaPrinterAgent::lan_connection_target; using OrcaPrinterAgent::lan_connection_target;
using OrcaPrinterAgent::build_filament_mapping; using OrcaPrinterAgent::build_filament_mapping;
using OrcaPrinterAgent::build_gcode_file_payload; 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); 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));